Add a setAlwaysAllowBrowser checkbox to settings

This commit is contained in:
Matt Rubens
2024-11-22 13:23:10 -05:00
parent e55696e247
commit 0346fdeecb
9 changed files with 124 additions and 18 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "roo-cline", "name": "roo-cline",
"version": "2.0.2", "version": "2.0.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "roo-cline", "name": "roo-cline",
"version": "2.0.2", "version": "2.0.3",
"dependencies": { "dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/sdk": "^0.26.0",

View File

@@ -2,7 +2,7 @@
"name": "roo-cline", "name": "roo-cline",
"displayName": "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.", "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", "icon": "assets/icons/icon_Roo.png",
"galleryBanner": { "galleryBanner": {
"color": "#617A91", "color": "#617A91",
@@ -111,6 +111,15 @@
"when": "view == claude-dev.SidebarProvider" "when": "view == claude-dev.SidebarProvider"
} }
] ]
},
"configuration": {
"properties": {
"cline.alwaysAllowBrowser": {
"type": "boolean",
"default": false,
"description": "Always allow browser actions without requiring confirmation"
}
}
} }
}, },
"scripts": { "scripts": {

View File

@@ -77,6 +77,7 @@ export class Cline {
alwaysAllowReadOnly: boolean alwaysAllowReadOnly: boolean
alwaysAllowWrite: boolean alwaysAllowWrite: boolean
alwaysAllowExecute: boolean alwaysAllowExecute: boolean
alwaysAllowBrowser: boolean
apiConversationHistory: Anthropic.MessageParam[] = [] apiConversationHistory: Anthropic.MessageParam[] = []
clineMessages: ClineMessage[] = [] clineMessages: ClineMessage[] = []
@@ -109,6 +110,7 @@ export class Cline {
alwaysAllowReadOnly?: boolean, alwaysAllowReadOnly?: boolean,
alwaysAllowWrite?: boolean, alwaysAllowWrite?: boolean,
alwaysAllowExecute?: boolean, alwaysAllowExecute?: boolean,
alwaysAllowBrowser?: boolean,
task?: string, task?: string,
images?: string[], images?: string[],
historyItem?: HistoryItem historyItem?: HistoryItem
@@ -123,6 +125,7 @@ export class Cline {
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
this.alwaysAllowWrite = alwaysAllowWrite ?? false this.alwaysAllowWrite = alwaysAllowWrite ?? false
this.alwaysAllowExecute = alwaysAllowExecute ?? false this.alwaysAllowExecute = alwaysAllowExecute ?? false
this.alwaysAllowBrowser = alwaysAllowBrowser ?? false
if (historyItem) { if (historyItem) {
this.taskId = historyItem.id 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) // (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 <thinking or </thinking (below xml parsing is only for opening tags) // Remove end substrings of <thinking or </thinking (below xml parsing is only for opening tags)
// (this is done with the xml parsing below now, but keeping here for reference) // (this is done with the xml parsing below now, but keeping here for reference)
// content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?)?$/, "") // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?)?$/, "")
// Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before) // Remove all instances of <thinking> (with optional line break after) and </thinking> (with optional line break before)
// - Needs to be separate since we dont want to remove the line break before the first tag // - 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 // - Needs to happen before the xml parsing below
@@ -1418,11 +1421,24 @@ export class Cline {
try { try {
if (block.partial) { if (block.partial) {
if (action === "launch") { if (action === "launch") {
await this.ask( if (this.alwaysAllowBrowser) {
"browser_action_launch", await this.say(
removeClosingTag("url", url), "browser_action",
block.partial JSON.stringify({
).catch(() => {}) 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 { } else {
await this.say( await this.say(
"browser_action", "browser_action",
@@ -1448,7 +1464,7 @@ export class Cline {
break break
} }
this.consecutiveMistakeCount = 0 this.consecutiveMistakeCount = 0
const didApprove = await askApproval("browser_action_launch", url) const didApprove = this.alwaysAllowBrowser || await askApproval("browser_action_launch", url)
if (!didApprove) { if (!didApprove) {
break break
} }

View File

@@ -239,12 +239,14 @@ describe('Cline', () => {
undefined, // alwaysAllowReadOnly undefined, // alwaysAllowReadOnly
undefined, // alwaysAllowWrite undefined, // alwaysAllowWrite
undefined, // alwaysAllowExecute undefined, // alwaysAllowExecute
undefined, // alwaysAllowBrowser
'test task' 'test task'
); );
expect(cline.alwaysAllowReadOnly).toBe(false); expect(cline.alwaysAllowReadOnly).toBe(false);
expect(cline.alwaysAllowWrite).toBe(false); expect(cline.alwaysAllowWrite).toBe(false);
expect(cline.alwaysAllowExecute).toBe(false); expect(cline.alwaysAllowExecute).toBe(false);
expect(cline.alwaysAllowBrowser).toBe(false);
}); });
it('should respect provided settings', () => { it('should respect provided settings', () => {
@@ -255,12 +257,14 @@ describe('Cline', () => {
true, // alwaysAllowReadOnly true, // alwaysAllowReadOnly
true, // alwaysAllowWrite true, // alwaysAllowWrite
true, // alwaysAllowExecute true, // alwaysAllowExecute
true, // alwaysAllowBrowser
'test task' 'test task'
); );
expect(cline.alwaysAllowReadOnly).toBe(true); expect(cline.alwaysAllowReadOnly).toBe(true);
expect(cline.alwaysAllowWrite).toBe(true); expect(cline.alwaysAllowWrite).toBe(true);
expect(cline.alwaysAllowExecute).toBe(true); expect(cline.alwaysAllowExecute).toBe(true);
expect(cline.alwaysAllowBrowser).toBe(true);
expect(cline.customInstructions).toBe('custom instructions'); expect(cline.customInstructions).toBe('custom instructions');
}); });
@@ -285,6 +289,7 @@ describe('Cline', () => {
false, false,
false, false,
false, false,
false,
'test task' 'test task'
); );
}); });
@@ -297,6 +302,7 @@ describe('Cline', () => {
false, false,
true, // alwaysAllowWrite true, // alwaysAllowWrite
false, false,
false,
'test task' 'test task'
); );
@@ -312,6 +318,7 @@ describe('Cline', () => {
false, false,
false, // alwaysAllowWrite false, // alwaysAllowWrite
false, false,
false,
'test task' 'test task'
); );
@@ -350,6 +357,7 @@ describe('Cline', () => {
false, // alwaysAllowReadOnly false, // alwaysAllowReadOnly
false, // alwaysAllowWrite false, // alwaysAllowWrite
false, // alwaysAllowExecute false, // alwaysAllowExecute
false, // alwaysAllowBrowser
'test task' // task 'test task' // task
) )

View File

@@ -48,6 +48,7 @@ type GlobalStateKey =
| "alwaysAllowReadOnly" | "alwaysAllowReadOnly"
| "alwaysAllowWrite" | "alwaysAllowWrite"
| "alwaysAllowExecute" | "alwaysAllowExecute"
| "alwaysAllowBrowser"
| "taskHistory" | "taskHistory"
| "openAiBaseUrl" | "openAiBaseUrl"
| "openAiModelId" | "openAiModelId"
@@ -189,14 +190,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
} }
async initClineWithTask(task?: string, images?: string[]) { 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)
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask() 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.cline = new Cline(
this, this,
apiConfiguration, apiConfiguration,
@@ -204,6 +207,31 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowReadOnly, alwaysAllowReadOnly,
alwaysAllowWrite, alwaysAllowWrite,
alwaysAllowExecute, alwaysAllowExecute,
alwaysAllowBrowser,
task,
images
)
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask()
const {
apiConfiguration,
customInstructions,
alwaysAllowReadOnly,
alwaysAllowWrite,
alwaysAllowExecute,
alwaysAllowBrowser
} = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
customInstructions,
alwaysAllowReadOnly,
alwaysAllowWrite,
alwaysAllowExecute,
alwaysAllowBrowser,
undefined, undefined,
undefined, undefined,
historyItem 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 // 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 break
// Add more switch case statements here as more webview message commands // Add more switch case statements here as more webview message commands
// are created within the webview context (i.e. inside media/main.js) // 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) { async deleteTaskFromState(id: string) {
// Remove the task from history // 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) const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
await this.updateGlobalState("taskHistory", updatedTaskHistory) await this.updateGlobalState("taskHistory", updatedTaskHistory)
@@ -799,8 +834,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
} }
async getStateToPostToWebview() { async getStateToPostToWebview() {
const { apiConfiguration, lastShownAnnouncementId, customInstructions, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, taskHistory } = const {
await this.getState() apiConfiguration,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
alwaysAllowWrite,
alwaysAllowExecute,
alwaysAllowBrowser,
taskHistory
} = await this.getState()
return { return {
version: this.context.extension?.packageJSON?.version ?? "", version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration, apiConfiguration,
@@ -808,9 +852,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
alwaysAllowWrite: alwaysAllowWrite ?? false, alwaysAllowWrite: alwaysAllowWrite ?? false,
alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowExecute: alwaysAllowExecute ?? false,
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
uriScheme: vscode.env.uriScheme, uriScheme: vscode.env.uriScheme,
clineMessages: this.cline?.clineMessages || [], 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, shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
} }
} }
@@ -898,6 +945,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowWrite, alwaysAllowWrite,
alwaysAllowExecute, alwaysAllowExecute,
taskHistory, taskHistory,
alwaysAllowBrowser,
] = await Promise.all([ ] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>, this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>, this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -929,6 +977,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("alwaysAllowWrite") as Promise<boolean | undefined>, this.getGlobalState("alwaysAllowWrite") as Promise<boolean | undefined>,
this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>, this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>, this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
]) ])
let apiProvider: ApiProvider let apiProvider: ApiProvider
@@ -977,6 +1026,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
alwaysAllowWrite: alwaysAllowWrite ?? false, alwaysAllowWrite: alwaysAllowWrite ?? false,
alwaysAllowExecute: alwaysAllowExecute ?? false, alwaysAllowExecute: alwaysAllowExecute ?? false,
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
taskHistory, taskHistory,
} }
} }

View File

@@ -35,6 +35,7 @@ export interface ExtensionState {
alwaysAllowReadOnly?: boolean alwaysAllowReadOnly?: boolean
alwaysAllowWrite?: boolean alwaysAllowWrite?: boolean
alwaysAllowExecute?: boolean alwaysAllowExecute?: boolean
alwaysAllowBrowser?: boolean
uriScheme?: string uriScheme?: string
clineMessages: ClineMessage[] clineMessages: ClineMessage[]
taskHistory: HistoryItem[] taskHistory: HistoryItem[]

View File

@@ -25,6 +25,7 @@ export interface WebviewMessage {
| "openMention" | "openMention"
| "cancelTask" | "cancelTask"
| "refreshOpenRouterModels" | "refreshOpenRouterModels"
| "alwaysAllowBrowser"
text?: string text?: string
askResponse?: ClineAskResponse askResponse?: ClineAskResponse
apiConfiguration?: ApiConfiguration apiConfiguration?: ApiConfiguration

View File

@@ -23,6 +23,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setAlwaysAllowWrite, setAlwaysAllowWrite,
alwaysAllowExecute, alwaysAllowExecute,
setAlwaysAllowExecute, setAlwaysAllowExecute,
alwaysAllowBrowser,
setAlwaysAllowBrowser,
openRouterModels, openRouterModels,
} = useExtensionState() } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined) const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
@@ -39,6 +41,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly }) vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite }) vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute }) vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
onDone() onDone()
} }
} }
@@ -170,6 +173,22 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p> </p>
</div> </div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={alwaysAllowBrowser}
onChange={(e: any) => setAlwaysAllowBrowser(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve browser actions</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Cline will automatically perform browser actions without requiring
you to click the Approve button.
</p>
</div>
{IS_DEV && ( {IS_DEV && (
<> <>

View File

@@ -22,6 +22,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setAlwaysAllowReadOnly: (value: boolean) => void setAlwaysAllowReadOnly: (value: boolean) => void
setAlwaysAllowWrite: (value: boolean) => void setAlwaysAllowWrite: (value: boolean) => void
setAlwaysAllowExecute: (value: boolean) => void setAlwaysAllowExecute: (value: boolean) => void
setAlwaysAllowBrowser: (value: boolean) => void
setShowAnnouncement: (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 })), setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })), setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })), setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
} }