From d632e621be79ab0dab306fc08a5904bfb988ecf3 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 28 Nov 2024 13:44:02 -0500 Subject: [PATCH 01/18] Simplify auto-approving code and make it work better with browser actions (#21) --- package.json | 9 - src/core/Cline.ts | 157 ++++------------- src/core/__tests__/Cline.test.ts | 164 ------------------ src/core/webview/ClineProvider.ts | 50 ++---- webview-ui/src/components/chat/ChatView.tsx | 66 ++++++- .../chat/__tests__/ChatView.test.tsx | 125 ++++++++++++- 6 files changed, 224 insertions(+), 347 deletions(-) diff --git a/package.json b/package.json index 47650653fd..9f0256cae8 100644 --- a/package.json +++ b/package.json @@ -116,15 +116,6 @@ "when": "view == claude-dev.SidebarProvider" } ] - }, - "configuration": { - "properties": { - "cline.alwaysAllowBrowser": { - "type": "boolean", - "default": false, - "description": "Always allow browser actions without requiring confirmation" - } - } } }, "scripts": { diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 98f05ac695..886f8354d1 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -56,16 +56,6 @@ type UserContent = Array< Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam > -const ALLOWED_AUTO_EXECUTE_COMMANDS = [ - 'npm', - 'npx', - 'tsc', - 'git log', - 'git diff', - 'git show', - 'list' -] as const - export class Cline { readonly taskId: string api: ApiHandler @@ -74,10 +64,6 @@ export class Cline { private browserSession: BrowserSession private didEditFile: boolean = false customInstructions?: string - alwaysAllowReadOnly: boolean - alwaysAllowWrite: boolean - alwaysAllowExecute: boolean - alwaysAllowBrowser: boolean apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] @@ -107,10 +93,6 @@ export class Cline { provider: ClineProvider, apiConfiguration: ApiConfiguration, customInstructions?: string, - alwaysAllowReadOnly?: boolean, - alwaysAllowWrite?: boolean, - alwaysAllowExecute?: boolean, - alwaysAllowBrowser?: boolean, task?: string, images?: string[], historyItem?: HistoryItem, @@ -122,10 +104,6 @@ export class Cline { this.browserSession = new BrowserSession(provider.context) this.diffViewProvider = new DiffViewProvider(cwd) this.customInstructions = customInstructions - this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false - this.alwaysAllowWrite = alwaysAllowWrite ?? false - this.alwaysAllowExecute = alwaysAllowExecute ?? false - this.alwaysAllowBrowser = alwaysAllowBrowser ?? false if (historyItem) { this.taskId = historyItem.id @@ -138,25 +116,6 @@ export class Cline { } } - private isAllowedCommand(command?: string): boolean { - if (!command) { - return false; - } - // Check for command chaining characters - if (command.includes('&&') || - command.includes(';') || - command.includes('||') || - command.includes('|') || - command.includes('$(') || - command.includes('`')) { - return false; - } - const trimmedCommand = command.trim().toLowerCase(); - return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix => - trimmedCommand.startsWith(prefix.toLowerCase()) - ); - } - // Storing task to disk for history private async ensureTaskDirectoryExists(): Promise { @@ -1101,11 +1060,7 @@ export class Cline { if (block.partial) { // update gui message const partialMessage = JSON.stringify(sharedMessageProps) - if (this.alwaysAllowWrite) { - await this.say("tool", partialMessage, undefined, block.partial) - } else { - await this.ask("tool", partialMessage, block.partial).catch(() => {}) - } + await this.ask("tool", partialMessage, block.partial).catch(() => {}) // update editor if (!this.diffViewProvider.isEditing) { // open the editor and prepare to stream content in @@ -1135,11 +1090,7 @@ export class Cline { if (!this.diffViewProvider.isEditing) { // show gui message before showing edit animation const partialMessage = JSON.stringify(sharedMessageProps) - if (this.alwaysAllowWrite) { - await this.say("tool", partialMessage, undefined, true) - } else { - await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor - } + await this.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor await this.diffViewProvider.open(relPath) } await this.diffViewProvider.update(newContent, true) @@ -1158,7 +1109,7 @@ export class Cline { ) : undefined, } satisfies ClineSayTool) - const didApprove = this.alwaysAllowWrite || (await askApproval("tool", completeMessage)) + const didApprove = await askApproval("tool", completeMessage) if (!didApprove) { await this.diffViewProvider.revertChanges() break @@ -1211,11 +1162,7 @@ export class Cline { ...sharedMessageProps, content: undefined, } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", partialMessage, undefined, block.partial) - } else { - await this.ask("tool", partialMessage, block.partial).catch(() => {}) - } + await this.ask("tool", partialMessage, block.partial).catch(() => {}) break } else { if (!relPath) { @@ -1229,13 +1176,9 @@ export class Cline { ...sharedMessageProps, content: absolutePath, } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", completeMessage, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message - } else { - const didApprove = await askApproval("tool", completeMessage) - if (!didApprove) { - break - } + const didApprove = await askApproval("tool", completeMessage) + if (!didApprove) { + break } // now execute the tool like normal const content = await extractTextFromFile(absolutePath) @@ -1261,11 +1204,7 @@ export class Cline { ...sharedMessageProps, content: "", } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", partialMessage, undefined, block.partial) - } else { - await this.ask("tool", partialMessage, block.partial).catch(() => {}) - } + await this.ask("tool", partialMessage, block.partial).catch(() => {}) break } else { if (!relDirPath) { @@ -1281,13 +1220,9 @@ export class Cline { ...sharedMessageProps, content: result, } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", completeMessage, undefined, false) - } else { - const didApprove = await askApproval("tool", completeMessage) - if (!didApprove) { - break - } + const didApprove = await askApproval("tool", completeMessage) + if (!didApprove) { + break } pushToolResult(result) break @@ -1309,11 +1244,7 @@ export class Cline { ...sharedMessageProps, content: "", } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", partialMessage, undefined, block.partial) - } else { - await this.ask("tool", partialMessage, block.partial).catch(() => {}) - } + await this.ask("tool", partialMessage, block.partial).catch(() => {}) break } else { if (!relDirPath) { @@ -1330,13 +1261,9 @@ export class Cline { ...sharedMessageProps, content: result, } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", completeMessage, undefined, false) - } else { - const didApprove = await askApproval("tool", completeMessage) - if (!didApprove) { - break - } + const didApprove = await askApproval("tool", completeMessage) + if (!didApprove) { + break } pushToolResult(result) break @@ -1362,11 +1289,7 @@ export class Cline { ...sharedMessageProps, content: "", } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", partialMessage, undefined, block.partial) - } else { - await this.ask("tool", partialMessage, block.partial).catch(() => {}) - } + await this.ask("tool", partialMessage, block.partial).catch(() => {}) break } else { if (!relDirPath) { @@ -1386,13 +1309,9 @@ export class Cline { ...sharedMessageProps, content: results, } satisfies ClineSayTool) - if (this.alwaysAllowReadOnly) { - await this.say("tool", completeMessage, undefined, false) - } else { - const didApprove = await askApproval("tool", completeMessage) - if (!didApprove) { - break - } + const didApprove = await askApproval("tool", completeMessage) + if (!didApprove) { + break } pushToolResult(results) break @@ -1421,24 +1340,11 @@ export class Cline { try { if (block.partial) { if (action === "launch") { - if (this.alwaysAllowBrowser) { - await this.say( - "browser_action", - JSON.stringify({ - action: action as BrowserAction, - coordinate: undefined, - text: undefined - } satisfies ClineSayBrowserAction), - undefined, - block.partial - ) - } else { - await this.ask( - "browser_action_launch", - removeClosingTag("url", url), - block.partial - ).catch(() => {}) - } + await this.ask( + "browser_action_launch", + removeClosingTag("url", url), + block.partial + ).catch(() => {}) } else { await this.say( "browser_action", @@ -1464,7 +1370,7 @@ export class Cline { break } this.consecutiveMistakeCount = 0 - const didApprove = this.alwaysAllowBrowser || await askApproval("browser_action_launch", url) + const didApprove = await askApproval("browser_action_launch", url) if (!didApprove) { break } @@ -1565,13 +1471,9 @@ export class Cline { const command: string | undefined = block.params.command try { if (block.partial) { - if (this.alwaysAllowExecute && this.isAllowedCommand(command)) { - await this.say("command", command, undefined, block.partial) - } else { - await this.ask("command", removeClosingTag("command", command), block.partial).catch( - () => {} - ) - } + await this.ask("command", removeClosingTag("command", command), block.partial).catch( + () => {} + ) break } else { if (!command) { @@ -1583,8 +1485,7 @@ export class Cline { } this.consecutiveMistakeCount = 0 - const didApprove = (this.alwaysAllowExecute && this.isAllowedCommand(command)) || - (await askApproval("command", command)) + const didApprove = await askApproval("command", command) if (!didApprove) { break } diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index fd9f03626f..65e595c2f4 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -231,40 +231,14 @@ describe('Cline', () => { }); describe('constructor', () => { - it('should initialize with default settings', () => { - const cline = new Cline( - mockProvider, - mockApiConfig, - undefined, // customInstructions - undefined, // alwaysAllowReadOnly - undefined, // alwaysAllowWrite - undefined, // alwaysAllowExecute - undefined, // alwaysAllowBrowser - 'test task' - ); - - expect(cline.alwaysAllowReadOnly).toBe(false); - expect(cline.alwaysAllowWrite).toBe(false); - expect(cline.alwaysAllowExecute).toBe(false); - expect(cline.alwaysAllowBrowser).toBe(false); - }); - it('should respect provided settings', () => { const cline = new Cline( mockProvider, mockApiConfig, 'custom instructions', - true, // alwaysAllowReadOnly - true, // alwaysAllowWrite - true, // alwaysAllowExecute - true, // alwaysAllowBrowser 'test task' ); - expect(cline.alwaysAllowReadOnly).toBe(true); - expect(cline.alwaysAllowWrite).toBe(true); - expect(cline.alwaysAllowExecute).toBe(true); - expect(cline.alwaysAllowBrowser).toBe(true); expect(cline.customInstructions).toBe('custom instructions'); }); @@ -277,142 +251,4 @@ describe('Cline', () => { }).toThrow('Either historyItem or task/images must be provided'); }); }); - - describe('file operations', () => { - let cline: Cline; - - beforeEach(() => { - cline = new Cline( - mockProvider, - mockApiConfig, - undefined, - false, - false, - false, - false, - 'test task' - ); - }); - - it('should bypass approval when alwaysAllowWrite is true', async () => { - const writeEnabledCline = new Cline( - mockProvider, - mockApiConfig, - undefined, - false, - true, // alwaysAllowWrite - false, - false, - 'test task' - ); - - expect(writeEnabledCline.alwaysAllowWrite).toBe(true); - // The write operation would bypass approval in actual implementation - }); - - it('should require approval when alwaysAllowWrite is false', async () => { - const writeDisabledCline = new Cline( - mockProvider, - mockApiConfig, - undefined, - false, - false, // alwaysAllowWrite - false, - false, - 'test task' - ); - - expect(writeDisabledCline.alwaysAllowWrite).toBe(false); - // The write operation would require approval in actual implementation - }); - }); - - describe('isAllowedCommand', () => { - let cline: any - - beforeEach(() => { - // Create a more complete mock provider - const mockProvider = { - context: { - globalStorageUri: { fsPath: '/mock/path' } - }, - postStateToWebview: jest.fn(), - postMessageToWebview: jest.fn(), - updateTaskHistory: jest.fn() - } - - // Mock the required dependencies - const mockApiConfig = { - getModel: () => ({ - id: 'claude-3-sonnet', - info: { supportsComputerUse: true } - }) - } - - // Create test instance with mocked constructor params - cline = new Cline( - mockProvider as any, - mockApiConfig as any, - undefined, // customInstructions - false, // alwaysAllowReadOnly - false, // alwaysAllowWrite - false, // alwaysAllowExecute - false, // alwaysAllowBrowser - 'test task' // task - ) - - // Mock internal methods that are called during initialization - cline.initiateTaskLoop = jest.fn() - cline.say = jest.fn() - cline.addToClineMessages = jest.fn() - cline.overwriteClineMessages = jest.fn() - cline.addToApiConversationHistory = jest.fn() - cline.overwriteApiConversationHistory = jest.fn() - }) - - test('returns true for allowed commands', () => { - expect(cline.isAllowedCommand('npm install')).toBe(true) - expect(cline.isAllowedCommand('npx create-react-app')).toBe(true) - expect(cline.isAllowedCommand('tsc --watch')).toBe(true) - expect(cline.isAllowedCommand('git log --oneline')).toBe(true) - expect(cline.isAllowedCommand('git diff main')).toBe(true) - }) - - test('returns true regardless of case or whitespace', () => { - expect(cline.isAllowedCommand('NPM install')).toBe(true) - expect(cline.isAllowedCommand(' npm install')).toBe(true) - expect(cline.isAllowedCommand('GIT DIFF')).toBe(true) - }) - - test('returns false for non-allowed commands', () => { - expect(cline.isAllowedCommand('rm -rf /')).toBe(false) - expect(cline.isAllowedCommand('git push')).toBe(false) - expect(cline.isAllowedCommand('git commit')).toBe(false) - expect(cline.isAllowedCommand('curl http://example.com')).toBe(false) - }) - - test('returns false for undefined or empty commands', () => { - expect(cline.isAllowedCommand()).toBe(false) - expect(cline.isAllowedCommand('')).toBe(false) - expect(cline.isAllowedCommand(' ')).toBe(false) - }) - - test('returns false for commands with chaining operators', () => { - const maliciousCommands = [ - 'npm install && rm -rf /', - 'git status; dangerous-command', - 'git log || evil-script', - 'git status | malicious-pipe', - 'git log $(evil-command)', - 'git status `rm -rf /`', - 'npm install && echo "malicious"', - 'git status; curl http://evil.com', - 'tsc --watch || wget malware', - ]; - - maliciousCommands.forEach(cmd => { - expect(cline.isAllowedCommand(cmd)).toBe(false); - }); - }); - }) }); diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4c446fbc33..1b3355d3e0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -197,20 +197,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { apiConfiguration, customInstructions, - alwaysAllowReadOnly, - alwaysAllowWrite, - alwaysAllowExecute, - alwaysAllowBrowser } = await this.getState() this.cline = new Cline( this, apiConfiguration, customInstructions, - alwaysAllowReadOnly, - alwaysAllowWrite, - alwaysAllowExecute, - alwaysAllowBrowser, task, images ) @@ -221,20 +213,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { apiConfiguration, customInstructions, - alwaysAllowReadOnly, - alwaysAllowWrite, - alwaysAllowExecute, - alwaysAllowBrowser } = await this.getState() this.cline = new Cline( this, apiConfiguration, customInstructions, - alwaysAllowReadOnly, - alwaysAllowWrite, - alwaysAllowExecute, - alwaysAllowBrowser, undefined, undefined, historyItem, @@ -440,23 +424,18 @@ export class ClineProvider implements vscode.WebviewViewProvider { break case "alwaysAllowReadOnly": await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined) - if (this.cline) { - this.cline.alwaysAllowReadOnly = message.bool ?? false - } await this.postStateToWebview() break case "alwaysAllowWrite": await this.updateGlobalState("alwaysAllowWrite", message.bool ?? undefined) - if (this.cline) { - this.cline.alwaysAllowWrite = message.bool ?? false - } await this.postStateToWebview() break case "alwaysAllowExecute": await this.updateGlobalState("alwaysAllowExecute", message.bool ?? undefined) - if (this.cline) { - this.cline.alwaysAllowExecute = message.bool ?? false - } + await this.postStateToWebview() + break + case "alwaysAllowBrowser": + await this.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) await this.postStateToWebview() break case "askResponse": @@ -530,13 +509,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { // await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list } - break - case "alwaysAllowBrowser": - await this.updateGlobalState("alwaysAllowBrowser", message.bool ?? undefined) - if (this.cline) { - this.cline.alwaysAllowBrowser = message.bool ?? false - } - await this.postStateToWebview() break // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) @@ -840,12 +812,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { apiConfiguration, lastShownAnnouncementId, - customInstructions, - alwaysAllowReadOnly, - alwaysAllowWrite, + customInstructions, + alwaysAllowReadOnly, + alwaysAllowWrite, alwaysAllowExecute, - alwaysAllowBrowser, - taskHistory + alwaysAllowBrowser, + taskHistory, } = await this.getState() return { @@ -947,8 +919,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, - taskHistory, alwaysAllowBrowser, + taskHistory, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -979,8 +951,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("alwaysAllowReadOnly") as Promise, this.getGlobalState("alwaysAllowWrite") as Promise, this.getGlobalState("alwaysAllowExecute") as Promise, - this.getGlobalState("taskHistory") as Promise, this.getGlobalState("alwaysAllowBrowser") as Promise, + this.getGlobalState("taskHistory") as Promise, ]) let apiProvider: ApiProvider diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index f378dd1c05..ff46c450b4 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -34,8 +34,18 @@ interface ChatViewProps { export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images +const ALLOWED_AUTO_EXECUTE_COMMANDS = [ + 'npm', + 'npx', + 'tsc', + 'git log', + 'git diff', + 'git show', + 'ls' +] as const + const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { - const { version, clineMessages: messages, taskHistory, apiConfiguration } = useExtensionState() + const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = useExtensionState() //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort) @@ -675,6 +685,60 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie [expandedRows, modifiedMessages, groupedMessages.length, toggleRowExpansion, handleRowHeightChange], ) + useEffect(() => { + // Only proceed if we have an ask and buttons are enabled + if (!clineAsk || !enableButtons) return + + const isReadOnlyToolAction = () => { + const lastMessage = messages.at(-1) + if (lastMessage?.type === "ask" && lastMessage.text) { + const tool = JSON.parse(lastMessage.text) + return ["readFile", "listFiles", "searchFiles"].includes(tool.tool) + } + return false + } + + const isWriteToolAction = () => { + const lastMessage = messages.at(-1) + if (lastMessage?.type === "ask" && lastMessage.text) { + const tool = JSON.parse(lastMessage.text) + return ["editedExistingFile", "newFileCreated"].includes(tool.tool) + } + return false + } + + const isAllowedCommand = () => { + const lastMessage = messages.at(-1) + if (lastMessage?.type === "ask" && lastMessage.text) { + const command = lastMessage.text + + // Check for command chaining characters + if (command.includes('&&') || + command.includes(';') || + command.includes('||') || + command.includes('|') || + command.includes('$(') || + command.includes('`')) { + return false + } + const trimmedCommand = command.trim().toLowerCase() + return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix => + trimmedCommand.startsWith(prefix.toLowerCase()) + ) + } + return false + } + + if ( + (alwaysAllowBrowser && clineAsk === "browser_action_launch") || + (alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) || + (alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) || + (alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) + ) { + handlePrimaryButtonClick() + } + }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages]) + return (
({ } })) - // Mock Virtuoso component jest.mock('react-virtuoso', () => ({ Virtuoso: ({ children }: any) => ( @@ -74,6 +73,7 @@ describe('ChatView', () => { alwaysAllowReadOnly: true, alwaysAllowWrite: true, alwaysAllowExecute: true, + alwaysAllowBrowser: true, openRouterModels: {}, didHydrateState: true, showWelcome: false, @@ -82,13 +82,14 @@ describe('ChatView', () => { taskHistory: [], shouldShowAnnouncement: false, uriScheme: 'vscode', - + + setApiConfiguration: jest.fn(), + setShowAnnouncement: jest.fn(), + setCustomInstructions: jest.fn(), setAlwaysAllowReadOnly: jest.fn(), setAlwaysAllowWrite: jest.fn(), - setCustomInstructions: jest.fn(), setAlwaysAllowExecute: jest.fn(), - setApiConfiguration: jest.fn(), - setShowAnnouncement: jest.fn() + setAlwaysAllowBrowser: jest.fn() } // Mock the useExtensionState hook @@ -106,6 +107,118 @@ describe('ChatView', () => { ) } + describe('Always Allow Logic', () => { + it('should auto-approve read-only tool actions when alwaysAllowReadOnly is true', () => { + mockState.clineMessages = [ + { + type: 'ask', + ask: 'tool', + text: JSON.stringify({ tool: 'readFile' }), + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + + it('should auto-approve write tool actions when alwaysAllowWrite is true', () => { + mockState.clineMessages = [ + { + type: 'ask', + ask: 'tool', + text: JSON.stringify({ tool: 'editedExistingFile' }), + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + + it('should auto-approve allowed execute commands when alwaysAllowExecute is true', () => { + mockState.clineMessages = [ + { + type: 'ask', + ask: 'command', + text: 'npm install', + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + + it('should not auto-approve disallowed execute commands even when alwaysAllowExecute is true', () => { + mockState.clineMessages = [ + { + type: 'ask', + ask: 'command', + text: 'rm -rf /', + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).not.toHaveBeenCalled() + }) + + it('should not auto-approve commands with chaining characters when alwaysAllowExecute is true', () => { + mockState.clineMessages = [ + { + type: 'ask', + ask: 'command', + text: 'npm install && rm -rf /', + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).not.toHaveBeenCalled() + }) + + it('should auto-approve browser actions when alwaysAllowBrowser is true', () => { + mockState.clineMessages = [ + { + type: 'ask', + ask: 'browser_action_launch', + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + + it('should not auto-approve when corresponding alwaysAllow flag is false', () => { + mockState.alwaysAllowReadOnly = false + mockState.clineMessages = [ + { + type: 'ask', + ask: 'tool', + text: JSON.stringify({ tool: 'readFile' }), + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).not.toHaveBeenCalled() + }) + }) + describe('Streaming State', () => { it('should show cancel button while streaming and trigger cancel on click', async () => { mockState.clineMessages = [ @@ -168,4 +281,4 @@ describe('ChatView', () => { }) }) }) -}) \ No newline at end of file +}) From cc3186c88faec9d92feba61499261e101b32a7fd Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 28 Nov 2024 13:52:58 -0500 Subject: [PATCH 02/18] Bump version to 2.1.3 (#22) --- CHANGELOG.md | 3 ++- package-lock.json | 4 ++-- package.json | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d951ac65cd..bf41aae05d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Change Log -## Roo Cline 2.1.2 +## Roo Cline 2.1.3 - Roo Cline now publishes to the VS Code Marketplace! +- Roo Cline now allows browser actions without approval when `alwaysAllowBrowser` is true ## [2.1.6] diff --git a/package-lock.json b/package-lock.json index e77ad7b656..00e9f42f1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.1.2", + "version": "2.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.1.2", + "version": "2.1.3", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 9f0256cae8..403143a9ea 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", "publisher": "RooVeterinaryInc", - "version": "2.1.2", + "version": "2.1.3", "files": [ - "bin/roo-cline-2.1.2.vsix", + "bin/roo-cline-2.1.3.vsix", "assets/icons/icon_Roo.png" ], "icon": "assets/icons/icon_Roo.png", From cafc2852ca03b86f34f6272cbaa8b24f6747b8ea Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 28 Nov 2024 14:25:41 -0500 Subject: [PATCH 03/18] Allow Roo-Cline to coexist with Cline (#23) Co-authored-by: ColemanRoo --- .github/workflows/marketplace-publish.yml | 2 +- CHANGELOG.md | 3 +- package-lock.json | 4 +-- package.json | 42 +++++++++++------------ src/core/webview/ClineProvider.ts | 4 +-- src/extension.ts | 10 +++--- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index 0084f9e5b5..d4f5547cda 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -28,5 +28,5 @@ jobs: run: | current_package_version=$(node -p "require('./package.json').version") npm run vsix - npm run publish:marketplace + npm run publish:marketplace -- --pat VSCE_PAT echo "Successfully published version $current_package_version to VS Code Marketplace" diff --git a/CHANGELOG.md b/CHANGELOG.md index bf41aae05d..1f04ff2dfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Change Log -## Roo Cline 2.1.3 +## Roo Cline 2.1.4 - Roo Cline now publishes to the VS Code Marketplace! - Roo Cline now allows browser actions without approval when `alwaysAllowBrowser` is true +- Roo Cline now can run side-by-side with Cline ## [2.1.6] diff --git a/package-lock.json b/package-lock.json index 00e9f42f1c..73366d4717 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.1.3", + "version": "2.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.1.3", + "version": "2.1.4", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 403143a9ea..8fc02c9826 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", "publisher": "RooVeterinaryInc", - "version": "2.1.3", + "version": "2.1.4", "files": [ - "bin/roo-cline-2.1.3.vsix", + "bin/roo-cline-2.1.4.vsix", "assets/icons/icon_Roo.png" ], "icon": "assets/icons/icon_Roo.png", @@ -51,69 +51,69 @@ "viewsContainers": { "activitybar": [ { - "id": "claude-dev-ActivityBar", - "title": "Cline", + "id": "roo-cline-ActivityBar", + "title": "Roo Cline", "icon": "$(robot)" } ] }, "views": { - "claude-dev-ActivityBar": [ + "roo-cline-ActivityBar": [ { "type": "webview", - "id": "claude-dev.SidebarProvider", + "id": "roo-cline.SidebarProvider", "name": "" } ] }, "commands": [ { - "command": "cline.plusButtonClicked", + "command": "roo-cline.plusButtonClicked", "title": "New Task", "icon": "$(add)" }, { - "command": "cline.historyButtonClicked", + "command": "roo-cline.historyButtonClicked", "title": "History", "icon": "$(history)" }, { - "command": "cline.popoutButtonClicked", + "command": "roo-cline.popoutButtonClicked", "title": "Open in Editor", "icon": "$(link-external)" }, { - "command": "cline.settingsButtonClicked", + "command": "roo-cline.settingsButtonClicked", "title": "Settings", "icon": "$(settings-gear)" }, { - "command": "cline.openInNewTab", + "command": "roo-cline.openInNewTab", "title": "Open In New Tab", - "category": "Cline" + "category": "Roo Cline" } ], "menus": { "view/title": [ { - "command": "cline.plusButtonClicked", + "command": "roo-cline.plusButtonClicked", "group": "navigation@1", - "when": "view == claude-dev.SidebarProvider" + "when": "view == roo-cline.SidebarProvider" }, { - "command": "cline.historyButtonClicked", + "command": "roo-cline.historyButtonClicked", "group": "navigation@2", - "when": "view == claude-dev.SidebarProvider" + "when": "view == roo-cline.SidebarProvider" }, { - "command": "cline.popoutButtonClicked", + "command": "roo-cline.popoutButtonClicked", "group": "navigation@3", - "when": "view == claude-dev.SidebarProvider" + "when": "view == roo-cline.SidebarProvider" }, { - "command": "cline.settingsButtonClicked", + "command": "roo-cline.settingsButtonClicked", "group": "navigation@4", - "when": "view == claude-dev.SidebarProvider" + "when": "view == roo-cline.SidebarProvider" } ] } @@ -136,7 +136,7 @@ "start:webview": "cd webview-ui && npm run start", "build:webview": "cd webview-ui && npm run build", "test:webview": "cd webview-ui && npm run test", - "publish:marketplace": "vsce publish && ovsx publish" + "publish:marketplace": "vsce publish" }, "devDependencies": { "@types/diff": "^5.2.1", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1b3355d3e0..25254e7b22 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -68,8 +68,8 @@ export const GlobalFileNames = { } export class ClineProvider implements vscode.WebviewViewProvider { - public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension. - public static readonly tabPanelId = "claude-dev.TabPanelProvider" + public static readonly sideBarId = "roo-cline.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension. + public static readonly tabPanelId = "roo-cline.TabPanelProvider" private static activeInstances: Set = new Set() private disposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel diff --git a/src/extension.ts b/src/extension.ts index 6a3dad32fb..bd3e780c1d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -35,7 +35,7 @@ export function activate(context: vscode.ExtensionContext) { ) context.subscriptions.push( - vscode.commands.registerCommand("cline.plusButtonClicked", async () => { + vscode.commands.registerCommand("roo-cline.plusButtonClicked", async () => { outputChannel.appendLine("Plus button Clicked") await sidebarProvider.clearTask() await sidebarProvider.postStateToWebview() @@ -76,18 +76,18 @@ export function activate(context: vscode.ExtensionContext) { await vscode.commands.executeCommand("workbench.action.lockEditorGroup") } - context.subscriptions.push(vscode.commands.registerCommand("cline.popoutButtonClicked", openClineInNewTab)) - context.subscriptions.push(vscode.commands.registerCommand("cline.openInNewTab", openClineInNewTab)) + context.subscriptions.push(vscode.commands.registerCommand("roo-cline.popoutButtonClicked", openClineInNewTab)) + context.subscriptions.push(vscode.commands.registerCommand("roo-cline.openInNewTab", openClineInNewTab)) context.subscriptions.push( - vscode.commands.registerCommand("cline.settingsButtonClicked", () => { + vscode.commands.registerCommand("roo-cline.settingsButtonClicked", () => { //vscode.window.showInformationMessage(message) sidebarProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) }), ) context.subscriptions.push( - vscode.commands.registerCommand("cline.historyButtonClicked", () => { + vscode.commands.registerCommand("roo-cline.historyButtonClicked", () => { sidebarProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" }) }), ) From 178fd3ac34a3659d0f2d35418bde6eb3b466a377 Mon Sep 17 00:00:00 2001 From: ColemanRoo <117104599+ColemanRoo@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:32:00 -0600 Subject: [PATCH 04/18] Fix marketplace publish (#24) --- .github/workflows/marketplace-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/marketplace-publish.yml b/.github/workflows/marketplace-publish.yml index d4f5547cda..0084f9e5b5 100644 --- a/.github/workflows/marketplace-publish.yml +++ b/.github/workflows/marketplace-publish.yml @@ -28,5 +28,5 @@ jobs: run: | current_package_version=$(node -p "require('./package.json').version") npm run vsix - npm run publish:marketplace -- --pat VSCE_PAT + npm run publish:marketplace echo "Successfully published version $current_package_version to VS Code Marketplace" From 6fed90805a68b680483b4fb0e0b9e42d791c9b5e Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 29 Nov 2024 12:35:33 -0500 Subject: [PATCH 05/18] Add a few more read-only operations (#25) --- package-lock.json | 4 +-- package.json | 4 +-- webview-ui/src/components/chat/ChatView.tsx | 2 +- .../chat/__tests__/ChatView.test.tsx | 25 +++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 73366d4717..c166721f4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.1.4", + "version": "2.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.1.4", + "version": "2.1.5", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 8fc02c9826..30b2638213 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", "publisher": "RooVeterinaryInc", - "version": "2.1.4", + "version": "2.1.5", "files": [ - "bin/roo-cline-2.1.4.vsix", + "bin/roo-cline-2.1.5.vsix", "assets/icons/icon_Roo.png" ], "icon": "assets/icons/icon_Roo.png", diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index ff46c450b4..b135fb0407 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -693,7 +693,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie const lastMessage = messages.at(-1) if (lastMessage?.type === "ask" && lastMessage.text) { const tool = JSON.parse(lastMessage.text) - return ["readFile", "listFiles", "searchFiles"].includes(tool.tool) + return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool) } return false } diff --git a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx index 95fd104d8f..6830eca52e 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx @@ -125,6 +125,31 @@ describe('ChatView', () => { }) }) + it('should auto-approve all file listing tool types when alwaysAllowReadOnly is true', () => { + const fileListingTools = [ + 'readFile', 'listFiles', 'listFilesTopLevel', + 'listFilesRecursive', 'listCodeDefinitionNames', 'searchFiles' + ] + + fileListingTools.forEach(tool => { + jest.clearAllMocks() + mockState.clineMessages = [ + { + type: 'ask', + ask: 'tool', + text: JSON.stringify({ tool }), + ts: Date.now(), + } + ] + renderChatView() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + }) + it('should auto-approve write tool actions when alwaysAllowWrite is true', () => { mockState.clineMessages = [ { From 55e75f254ee5487a5200f159dfed9225bad38f91 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 29 Nov 2024 22:03:54 -0500 Subject: [PATCH 06/18] Relax vscode version targeting (#27) --- package-lock.json | 6 +++--- package.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c166721f4b..8699801e08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.1.5", + "version": "2.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.1.5", + "version": "2.1.6", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", @@ -57,7 +57,7 @@ "typescript": "^5.4.5" }, "engines": { - "vscode": "1.93.1" + "vscode": "^1.93.1" } }, "node_modules/@ampproject/remapping": { diff --git a/package.json b/package.json index 30b2638213..241faf1f6e 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", "publisher": "RooVeterinaryInc", - "version": "2.1.5", + "version": "2.1.6", "files": [ - "bin/roo-cline-2.1.5.vsix", + "bin/roo-cline-2.1.6.vsix", "assets/icons/icon_Roo.png" ], "icon": "assets/icons/icon_Roo.png", @@ -14,7 +14,7 @@ "theme": "dark" }, "engines": { - "vscode": "1.93.1" + "vscode": "^1.93.1" }, "author": { "name": "Roo Vet" From 987add36536388786384745ab035ed9af3971e19 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sat, 30 Nov 2024 23:32:51 -0500 Subject: [PATCH 07/18] Update metadata (#29) --- README.md | 28 +--------------------------- assets/icons/icon.png | Bin 9385 -> 0 bytes assets/icons/icon_Roo.png | Bin 11161 -> 0 bytes assets/icons/robot_panel_dark.png | Bin 718 -> 0 bytes assets/icons/robot_panel_light.png | Bin 689 -> 0 bytes assets/icons/rocket.png | Bin 0 -> 2140 bytes package-lock.json | 4 ++-- package.json | 14 +++++++------- 8 files changed, 10 insertions(+), 36 deletions(-) delete mode 100644 assets/icons/icon.png delete mode 100644 assets/icons/icon_Roo.png delete mode 100644 assets/icons/robot_panel_dark.png delete mode 100644 assets/icons/robot_panel_light.png create mode 100644 assets/icons/rocket.png diff --git a/README.md b/README.md index f2cf691a31..2784e368ae 100644 --- a/README.md +++ b/README.md @@ -7,33 +7,7 @@ 1. Bump the version in `package.json` 2. Update the version number in the `files` list in `package.json` -### Packaging -1. Bump the version in `package.json` -2. Remove the old VSIX file: - ```bash - rm bin/roo-cline-*.vsix - ``` -3. Build the VSIX file: - ```bash - npm run vsix - ``` -4. The new VSIX file will be created in the `bin/` directory -5. Commit the new VSIX file to git: - ```bash - git add bin/*.vsix - git commit -m "chore: update VSIX to version " - ``` - -### Installation -Install the plugin using the Cursor CLI: - -```bash -cursor --install-extension bin/roo-cline-.vsix -``` - -Note: The VSIX file is checked into the git repository's `bin/` directory for easy distribution. - -After installation, Roo Cline will appear in your Cursor's installed extensions list. You can verify this by opening Cursor's Extensions panel (Cmd/Ctrl+Shift+X) and checking under the "Installed" section. +After installation, Roo Cline will appear in your VSCode-compatible editor's installed extensions list. You can verify this by opening your editor's Extensions panel (Cmd/Ctrl+Shift+X) and checking under the "Installed" section. --- diff --git a/assets/icons/icon.png b/assets/icons/icon.png deleted file mode 100644 index e8736aaa02433a094b7696f9b0f17ade498c7b62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9385 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 diff --git a/assets/icons/robot_panel_dark.png b/assets/icons/robot_panel_dark.png deleted file mode 100644 index 0ed7cc6274eef604044cc14e1225f918d8f65806..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 718 zcmeAS@N?(olHy`uVBq!ia0vp^GC(ZH!3HGlkJilxQjEnx?oJHr&dIz4a$Hg)JkxxA z8MJ_G4hF{dOa>N^5+IfWVg?501&j>LK$;OGwtxvPE3<$Z&Xxlyl&f8Sh=GC0!qdeu z#DjP5w2gi;h5~Kh1sa={cs6qcT)gx|HfV{*^e*-XmNzD;={vMf_{5+o5Y#foD0P>| z#30pOAt6gT1$6Ge|0Rjor?~ckqhyN3T|w*p>2a zyVKiCwM4J>T%5YpZ0&@MX<1z>Yr|IPKjliTT~+p=&~fDy?I~v`BZZYFpJd4dKJne9u;Co&%anWB(s%qAo zq+B|)e#(+<4$BTrSDw|sXvxE^1$XPNxdi>zyM6kEr0}Jpu8edcfo}~{zT3R6uxMPi zExc@I==Y!Nx;cxYA1H6w9Q<+GRhuKL`=rvZ3DG9tQJ8ezO)NQsYCJKM7TOr4{JH6|2wbcplQzd7$ zFWz#pn`J)T;F?-1#X!SoMl3&c;&los!romI9g?oE@jp;?MaXIOR+k-8 z9I_kEAJpFWR9?Ho*0d_&UX$$n|DE=W|80-IUwq}Y*o(FA^iB1HiXPVcbN^?Kd!ya< tExelV&EB>T`+cKs`fKi;?*D661$R-{bN#Z%$vZ%4$N^5+IfWVg?501&j>LK$;OGwtxvPE3<$Z&Xxlyl&f8Sh=GAg+SA1` z#DjP5wB26JjuNelD>SbhbK4WT&1_$DYwO%lxdY#CTskJcL0FMLL9vM?DC!nx(^MgC z?Oc~VbMiN{tL@%A^N5O^#mxGDr+=oWwcj<7TAb+kQax$Ag4w>6jcoR+k7i5Vh}@~T zZ=s=eNt%A4-6Z+Xu9vFT1xn9zpOUoCbyl;fx$T^^O8{IY=@j0H!Z(7AM^G4>;l!iTr$Vq zbwtCx_^wyZ>z`S7nz5|;y;liyPR)^^!lh3(5NX>!-m?26-!QTm8j;5oj;|`O^XA2b=bGYd?Lkzkc4ozszSoZgV|1N53?<=dRaI z_9m&UzgyjnYf5U5s_b4V|II=<>ZI#tslMsyuj&~Wn=EaMGSRaB&3fXd=!M0T)1HBn Ni>Irf%Q~loCICz`6p{b{ diff --git a/assets/icons/rocket.png b/assets/icons/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..1ffd65e70e050e4c79aa8ce99bb9602b62717f55 GIT binary patch literal 2140 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 SNA7+A0000 Date: Sun, 1 Dec 2024 05:59:33 -0800 Subject: [PATCH 08/18] docs(README.md): fix link (#30) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2784e368ae..8cacf91489 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ After installation, Roo Cline will appear in your VSCode-compatible editor's ins
-Download on VS Marketplace +Download on VS Marketplace Join the Discord From 6b8f9f7a457c17f215254eec3be3b166bf0f043a Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 1 Dec 2024 15:34:36 -0500 Subject: [PATCH 09/18] Expose a list of allowed auto-execute commands (#31) --- CHANGELOG.md | 4 +- package.json | 26 +- src/core/webview/ClineProvider.ts | 16 + src/extension.ts | 10 + src/shared/ExtensionMessage.ts | 7 +- src/shared/WebviewMessage.ts | 2 + webview-ui/package-lock.json | 243 +++-- webview-ui/package.json | 9 + webview-ui/src/components/chat/ChatView.tsx | 35 +- .../chat/__tests__/ChatView.test.tsx | 832 +++++++++++------- .../src/components/settings/SettingsView.tsx | 148 +++- .../settings/__tests__/SettingsView.test.tsx | 409 +++++---- .../src/context/ExtensionStateContext.tsx | 3 + .../__tests__/ExtensionStateContext.test.tsx | 60 ++ 14 files changed, 1085 insertions(+), 719 deletions(-) create mode 100644 webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f04ff2dfa..f4cbefc0f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Change Log -## Roo Cline 2.1.4 - +## Roo Cline 2.1.8 - Roo Cline now publishes to the VS Code Marketplace! - Roo Cline now allows browser actions without approval when `alwaysAllowBrowser` is true - Roo Cline now can run side-by-side with Cline +- Roo Cline now allows configuration of allowed commands without approval ## [2.1.6] diff --git a/package.json b/package.json index d7b1f021c0..2470f60958 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,7 @@ "displayName": "Roo Cline", "description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.", "publisher": "RooVeterinaryInc", - "version": "2.1.7", - "files": [ - "bin/roo-cline-2.1.7.vsix", - "assets/icons/rocket.png" - ], + "version": "2.1.8", "icon": "assets/icons/rocket.png", "galleryBanner": { "color": "#617A91", @@ -116,6 +112,26 @@ "when": "view == roo-cline.SidebarProvider" } ] + }, + "configuration": { + "title": "RooCline", + "properties": { + "roo-cline.allowedCommands": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "npm test", + "npm install", + "tsc", + "git log", + "git diff", + "git show" + ], + "description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled" + } + } } }, "scripts": { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 25254e7b22..fbfc5a1d9c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -60,6 +60,7 @@ type GlobalStateKey = | "azureApiVersion" | "openRouterModelId" | "openRouterModelInfo" + | "allowedCommands" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -510,6 +511,13 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break + case "allowedCommands": + await this.context.globalState.update('allowedCommands', message.commands); + // Also update workspace settings + await vscode.workspace + .getConfiguration('roo-cline') + .update('allowedCommands', message.commands, vscode.ConfigurationTarget.Global); + break; // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } @@ -820,6 +828,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { taskHistory, } = await this.getState() + const allowedCommands = vscode.workspace + .getConfiguration('roo-cline') + .get('allowedCommands') || [] + return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -834,6 +846,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { .filter((item) => item.ts && item.task) .sort((a, b) => b.ts - a.ts), shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, + allowedCommands, } } @@ -921,6 +934,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowExecute, alwaysAllowBrowser, taskHistory, + allowedCommands, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -953,6 +967,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("alwaysAllowExecute") as Promise, this.getGlobalState("alwaysAllowBrowser") as Promise, this.getGlobalState("taskHistory") as Promise, + this.getGlobalState("allowedCommands") as Promise, ]) let apiProvider: ApiProvider @@ -1003,6 +1018,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowBrowser: alwaysAllowBrowser ?? false, taskHistory, + allowedCommands, } } diff --git a/src/extension.ts b/src/extension.ts index bd3e780c1d..659c1690d2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,16 @@ export function activate(context: vscode.ExtensionContext) { outputChannel.appendLine("Cline extension activated") + // Get default commands from configuration + const defaultCommands = vscode.workspace + .getConfiguration('roo-cline') + .get('allowedCommands') || []; + + // Initialize global state if not already set + if (!context.globalState.get('allowedCommands')) { + context.globalState.update('allowedCommands', defaultCommands); + } + const sidebarProvider = new ClineProvider(context, outputChannel) context.subscriptions.push( diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index beae01a35d..a4f687b34a 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -30,6 +30,9 @@ export interface ExtensionMessage { export interface ExtensionState { version: string + clineMessages: ClineMessage[] + taskHistory: HistoryItem[] + shouldShowAnnouncement: boolean apiConfiguration?: ApiConfiguration customInstructions?: string alwaysAllowReadOnly?: boolean @@ -37,9 +40,7 @@ export interface ExtensionState { alwaysAllowExecute?: boolean alwaysAllowBrowser?: boolean uriScheme?: string - clineMessages: ClineMessage[] - taskHistory: HistoryItem[] - shouldShowAnnouncement: boolean + allowedCommands?: string[] } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index ba23355b56..c77af6a46a 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -4,6 +4,7 @@ export interface WebviewMessage { type: | "apiConfiguration" | "customInstructions" + | "allowedCommands" | "alwaysAllowReadOnly" | "alwaysAllowWrite" | "alwaysAllowExecute" @@ -31,6 +32,7 @@ export interface WebviewMessage { apiConfiguration?: ApiConfiguration images?: string[] bool?: boolean + commands?: string[] } export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse" diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index ee8d460f5e..0a750e092e 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -33,6 +33,7 @@ "web-vitals": "^2.1.4" }, "devDependencies": { + "@babel/plugin-transform-private-property-in-object": "^7.25.9", "@types/vscode-webview": "^1.57.5" } }, @@ -68,12 +69,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "license": "MIT", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -165,27 +166,26 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "license": "MIT", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", "dependencies": { - "@babel/types": "^7.24.7", + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -230,19 +230,16 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", "semver": "^6.3.1" }, "engines": { @@ -341,13 +338,12 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -386,22 +382,20 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", - "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", "engines": { "node": ">=6.9.0" } @@ -424,14 +418,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -454,13 +447,12 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", - "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -479,19 +471,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } @@ -533,26 +523,13 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "license": "MIT", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dependencies": { + "@babel/types": "^7.26.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -727,18 +704,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -1631,15 +1596,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", - "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2055,6 +2018,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://roo-815250993495.d.codeartifact.us-east-1.amazonaws.com/npm/roo-dev/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2136,33 +2110,28 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "license": "MIT", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2171,14 +2140,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "license": "MIT", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -13354,15 +13321,14 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "license": "MIT", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -19238,15 +19204,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "license": "BSD-3-Clause" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index cc5beb6397..e9801f044a 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -52,6 +52,15 @@ ] }, "devDependencies": { + "@babel/plugin-transform-private-property-in-object": "^7.25.9", "@types/vscode-webview": "^1.57.5" + }, + "jest": { + "transformIgnorePatterns": [ + "/node_modules/(?!(rehype-highlight|react-remark|unist-util-visit|vfile|unified|bail|is-plain-obj|trough|vfile-message|unist-util-stringify-position|mdast-util-from-markdown|mdast-util-to-string|micromark|decode-named-character-reference|character-entities|markdown-table|zwitch|longest-streak|escape-string-regexp|unist-util-is|hast-util-to-text)/)" + ], + "moduleNameMapper": { + "\\.(css|less|scss|sass)$": "identity-obj-proxy" + } } } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index b135fb0407..63ea4e21fe 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -34,18 +34,8 @@ interface ChatViewProps { export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images -const ALLOWED_AUTO_EXECUTE_COMMANDS = [ - 'npm', - 'npx', - 'tsc', - 'git log', - 'git diff', - 'git show', - 'ls' -] as const - const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { - const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = useExtensionState() + const { version, clineMessages: messages, taskHistory, apiConfiguration, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, allowedCommands } = useExtensionState() //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort) @@ -712,19 +702,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie if (lastMessage?.type === "ask" && lastMessage.text) { const command = lastMessage.text - // Check for command chaining characters - if (command.includes('&&') || - command.includes(';') || - command.includes('||') || - command.includes('|') || - command.includes('$(') || - command.includes('`')) { - return false - } - const trimmedCommand = command.trim().toLowerCase() - return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix => - trimmedCommand.startsWith(prefix.toLowerCase()) - ) + // Split command by chaining operators + const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim()) + + // Check if all individual commands are allowed + return commands.every((cmd) => { + const trimmedCommand = cmd.toLowerCase() + return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase())) + }) } return false } @@ -737,7 +722,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie ) { handlePrimaryButtonClick() } - }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages]) + }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages, allowedCommands]) return (
({ - vscode: { - postMessage: jest.fn() - } -})) - -// Mock all components that use problematic dependencies -jest.mock('../../common/CodeBlock', () => ({ - __esModule: true, - default: () =>
-})) - -jest.mock('../../common/MarkdownBlock', () => ({ - __esModule: true, - default: () =>
+ vscode: { + postMessage: jest.fn(), + }, })) +// Mock components that use ESM dependencies jest.mock('../BrowserSessionRow', () => ({ - __esModule: true, - default: () =>
+ __esModule: true, + default: function MockBrowserSessionRow({ messages }: { messages: ClineMessage[] }) { + return
{JSON.stringify(messages)}
+ } })) -// Update ChatRow mock to capture props -let chatRowProps = null jest.mock('../ChatRow', () => ({ + __esModule: true, + default: function MockChatRow({ message }: { message: ClineMessage }) { + return
{JSON.stringify(message)}
+ } +})) + +interface ChatTextAreaProps { + onSend: (value: string) => void; + inputValue?: string; + textAreaDisabled?: boolean; + placeholderText?: string; + selectedImages?: string[]; + shouldDisableImages?: boolean; +} + +jest.mock('../ChatTextArea', () => { + const mockReact = require('react') + return { __esModule: true, - default: (props: any) => { - chatRowProps = props - return
- } -})) - -// Mock Virtuoso component -jest.mock('react-virtuoso', () => ({ - Virtuoso: ({ children }: any) => ( -
{children}
- ) -})) - -// Mock VS Code components -jest.mock('@vscode/webview-ui-toolkit/react', () => ({ - VSCodeButton: ({ children, onClick }: any) => ( - - ), - VSCodeProgressRing: () =>
-})) - -describe('ChatView', () => { - const mockShowHistoryView = jest.fn() - const mockHideAnnouncement = jest.fn() - - let mockState: ExtensionStateContextType - - beforeEach(() => { - jest.clearAllMocks() - - mockState = { - clineMessages: [], - apiConfiguration: { - apiProvider: 'anthropic', - apiModelId: 'claude-3-sonnet' - }, - version: '1.0.0', - customInstructions: '', - alwaysAllowReadOnly: true, - alwaysAllowWrite: true, - alwaysAllowExecute: true, - alwaysAllowBrowser: true, - openRouterModels: {}, - didHydrateState: true, - showWelcome: false, - theme: 'dark', - filePaths: [], - taskHistory: [], - shouldShowAnnouncement: false, - uriScheme: 'vscode', - - setApiConfiguration: jest.fn(), - setShowAnnouncement: jest.fn(), - setCustomInstructions: jest.fn(), - setAlwaysAllowReadOnly: jest.fn(), - setAlwaysAllowWrite: jest.fn(), - setAlwaysAllowExecute: jest.fn(), - setAlwaysAllowBrowser: jest.fn() - } - - // Mock the useExtensionState hook - jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState) - }) - - const renderChatView = () => { - return render( - - ) - } - - describe('Always Allow Logic', () => { - it('should auto-approve read-only tool actions when alwaysAllowReadOnly is true', () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'tool', - text: JSON.stringify({ tool: 'readFile' }), - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'askResponse', - askResponse: 'yesButtonClicked' - }) - }) - - it('should auto-approve all file listing tool types when alwaysAllowReadOnly is true', () => { - const fileListingTools = [ - 'readFile', 'listFiles', 'listFilesTopLevel', - 'listFilesRecursive', 'listCodeDefinitionNames', 'searchFiles' - ] - - fileListingTools.forEach(tool => { - jest.clearAllMocks() - mockState.clineMessages = [ - { - type: 'ask', - ask: 'tool', - text: JSON.stringify({ tool }), - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'askResponse', - askResponse: 'yesButtonClicked' - }) - }) - }) - - it('should auto-approve write tool actions when alwaysAllowWrite is true', () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'tool', - text: JSON.stringify({ tool: 'editedExistingFile' }), - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'askResponse', - askResponse: 'yesButtonClicked' - }) - }) - - it('should auto-approve allowed execute commands when alwaysAllowExecute is true', () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'command', - text: 'npm install', - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'askResponse', - askResponse: 'yesButtonClicked' - }) - }) - - it('should not auto-approve disallowed execute commands even when alwaysAllowExecute is true', () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'command', - text: 'rm -rf /', - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).not.toHaveBeenCalled() - }) - - it('should not auto-approve commands with chaining characters when alwaysAllowExecute is true', () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'command', - text: 'npm install && rm -rf /', - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).not.toHaveBeenCalled() - }) - - it('should auto-approve browser actions when alwaysAllowBrowser is true', () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'browser_action_launch', - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'askResponse', - askResponse: 'yesButtonClicked' - }) - }) - - it('should not auto-approve when corresponding alwaysAllow flag is false', () => { - mockState.alwaysAllowReadOnly = false - mockState.clineMessages = [ - { - type: 'ask', - ask: 'tool', - text: JSON.stringify({ tool: 'readFile' }), - ts: Date.now(), - } - ] - renderChatView() - - expect(vscode.postMessage).not.toHaveBeenCalled() - }) - }) - - describe('Streaming State', () => { - it('should show cancel button while streaming and trigger cancel on click', async () => { - mockState.clineMessages = [ - { - type: 'say', - say: 'task', - ts: Date.now(), - }, - { - type: 'say', - say: 'text', - partial: true, - ts: Date.now(), - } - ] - renderChatView() - - const cancelButton = screen.getByText('Cancel') - await userEvent.click(cancelButton) - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'cancelTask' - }) - }) - - it('should show terminate button when task is paused and trigger terminate on click', async () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'resume_task', - ts: Date.now(), - } - ] - renderChatView() - - const terminateButton = screen.getByText('Terminate') - await userEvent.click(terminateButton) - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'clearTask' - }) - }) - - it('should show retry button when API error occurs and trigger retry on click', async () => { - mockState.clineMessages = [ - { - type: 'ask', - ask: 'api_req_failed', - ts: Date.now(), - } - ] - renderChatView() - - const retryButton = screen.getByText('Retry') - await userEvent.click(retryButton) - - expect(vscode.postMessage).toHaveBeenCalledWith({ - type: 'askResponse', - askResponse: 'yesButtonClicked' - }) - }) + default: mockReact.forwardRef(function MockChatTextArea(props: ChatTextAreaProps, ref: React.ForwardedRef) { + return ( +
+ props.onSend(e.target.value)} /> +
+ ) }) + } +}) + +jest.mock('../TaskHeader', () => ({ + __esModule: true, + default: function MockTaskHeader({ task }: { task: ClineMessage }) { + return
{JSON.stringify(task)}
+ } +})) + +// Mock VSCode components +jest.mock('@vscode/webview-ui-toolkit/react', () => ({ + VSCodeButton: function MockVSCodeButton({ + children, + onClick, + appearance + }: { + children: React.ReactNode; + onClick?: () => void; + appearance?: string; + }) { + return + }, + VSCodeTextField: function MockVSCodeTextField({ + value, + onInput, + placeholder + }: { + value?: string; + onInput?: (e: { target: { value: string } }) => void; + placeholder?: string; + }) { + return ( + onInput?.({ target: { value: e.target.value } })} + placeholder={placeholder} + /> + ) + }, + VSCodeLink: function MockVSCodeLink({ + children, + href + }: { + children: React.ReactNode; + href?: string; + }) { + return {children} + } +})) + +// Mock window.postMessage to trigger state hydration +const mockPostMessage = (state: Partial) => { + window.postMessage({ + type: 'state', + state: { + version: '1.0.0', + clineMessages: [], + taskHistory: [], + shouldShowAnnouncement: false, + allowedCommands: [], + alwaysAllowExecute: false, + ...state + } + }, '*') +} + +describe('ChatView - Auto Approval Tests', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('auto-approves browser actions when alwaysAllowBrowser is enabled', async () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowBrowser: true, + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the browser action ask message + mockPostMessage({ + alwaysAllowBrowser: true, + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'browser_action_launch', + ts: Date.now(), + text: JSON.stringify({ action: 'launch', url: 'http://example.com' }), + partial: false + } + ] + }) + + // Wait for the auto-approval message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + }) + + it('auto-approves read-only tools when alwaysAllowReadOnly is enabled', async () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowReadOnly: true, + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the read-only tool ask message + mockPostMessage({ + alwaysAllowReadOnly: true, + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'tool', + ts: Date.now(), + text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }), + partial: false + } + ] + }) + + // Wait for the auto-approval message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + }) + + it('auto-approves write tools when alwaysAllowWrite is enabled', async () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowWrite: true, + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the write tool ask message + mockPostMessage({ + alwaysAllowWrite: true, + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'tool', + ts: Date.now(), + text: JSON.stringify({ tool: 'editedExistingFile', path: 'test.txt' }), + partial: false + } + ] + }) + + // Wait for the auto-approval message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + }) + + it('auto-approves allowed commands when alwaysAllowExecute is enabled', async () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the command ask message + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'command', + ts: Date.now(), + text: 'npm test', + partial: false + } + ] + }) + + // Wait for the auto-approval message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + }) + + it('does not auto-approve disallowed commands even when alwaysAllowExecute is enabled', () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the disallowed command ask message + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'command', + ts: Date.now(), + text: 'rm -rf /', + partial: false + } + ] + }) + + // Verify no auto-approval message was sent + expect(vscode.postMessage).not.toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + + describe('Command Chaining Tests', () => { + it('auto-approves chained commands when all parts are allowed', async () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // Test various allowed command chaining scenarios + const allowedChainedCommands = [ + 'npm test && npm run build', + 'npm test; npm run build', + 'npm test || npm run build', + 'npm test | npm run build' + ] + + for (const command of allowedChainedCommands) { + jest.clearAllMocks() + + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test', 'npm run build'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the chained command ask message + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test', 'npm run build'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'command', + ts: Date.now(), + text: command, + partial: false + } + ] + }) + + // Wait for the auto-approval message + await waitFor(() => { + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + } + }) + + it('does not auto-approve chained commands when any part is disallowed', () => { + render( + + {}} + showHistoryView={() => {}} + /> + + ) + + // Test various command chaining scenarios with disallowed parts + const disallowedChainedCommands = [ + 'npm test && rm -rf /', + 'npm test; rm -rf /', + 'npm test || rm -rf /', + 'npm test | rm -rf /', + 'npm test $(echo dangerous)', + 'npm test `echo dangerous`' + ] + + disallowedChainedCommands.forEach(command => { + // First hydrate state with initial task + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + } + ] + }) + + // Then send the chained command ask message + mockPostMessage({ + alwaysAllowExecute: true, + allowedCommands: ['npm test'], + clineMessages: [ + { + type: 'say', + say: 'task', + ts: Date.now() - 2000, + text: 'Initial task' + }, + { + type: 'ask', + ask: 'command', + ts: Date.now(), + text: command, + partial: false + } + ] + }) + + // Verify no auto-approval message was sent for chained commands with disallowed parts + expect(vscode.postMessage).not.toHaveBeenCalledWith({ + type: 'askResponse', + askResponse: 'yesButtonClicked' + }) + }) + }) + }) }) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 525807cb7b..5af3c9b3b3 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,4 +1,4 @@ -import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { memo, useEffect, useState } from "react" import { useExtensionState } from "../../context/ExtensionStateContext" import { validateApiConfiguration, validateModelId } from "../../utils/validate" @@ -26,9 +26,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { alwaysAllowBrowser, setAlwaysAllowBrowser, openRouterModels, + setAllowedCommands, + allowedCommands, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) + const [commandInput, setCommandInput] = useState("") + const handleSubmit = () => { const apiValidationResult = validateApiConfiguration(apiConfiguration) const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels) @@ -42,6 +46,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser }) + vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] }) onDone() } } @@ -51,22 +56,31 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { setModelIdErrorMessage(undefined) }, [apiConfiguration]) - // validate as soon as the component is mounted - /* - useEffect will use stale values of variables if they are not included in the dependency array. so trying to use useEffect with a dependency array of only one value for example will use any other variables' old values. In most cases you don't want this, and should opt to use react-use hooks. - + // Initial validation on mount useEffect(() => { - // uses someVar and anotherVar - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [someVar]) - - If we only want to run code once on mount we can use react-use's useEffectOnce or useMount - */ + const apiValidationResult = validateApiConfiguration(apiConfiguration) + const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels) + setApiErrorMessage(apiValidationResult) + setModelIdErrorMessage(modelIdValidationResult) + }, [apiConfiguration, openRouterModels]) const handleResetState = () => { vscode.postMessage({ type: "resetState" }) } + const handleAddCommand = () => { + const currentCommands = allowedCommands ?? [] + if (commandInput && !currentCommands.includes(commandInput)) { + const newCommands = [...currentCommands, commandInput] + setAllowedCommands(newCommands) + setCommandInput("") + vscode.postMessage({ + type: "allowedCommands", + commands: newCommands + }) + } + } + return (
{ style={{ fontSize: "12px", marginTop: "5px", - color: "var(--vscode-descriptionForeground)", + padding: "8px", + border: "1px solid var(--vscode-errorBorder)", + borderRadius: "4px", + color: "var(--vscode-errorForeground)", }}> - When enabled, Cline will automatically write to files and create directories - without requiring you to click the Approve button. -

-
- -
- setAlwaysAllowExecute(e.target.checked)}> - Always approve execute operations - -

- When enabled, Cline will automatically CLI commands without requiring - you to click the Approve button. + ⚠️ WARNING: When enabled, Cline will automatically create and edit files without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.

@@ -183,13 +182,90 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { style={{ fontSize: "12px", marginTop: "5px", - color: "var(--vscode-descriptionForeground)", + padding: "8px", + backgroundColor: "var(--vscode-errorBackground)", + border: "1px solid var(--vscode-errorBorder)", + borderRadius: "4px", + color: "var(--vscode-errorForeground)", }}> - When enabled, Cline will automatically perform browser actions without requiring - you to click the Approve button. + ⚠️ WARNING: When enabled, Cline will automatically perform browser actions without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks.

NOTE: The checkbox only applies when the model supports computer use. +

+
+ setAlwaysAllowExecute(e.target.checked)}> + Always approve allowed execute operations + +

+ ⚠️ WARNING: When enabled, Cline will automatically execute allowed terminal commands without requiring approval. This is potentially very dangerous and could lead to unwanted system modifications or security risks. Enable only if you fully trust the AI and understand the risks. +

+
+ + {alwaysAllowExecute && ( +
+
+ Allowed Auto-Execute Commands +

+ Command prefixes that can be auto-executed when "Always approve execute operations" is enabled. +

+ +
+ setCommandInput(e.target.value)} + placeholder="Enter command prefix (e.g., 'git ')" + style={{ flexGrow: 1 }} + /> + + Add + +
+ +
+ {(allowedCommands ?? []).map((cmd, index) => ( +
+ {cmd} + { + const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index) + setAllowedCommands(newCommands) + vscode.postMessage({ + type: "allowedCommands", + commands: newCommands + }) + }} + > + + +
+ ))} +
+
+
+ )} + {IS_DEV && ( <>
Debug
@@ -218,8 +294,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { }}>

If you have any questions or feedback, feel free to open an issue at{" "} - - https://github.com/cline/cline + + https://github.com/RooVetGit/Roo-Cline

v{version}

diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index d4b5c7faea..776c712622 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -1,230 +1,221 @@ -import { render, screen, act } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { ExtensionStateContextType } from '../../../context/ExtensionStateContext' +import React from 'react' +import { render, screen, fireEvent } from '@testing-library/react' import SettingsView from '../SettingsView' +import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext' import { vscode } from '../../../utils/vscode' -import * as ExtensionStateContext from '../../../context/ExtensionStateContext' -import { ModelInfo } from '../../../../../src/shared/api' -// Mock dependencies +// Mock vscode API jest.mock('../../../utils/vscode', () => ({ - vscode: { - postMessage: jest.fn() - } + 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: () =>
-})) - -// Mock VS Code components +// Mock VSCode components jest.mock('@vscode/webview-ui-toolkit/react', () => ({ - VSCodeButton: ({ children, onClick }: any) => ( - - ), - VSCodeCheckbox: ({ children, checked, onChange }: any) => ( - - ), - VSCodeTextArea: ({ children, value, onInput }: any) => ( - - ), - VSCodeLink: ({ children, href }: any) => ( - {children} - ) + VSCodeButton: ({ children, onClick, appearance }: any) => ( + appearance === 'icon' ? + : + + ), + VSCodeCheckbox: ({ children, onChange, checked }: any) => ( + + ), + VSCodeTextField: ({ value, onInput, placeholder }: any) => ( + onInput({ target: { value: e.target.value } })} + placeholder={placeholder} + /> + ), + VSCodeTextArea: () =>