Add options to always approve write and execute operations

This commit is contained in:
John Stearns
2024-11-01 13:38:47 -07:00
parent 4658e5cead
commit 3c8a9c09dd
11 changed files with 3555 additions and 390 deletions

View File

@@ -19,6 +19,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setCustomInstructions,
alwaysAllowReadOnly,
setAlwaysAllowReadOnly,
alwaysAllowWrite,
setAlwaysAllowWrite,
alwaysAllowExecute,
setAlwaysAllowExecute,
openRouterModels,
} = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
@@ -33,6 +37,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({ type: "customInstructions", text: customInstructions })
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
onDone()
}
}
@@ -130,6 +136,41 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={alwaysAllowWrite}
onChange={(e: any) => setAlwaysAllowWrite(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve write operations</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Cline will automatically write to files and create directories
without requiring you to click the Approve button.
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={alwaysAllowExecute}
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve execute operations</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Cline will automatically CLI commands without requiring
you to click the Approve button.
</p>
</div>
{IS_DEV && (
<>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>

View File

@@ -0,0 +1,230 @@
import { render, screen, act } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ExtensionStateContextType } from '../../../context/ExtensionStateContext'
import SettingsView from '../SettingsView'
import { vscode } from '../../../utils/vscode'
import * as ExtensionStateContext from '../../../context/ExtensionStateContext'
import { ModelInfo } from '../../../../../src/shared/api'
// Mock dependencies
jest.mock('../../../utils/vscode', () => ({
vscode: {
postMessage: jest.fn()
}
}))
// Mock validation functions
jest.mock('../../../utils/validate', () => ({
validateApiConfiguration: jest.fn(() => undefined),
validateModelId: jest.fn(() => undefined)
}))
// Mock ApiOptions component
jest.mock('../ApiOptions', () => ({
__esModule: true,
default: () => <div data-testid="mock-api-options" />
}))
// Mock VS Code components
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
VSCodeButton: ({ children, onClick }: any) => (
<button onClick={onClick}>{children}</button>
),
VSCodeCheckbox: ({ children, checked, onChange }: any) => (
<label>
<input
type="checkbox"
checked={checked}
onChange={e => onChange(e)}
aria-checked={checked}
/>
{children}
</label>
),
VSCodeTextArea: ({ children, value, onInput }: any) => (
<textarea
data-testid="custom-instructions"
value={value}
readOnly
aria-label="Custom Instructions"
>{children}</textarea>
),
VSCodeLink: ({ children, href }: any) => (
<a href={href}>{children}</a>
)
}))
describe('SettingsView', () => {
const mockOnDone = jest.fn()
const mockSetAlwaysAllowWrite = jest.fn()
const mockSetAlwaysAllowReadOnly = jest.fn()
const mockSetCustomInstructions = jest.fn()
const mockSetAlwaysAllowExecute = jest.fn()
let mockState: ExtensionStateContextType
const mockOpenRouterModels: Record<string, ModelInfo> = {
'claude-3-sonnet': {
maxTokens: 200000,
contextWindow: 200000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: true,
inputPrice: 0.000008,
outputPrice: 0.000024,
description: "Anthropic's Claude 3 Sonnet model"
}
}
beforeEach(() => {
jest.clearAllMocks()
mockState = {
apiConfiguration: {
apiProvider: 'anthropic',
apiModelId: 'claude-3-sonnet'
},
version: '1.0.0',
customInstructions: 'Test instructions',
alwaysAllowReadOnly: true,
alwaysAllowWrite: true,
alwaysAllowExecute: true,
openRouterModels: mockOpenRouterModels,
didHydrateState: true,
showWelcome: false,
theme: 'dark',
filePaths: [],
taskHistory: [],
shouldShowAnnouncement: false,
clineMessages: [],
uriScheme: 'vscode',
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
setAlwaysAllowWrite: mockSetAlwaysAllowWrite,
setCustomInstructions: mockSetCustomInstructions,
setAlwaysAllowExecute: mockSetAlwaysAllowExecute,
setApiConfiguration: jest.fn(),
setShowAnnouncement: jest.fn()
}
// Mock the useExtensionState hook
jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState)
})
const renderSettingsView = () => {
return render(
<SettingsView onDone={mockOnDone} />
)
}
describe('Checkboxes', () => {
it('should toggle alwaysAllowWrite checkbox', async () => {
mockState.alwaysAllowWrite = false
renderSettingsView()
const writeCheckbox = screen.getByRole('checkbox', {
name: /Always approve write operations/i
})
expect(writeCheckbox).not.toBeChecked()
await act(async () => {
await userEvent.click(writeCheckbox)
})
expect(mockSetAlwaysAllowWrite).toHaveBeenCalledWith(true)
})
it('should toggle alwaysAllowExecute checkbox', async () => {
mockState.alwaysAllowExecute = false
renderSettingsView()
const executeCheckbox = screen.getByRole('checkbox', {
name: /Always approve execute operations/i
})
expect(executeCheckbox).not.toBeChecked()
await act(async () => {
await userEvent.click(executeCheckbox)
})
expect(mockSetAlwaysAllowExecute).toHaveBeenCalledWith(true)
})
it('should toggle alwaysAllowReadOnly checkbox', async () => {
mockState.alwaysAllowReadOnly = false
renderSettingsView()
const readOnlyCheckbox = screen.getByRole('checkbox', {
name: /Always approve read-only operations/i
})
expect(readOnlyCheckbox).not.toBeChecked()
await act(async () => {
await userEvent.click(readOnlyCheckbox)
})
expect(mockSetAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
})
})
describe('Form Submission', () => {
it('should send correct messages when form is submitted', async () => {
renderSettingsView()
// Submit form
const doneButton = screen.getByRole('button', { name: /Done/i })
await act(async () => {
await userEvent.click(doneButton)
})
// Verify messages were sent in the correct order
const calls = (vscode.postMessage as jest.Mock).mock.calls
expect(calls).toHaveLength(5)
expect(calls[0][0]).toEqual({
type: 'apiConfiguration',
apiConfiguration: {
apiProvider: 'anthropic',
apiModelId: 'claude-3-sonnet'
}
})
expect(calls[1][0]).toEqual({
type: 'customInstructions',
text: 'Test instructions'
})
expect(calls[2][0]).toEqual({
type: 'alwaysAllowReadOnly',
bool: true
})
expect(calls[3][0]).toEqual({
type: 'alwaysAllowWrite',
bool: true
})
expect(calls[4][0]).toEqual({
type: 'alwaysAllowExecute',
bool: true
})
// Verify onDone was called
expect(mockOnDone).toHaveBeenCalled()
})
})
describe('Accessibility', () => {
it('should have accessible form controls', () => {
renderSettingsView()
// Check for proper labels and ARIA attributes
const writeCheckbox = screen.getByRole('checkbox', {
name: /Always approve write operations/i
})
expect(writeCheckbox).toHaveAttribute('aria-checked')
const textarea = screen.getByRole('textbox', {
name: /Custom Instructions/i
})
expect(textarea).toBeInTheDocument()
})
})
})

View File

@@ -11,7 +11,7 @@ import { vscode } from "../utils/vscode"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array"
interface ExtensionStateContextType extends ExtensionState {
export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: any
@@ -20,6 +20,8 @@ interface ExtensionStateContextType extends ExtensionState {
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setAlwaysAllowReadOnly: (value: boolean) => void
setAlwaysAllowWrite: (value: boolean) => void
setAlwaysAllowExecute: (value: boolean) => void
setShowAnnouncement: (value: boolean) => void
}
@@ -113,6 +115,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
}