diff --git a/package-lock.json b/package-lock.json index ce7f1e67e9..057953752c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "2.0.2", + "version": "2.0.3", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 8ba3d51f82..43e266b259 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "roo-cline", "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.", - "version": "2.0.2", + "version": "2.0.3", "icon": "assets/icons/icon_Roo.png", "galleryBanner": { "color": "#617A91", @@ -111,6 +111,15 @@ "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 5b407f0ebf..bc5f235075 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -77,6 +77,7 @@ export class Cline { alwaysAllowReadOnly: boolean alwaysAllowWrite: boolean alwaysAllowExecute: boolean + alwaysAllowBrowser: boolean apiConversationHistory: Anthropic.MessageParam[] = [] clineMessages: ClineMessage[] = [] @@ -109,6 +110,7 @@ export class Cline { alwaysAllowReadOnly?: boolean, alwaysAllowWrite?: boolean, alwaysAllowExecute?: boolean, + alwaysAllowBrowser?: boolean, task?: string, images?: string[], historyItem?: HistoryItem @@ -123,6 +125,7 @@ export class Cline { this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false this.alwaysAllowWrite = alwaysAllowWrite ?? false this.alwaysAllowExecute = alwaysAllowExecute ?? false + this.alwaysAllowBrowser = alwaysAllowBrowser ?? false if (historyItem) { this.taskId = historyItem.id @@ -869,7 +872,7 @@ export class Cline { // (have to do this for partial and complete since sending content in thinking tags to markdown renderer will automatically be removed) // Remove end substrings of (with optional line break after) and (with optional line break before) // - Needs to be separate since we dont want to remove the line break before the first tag // - Needs to happen before the xml parsing below @@ -1418,11 +1421,24 @@ export class Cline { try { if (block.partial) { if (action === "launch") { - await this.ask( - "browser_action_launch", - removeClosingTag("url", url), - block.partial - ).catch(() => {}) + 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(() => {}) + } } else { await this.say( "browser_action", @@ -1448,7 +1464,7 @@ export class Cline { break } this.consecutiveMistakeCount = 0 - const didApprove = await askApproval("browser_action_launch", url) + const didApprove = this.alwaysAllowBrowser || await askApproval("browser_action_launch", url) if (!didApprove) { break } diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index e02be1c822..fd9f03626f 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -239,12 +239,14 @@ describe('Cline', () => { 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', () => { @@ -255,12 +257,14 @@ describe('Cline', () => { 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'); }); @@ -285,6 +289,7 @@ describe('Cline', () => { false, false, false, + false, 'test task' ); }); @@ -297,6 +302,7 @@ describe('Cline', () => { false, true, // alwaysAllowWrite false, + false, 'test task' ); @@ -312,6 +318,7 @@ describe('Cline', () => { false, false, // alwaysAllowWrite false, + false, 'test task' ); @@ -350,6 +357,7 @@ describe('Cline', () => { false, // alwaysAllowReadOnly false, // alwaysAllowWrite false, // alwaysAllowExecute + false, // alwaysAllowBrowser 'test task' // task ) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3df652f0cb..5781eacd6e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -48,6 +48,7 @@ type GlobalStateKey = | "alwaysAllowReadOnly" | "alwaysAllowWrite" | "alwaysAllowExecute" + | "alwaysAllowBrowser" | "taskHistory" | "openAiBaseUrl" | "openAiModelId" @@ -189,14 +190,40 @@ export class ClineProvider implements vscode.WebviewViewProvider { } async initClineWithTask(task?: string, images?: string[]) { - await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one - const { apiConfiguration, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = await this.getState() - this.cline = new Cline(this, apiConfiguration, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, task, images) + await this.clearTask() + const { + apiConfiguration, + customInstructions, + alwaysAllowReadOnly, + alwaysAllowWrite, + alwaysAllowExecute, + alwaysAllowBrowser + } = await this.getState() + + this.cline = new Cline( + this, + apiConfiguration, + customInstructions, + alwaysAllowReadOnly, + alwaysAllowWrite, + alwaysAllowExecute, + alwaysAllowBrowser, + task, + images + ) } async initClineWithHistoryItem(historyItem: HistoryItem) { await this.clearTask() - const { apiConfiguration, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute } = await this.getState() + const { + apiConfiguration, + customInstructions, + alwaysAllowReadOnly, + alwaysAllowWrite, + alwaysAllowExecute, + alwaysAllowBrowser + } = await this.getState() + this.cline = new Cline( this, apiConfiguration, @@ -204,6 +231,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, + alwaysAllowBrowser, undefined, undefined, historyItem @@ -499,6 +527,13 @@ 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) @@ -785,7 +820,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { async deleteTaskFromState(id: string) { // Remove the task from history - const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || [] + const taskHistory = ((await this.getGlobalState("taskHistory")) as HistoryItem[]) || [] const updatedTaskHistory = taskHistory.filter((task) => task.id !== id) await this.updateGlobalState("taskHistory", updatedTaskHistory) @@ -799,8 +834,17 @@ export class ClineProvider implements vscode.WebviewViewProvider { } async getStateToPostToWebview() { - const { apiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, taskHistory } = - await this.getState() + const { + apiConfiguration, + lastShownAnnouncementId, + customInstructions, + alwaysAllowReadOnly, + alwaysAllowWrite, + alwaysAllowExecute, + alwaysAllowBrowser, + taskHistory + } = await this.getState() + return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -808,9 +852,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, alwaysAllowWrite: alwaysAllowWrite ?? false, alwaysAllowExecute: alwaysAllowExecute ?? false, + alwaysAllowBrowser: alwaysAllowBrowser ?? false, uriScheme: vscode.env.uriScheme, clineMessages: this.cline?.clineMessages || [], - taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts), + taskHistory: (taskHistory || []) + .filter((item) => item.ts && item.task) + .sort((a, b) => b.ts - a.ts), shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId, } } @@ -898,6 +945,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowWrite, alwaysAllowExecute, taskHistory, + alwaysAllowBrowser, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -929,6 +977,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("alwaysAllowWrite") as Promise, this.getGlobalState("alwaysAllowExecute") as Promise, this.getGlobalState("taskHistory") as Promise, + this.getGlobalState("alwaysAllowBrowser") as Promise, ]) let apiProvider: ApiProvider @@ -977,6 +1026,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, alwaysAllowWrite: alwaysAllowWrite ?? false, alwaysAllowExecute: alwaysAllowExecute ?? false, + alwaysAllowBrowser: alwaysAllowBrowser ?? false, taskHistory, } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 6cd9414cf3..beae01a35d 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -35,6 +35,7 @@ export interface ExtensionState { alwaysAllowReadOnly?: boolean alwaysAllowWrite?: boolean alwaysAllowExecute?: boolean + alwaysAllowBrowser?: boolean uriScheme?: string clineMessages: ClineMessage[] taskHistory: HistoryItem[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index b89ffec560..ba23355b56 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -25,6 +25,7 @@ export interface WebviewMessage { | "openMention" | "cancelTask" | "refreshOpenRouterModels" + | "alwaysAllowBrowser" text?: string askResponse?: ClineAskResponse apiConfiguration?: ApiConfiguration diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 893a7bf0f9..525807cb7b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -23,6 +23,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { setAlwaysAllowWrite, alwaysAllowExecute, setAlwaysAllowExecute, + alwaysAllowBrowser, + setAlwaysAllowBrowser, openRouterModels, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) @@ -39,6 +41,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) + vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser }) onDone() } } @@ -170,6 +173,22 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {

+
+ setAlwaysAllowBrowser(e.target.checked)}> + Always approve browser actions + +

+ When enabled, Cline will automatically perform browser actions without requiring + you to click the Approve button. +

+
{IS_DEV && ( <> diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 9314cf1861..519cf07935 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -22,6 +22,7 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysAllowReadOnly: (value: boolean) => void setAlwaysAllowWrite: (value: boolean) => void setAlwaysAllowExecute: (value: boolean) => void + setAlwaysAllowBrowser: (value: boolean) => void setShowAnnouncement: (value: boolean) => void } @@ -118,6 +119,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })), setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })), setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })), + setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), }