mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Expose a list of allowed auto-execute commands (#31)
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## Roo Cline 2.1.4
|
## Roo Cline 2.1.8
|
||||||
|
|
||||||
- Roo Cline now publishes to the VS Code Marketplace!
|
- Roo Cline now publishes to the VS Code Marketplace!
|
||||||
- Roo Cline now allows browser actions without approval when `alwaysAllowBrowser` is true
|
- 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 can run side-by-side with Cline
|
||||||
|
- Roo Cline now allows configuration of allowed commands without approval
|
||||||
|
|
||||||
## [2.1.6]
|
## [2.1.6]
|
||||||
|
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -3,11 +3,7 @@
|
|||||||
"displayName": "Roo Cline",
|
"displayName": "Roo Cline",
|
||||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
||||||
"publisher": "RooVeterinaryInc",
|
"publisher": "RooVeterinaryInc",
|
||||||
"version": "2.1.7",
|
"version": "2.1.8",
|
||||||
"files": [
|
|
||||||
"bin/roo-cline-2.1.7.vsix",
|
|
||||||
"assets/icons/rocket.png"
|
|
||||||
],
|
|
||||||
"icon": "assets/icons/rocket.png",
|
"icon": "assets/icons/rocket.png",
|
||||||
"galleryBanner": {
|
"galleryBanner": {
|
||||||
"color": "#617A91",
|
"color": "#617A91",
|
||||||
@@ -116,6 +112,26 @@
|
|||||||
"when": "view == roo-cline.SidebarProvider"
|
"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": {
|
"scripts": {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ type GlobalStateKey =
|
|||||||
| "azureApiVersion"
|
| "azureApiVersion"
|
||||||
| "openRouterModelId"
|
| "openRouterModelId"
|
||||||
| "openRouterModelInfo"
|
| "openRouterModelInfo"
|
||||||
|
| "allowedCommands"
|
||||||
|
|
||||||
export const GlobalFileNames = {
|
export const GlobalFileNames = {
|
||||||
apiConversationHistory: "api_conversation_history.json",
|
apiConversationHistory: "api_conversation_history.json",
|
||||||
@@ -510,6 +511,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break
|
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
|
// 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)
|
||||||
}
|
}
|
||||||
@@ -820,6 +828,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
taskHistory,
|
taskHistory,
|
||||||
} = await this.getState()
|
} = await this.getState()
|
||||||
|
|
||||||
|
const allowedCommands = vscode.workspace
|
||||||
|
.getConfiguration('roo-cline')
|
||||||
|
.get<string[]>('allowedCommands') || []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: this.context.extension?.packageJSON?.version ?? "",
|
version: this.context.extension?.packageJSON?.version ?? "",
|
||||||
apiConfiguration,
|
apiConfiguration,
|
||||||
@@ -834,6 +846,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
.filter((item) => item.ts && item.task)
|
.filter((item) => item.ts && item.task)
|
||||||
.sort((a, b) => b.ts - a.ts),
|
.sort((a, b) => b.ts - a.ts),
|
||||||
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
|
||||||
|
allowedCommands,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,6 +934,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
alwaysAllowExecute,
|
alwaysAllowExecute,
|
||||||
alwaysAllowBrowser,
|
alwaysAllowBrowser,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
|
allowedCommands,
|
||||||
] = 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>,
|
||||||
@@ -953,6 +967,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
|
this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
|
this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
|
||||||
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
|
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
|
||||||
|
this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
|
||||||
])
|
])
|
||||||
|
|
||||||
let apiProvider: ApiProvider
|
let apiProvider: ApiProvider
|
||||||
@@ -1003,6 +1018,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||||||
alwaysAllowExecute: alwaysAllowExecute ?? false,
|
alwaysAllowExecute: alwaysAllowExecute ?? false,
|
||||||
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
|
alwaysAllowBrowser: alwaysAllowBrowser ?? false,
|
||||||
taskHistory,
|
taskHistory,
|
||||||
|
allowedCommands,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
outputChannel.appendLine("Cline extension activated")
|
outputChannel.appendLine("Cline extension activated")
|
||||||
|
|
||||||
|
// Get default commands from configuration
|
||||||
|
const defaultCommands = vscode.workspace
|
||||||
|
.getConfiguration('roo-cline')
|
||||||
|
.get<string[]>('allowedCommands') || [];
|
||||||
|
|
||||||
|
// Initialize global state if not already set
|
||||||
|
if (!context.globalState.get('allowedCommands')) {
|
||||||
|
context.globalState.update('allowedCommands', defaultCommands);
|
||||||
|
}
|
||||||
|
|
||||||
const sidebarProvider = new ClineProvider(context, outputChannel)
|
const sidebarProvider = new ClineProvider(context, outputChannel)
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ export interface ExtensionMessage {
|
|||||||
|
|
||||||
export interface ExtensionState {
|
export interface ExtensionState {
|
||||||
version: string
|
version: string
|
||||||
|
clineMessages: ClineMessage[]
|
||||||
|
taskHistory: HistoryItem[]
|
||||||
|
shouldShowAnnouncement: boolean
|
||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
customInstructions?: string
|
customInstructions?: string
|
||||||
alwaysAllowReadOnly?: boolean
|
alwaysAllowReadOnly?: boolean
|
||||||
@@ -37,9 +40,7 @@ export interface ExtensionState {
|
|||||||
alwaysAllowExecute?: boolean
|
alwaysAllowExecute?: boolean
|
||||||
alwaysAllowBrowser?: boolean
|
alwaysAllowBrowser?: boolean
|
||||||
uriScheme?: string
|
uriScheme?: string
|
||||||
clineMessages: ClineMessage[]
|
allowedCommands?: string[]
|
||||||
taskHistory: HistoryItem[]
|
|
||||||
shouldShowAnnouncement: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClineMessage {
|
export interface ClineMessage {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface WebviewMessage {
|
|||||||
type:
|
type:
|
||||||
| "apiConfiguration"
|
| "apiConfiguration"
|
||||||
| "customInstructions"
|
| "customInstructions"
|
||||||
|
| "allowedCommands"
|
||||||
| "alwaysAllowReadOnly"
|
| "alwaysAllowReadOnly"
|
||||||
| "alwaysAllowWrite"
|
| "alwaysAllowWrite"
|
||||||
| "alwaysAllowExecute"
|
| "alwaysAllowExecute"
|
||||||
@@ -31,6 +32,7 @@ export interface WebviewMessage {
|
|||||||
apiConfiguration?: ApiConfiguration
|
apiConfiguration?: ApiConfiguration
|
||||||
images?: string[]
|
images?: string[]
|
||||||
bool?: boolean
|
bool?: boolean
|
||||||
|
commands?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
export type ClineAskResponse = "yesButtonClicked" | "noButtonClicked" | "messageResponse"
|
||||||
|
|||||||
243
webview-ui/package-lock.json
generated
243
webview-ui/package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-transform-private-property-in-object": "^7.25.9",
|
||||||
"@types/vscode-webview": "^1.57.5"
|
"@types/vscode-webview": "^1.57.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -68,12 +69,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.24.7",
|
"version": "7.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||||
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
|
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/highlight": "^7.24.7",
|
"@babel/helper-validator-identifier": "^7.25.9",
|
||||||
|
"js-tokens": "^4.0.0",
|
||||||
"picocolors": "^1.0.0"
|
"picocolors": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -165,27 +166,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.24.7",
|
"version": "7.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz",
|
||||||
"integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==",
|
"integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.24.7",
|
"@babel/parser": "^7.26.2",
|
||||||
|
"@babel/types": "^7.26.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.25",
|
"@jridgewell/trace-mapping": "^0.3.25",
|
||||||
"jsesc": "^2.5.1"
|
"jsesc": "^3.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-annotate-as-pure": {
|
"node_modules/@babel/helper-annotate-as-pure": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
|
||||||
"integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==",
|
"integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -230,19 +230,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz",
|
||||||
"integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==",
|
"integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.24.7",
|
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||||
"@babel/helper-environment-visitor": "^7.24.7",
|
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||||
"@babel/helper-function-name": "^7.24.7",
|
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||||
"@babel/helper-member-expression-to-functions": "^7.24.7",
|
"@babel/helper-replace-supers": "^7.25.9",
|
||||||
"@babel/helper-optimise-call-expression": "^7.24.7",
|
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
||||||
"@babel/helper-replace-supers": "^7.24.7",
|
"@babel/traverse": "^7.25.9",
|
||||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
|
|
||||||
"@babel/helper-split-export-declaration": "^7.24.7",
|
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -341,13 +338,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-member-expression-to-functions": {
|
"node_modules/@babel/helper-member-expression-to-functions": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
|
||||||
"integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==",
|
"integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.24.7",
|
"@babel/traverse": "^7.25.9",
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -386,22 +382,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-optimise-call-expression": {
|
"node_modules/@babel/helper-optimise-call-expression": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz",
|
||||||
"integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==",
|
"integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-plugin-utils": {
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz",
|
||||||
"integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==",
|
"integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -424,14 +418,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-replace-supers": {
|
"node_modules/@babel/helper-replace-supers": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz",
|
||||||
"integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==",
|
"integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-environment-visitor": "^7.24.7",
|
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
||||||
"@babel/helper-member-expression-to-functions": "^7.24.7",
|
"@babel/helper-optimise-call-expression": "^7.25.9",
|
||||||
"@babel/helper-optimise-call-expression": "^7.24.7"
|
"@babel/traverse": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -454,13 +447,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
|
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz",
|
||||||
"integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==",
|
"integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.24.7",
|
"@babel/traverse": "^7.25.9",
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -479,19 +471,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||||
"integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
|
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
|
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -533,26 +523,13 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.24.7",
|
"version": "7.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
|
||||||
"integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
|
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
|
||||||
"license": "MIT",
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.26.0"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
},
|
},
|
||||||
@@ -727,18 +704,6 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@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": {
|
"node_modules/@babel/plugin-syntax-async-generators": {
|
||||||
"version": "7.8.4",
|
"version": "7.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
|
"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": {
|
"node_modules/@babel/plugin-transform-private-property-in-object": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz",
|
||||||
"integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==",
|
"integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-annotate-as-pure": "^7.24.7",
|
"@babel/helper-annotate-as-pure": "^7.25.9",
|
||||||
"@babel/helper-create-class-features-plugin": "^7.24.7",
|
"@babel/helper-create-class-features-plugin": "^7.25.9",
|
||||||
"@babel/helper-plugin-utils": "^7.24.7",
|
"@babel/helper-plugin-utils": "^7.25.9"
|
||||||
"@babel/plugin-syntax-private-property-in-object": "^7.14.5"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -2055,6 +2018,17 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@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": {
|
"node_modules/@babel/preset-env/node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -2136,33 +2110,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||||
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
|
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.25.9",
|
||||||
"@babel/parser": "^7.24.7",
|
"@babel/parser": "^7.25.9",
|
||||||
"@babel/types": "^7.24.7"
|
"@babel/types": "^7.25.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
|
||||||
"integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==",
|
"integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.24.7",
|
"@babel/code-frame": "^7.25.9",
|
||||||
"@babel/generator": "^7.24.7",
|
"@babel/generator": "^7.25.9",
|
||||||
"@babel/helper-environment-visitor": "^7.24.7",
|
"@babel/parser": "^7.25.9",
|
||||||
"@babel/helper-function-name": "^7.24.7",
|
"@babel/template": "^7.25.9",
|
||||||
"@babel/helper-hoist-variables": "^7.24.7",
|
"@babel/types": "^7.25.9",
|
||||||
"@babel/helper-split-export-declaration": "^7.24.7",
|
|
||||||
"@babel/parser": "^7.24.7",
|
|
||||||
"@babel/types": "^7.24.7",
|
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"globals": "^11.1.0"
|
"globals": "^11.1.0"
|
||||||
},
|
},
|
||||||
@@ -2171,14 +2140,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.24.7",
|
"version": "7.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
|
||||||
"integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
|
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.24.7",
|
"@babel/helper-string-parser": "^7.25.9",
|
||||||
"@babel/helper-validator-identifier": "^7.24.7",
|
"@babel/helper-validator-identifier": "^7.25.9"
|
||||||
"to-fast-properties": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -13354,15 +13321,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "2.5.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
|
||||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jsesc": "bin/jsesc"
|
"jsesc": "bin/jsesc"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
@@ -19238,15 +19204,6 @@
|
|||||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,15 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-transform-private-property-in-object": "^7.25.9",
|
||||||
"@types/vscode-webview": "^1.57.5"
|
"@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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,18 +34,8 @@ interface ChatViewProps {
|
|||||||
|
|
||||||
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
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 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 = 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)
|
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) {
|
if (lastMessage?.type === "ask" && lastMessage.text) {
|
||||||
const command = lastMessage.text
|
const command = lastMessage.text
|
||||||
|
|
||||||
// Check for command chaining characters
|
// Split command by chaining operators
|
||||||
if (command.includes('&&') ||
|
const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
|
||||||
command.includes(';') ||
|
|
||||||
command.includes('||') ||
|
// Check if all individual commands are allowed
|
||||||
command.includes('|') ||
|
return commands.every((cmd) => {
|
||||||
command.includes('$(') ||
|
const trimmedCommand = cmd.toLowerCase()
|
||||||
command.includes('`')) {
|
return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
|
||||||
return false
|
})
|
||||||
}
|
|
||||||
const trimmedCommand = command.trim().toLowerCase()
|
|
||||||
return ALLOWED_AUTO_EXECUTE_COMMANDS.some(prefix =>
|
|
||||||
trimmedCommand.startsWith(prefix.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -737,7 +722,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
|||||||
) {
|
) {
|
||||||
handlePrimaryButtonClick()
|
handlePrimaryButtonClick()
|
||||||
}
|
}
|
||||||
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages])
|
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, messages, allowedCommands])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,309 +1,549 @@
|
|||||||
import { render, screen, act } from '@testing-library/react'
|
import React from 'react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import { render, waitFor } from '@testing-library/react'
|
||||||
import { ExtensionStateContextType } from '../../../context/ExtensionStateContext'
|
|
||||||
import ChatView from '../ChatView'
|
import ChatView from '../ChatView'
|
||||||
|
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
|
||||||
import { vscode } from '../../../utils/vscode'
|
import { vscode } from '../../../utils/vscode'
|
||||||
import * as ExtensionStateContext from '../../../context/ExtensionStateContext'
|
|
||||||
|
|
||||||
// Mock vscode
|
// Define minimal types needed for testing
|
||||||
|
interface ClineMessage {
|
||||||
|
type: 'say' | 'ask';
|
||||||
|
say?: string;
|
||||||
|
ask?: string;
|
||||||
|
ts: number;
|
||||||
|
text?: string;
|
||||||
|
partial?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtensionState {
|
||||||
|
version: string;
|
||||||
|
clineMessages: ClineMessage[];
|
||||||
|
taskHistory: any[];
|
||||||
|
shouldShowAnnouncement: boolean;
|
||||||
|
allowedCommands: string[];
|
||||||
|
alwaysAllowExecute: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock vscode API
|
||||||
jest.mock('../../../utils/vscode', () => ({
|
jest.mock('../../../utils/vscode', () => ({
|
||||||
vscode: {
|
vscode: {
|
||||||
postMessage: jest.fn()
|
postMessage: jest.fn(),
|
||||||
}
|
},
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock all components that use problematic dependencies
|
|
||||||
jest.mock('../../common/CodeBlock', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: () => <div data-testid="mock-code-block" />
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('../../common/MarkdownBlock', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: () => <div data-testid="mock-markdown-block" />
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Mock components that use ESM dependencies
|
||||||
jest.mock('../BrowserSessionRow', () => ({
|
jest.mock('../BrowserSessionRow', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: () => <div data-testid="mock-browser-session-row" />
|
default: function MockBrowserSessionRow({ messages }: { messages: ClineMessage[] }) {
|
||||||
|
return <div data-testid="browser-session">{JSON.stringify(messages)}</div>
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Update ChatRow mock to capture props
|
|
||||||
let chatRowProps = null
|
|
||||||
jest.mock('../ChatRow', () => ({
|
jest.mock('../ChatRow', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: (props: any) => {
|
default: function MockChatRow({ message }: { message: ClineMessage }) {
|
||||||
chatRowProps = props
|
return <div data-testid="chat-row">{JSON.stringify(message)}</div>
|
||||||
return <div data-testid="mock-chat-row" />
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock Virtuoso component
|
interface ChatTextAreaProps {
|
||||||
jest.mock('react-virtuoso', () => ({
|
onSend: (value: string) => void;
|
||||||
Virtuoso: ({ children }: any) => (
|
inputValue?: string;
|
||||||
<div data-testid="mock-virtuoso">{children}</div>
|
textAreaDisabled?: boolean;
|
||||||
|
placeholderText?: string;
|
||||||
|
selectedImages?: string[];
|
||||||
|
shouldDisableImages?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock('../ChatTextArea', () => {
|
||||||
|
const mockReact = require('react')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: mockReact.forwardRef(function MockChatTextArea(props: ChatTextAreaProps, ref: React.ForwardedRef<HTMLInputElement>) {
|
||||||
|
return (
|
||||||
|
<div data-testid="chat-textarea">
|
||||||
|
<input ref={ref} type="text" onChange={(e) => props.onSend(e.target.value)} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.mock('../TaskHeader', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: function MockTaskHeader({ task }: { task: ClineMessage }) {
|
||||||
|
return <div data-testid="task-header">{JSON.stringify(task)}</div>
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock VS Code components
|
// Mock VSCode components
|
||||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||||
VSCodeButton: ({ children, onClick }: any) => (
|
VSCodeButton: function MockVSCodeButton({
|
||||||
<button onClick={onClick}>{children}</button>
|
children,
|
||||||
),
|
onClick,
|
||||||
VSCodeProgressRing: () => <div data-testid="progress-ring" />
|
appearance
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
appearance?: string;
|
||||||
|
}) {
|
||||||
|
return <button onClick={onClick} data-appearance={appearance}>{children}</button>
|
||||||
|
},
|
||||||
|
VSCodeTextField: function MockVSCodeTextField({
|
||||||
|
value,
|
||||||
|
onInput,
|
||||||
|
placeholder
|
||||||
|
}: {
|
||||||
|
value?: string;
|
||||||
|
onInput?: (e: { target: { value: string } }) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onInput?.({ target: { value: e.target.value } })}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
VSCodeLink: function MockVSCodeLink({
|
||||||
|
children,
|
||||||
|
href
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
href?: string;
|
||||||
|
}) {
|
||||||
|
return <a href={href}>{children}</a>
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('ChatView', () => {
|
// Mock window.postMessage to trigger state hydration
|
||||||
const mockShowHistoryView = jest.fn()
|
const mockPostMessage = (state: Partial<ExtensionState>) => {
|
||||||
const mockHideAnnouncement = jest.fn()
|
window.postMessage({
|
||||||
|
type: 'state',
|
||||||
let mockState: ExtensionStateContextType
|
state: {
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
|
|
||||||
mockState = {
|
|
||||||
clineMessages: [],
|
|
||||||
apiConfiguration: {
|
|
||||||
apiProvider: 'anthropic',
|
|
||||||
apiModelId: 'claude-3-sonnet'
|
|
||||||
},
|
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
customInstructions: '',
|
clineMessages: [],
|
||||||
alwaysAllowReadOnly: true,
|
|
||||||
alwaysAllowWrite: true,
|
|
||||||
alwaysAllowExecute: true,
|
|
||||||
alwaysAllowBrowser: true,
|
|
||||||
openRouterModels: {},
|
|
||||||
didHydrateState: true,
|
|
||||||
showWelcome: false,
|
|
||||||
theme: 'dark',
|
|
||||||
filePaths: [],
|
|
||||||
taskHistory: [],
|
taskHistory: [],
|
||||||
shouldShowAnnouncement: false,
|
shouldShowAnnouncement: false,
|
||||||
uriScheme: 'vscode',
|
allowedCommands: [],
|
||||||
|
alwaysAllowExecute: false,
|
||||||
setApiConfiguration: jest.fn(),
|
...state
|
||||||
setShowAnnouncement: jest.fn(),
|
|
||||||
setCustomInstructions: jest.fn(),
|
|
||||||
setAlwaysAllowReadOnly: jest.fn(),
|
|
||||||
setAlwaysAllowWrite: jest.fn(),
|
|
||||||
setAlwaysAllowExecute: jest.fn(),
|
|
||||||
setAlwaysAllowBrowser: jest.fn()
|
|
||||||
}
|
}
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
|
||||||
// Mock the useExtensionState hook
|
describe('ChatView - Auto Approval Tests', () => {
|
||||||
jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState)
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderChatView = () => {
|
it('auto-approves browser actions when alwaysAllowBrowser is enabled', async () => {
|
||||||
return render(
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
<ChatView
|
<ChatView
|
||||||
isHidden={false}
|
isHidden={false}
|
||||||
showAnnouncement={false}
|
showAnnouncement={false}
|
||||||
hideAnnouncement={mockHideAnnouncement}
|
hideAnnouncement={() => {}}
|
||||||
showHistoryView={mockShowHistoryView}
|
showHistoryView={() => {}}
|
||||||
/>
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
describe('Always Allow Logic', () => {
|
// First hydrate state with initial task
|
||||||
it('should auto-approve read-only tool actions when alwaysAllowReadOnly is true', () => {
|
mockPostMessage({
|
||||||
mockState.clineMessages = [
|
alwaysAllowBrowser: true,
|
||||||
|
clineMessages: [
|
||||||
{
|
{
|
||||||
type: 'ask',
|
type: 'say',
|
||||||
ask: 'tool',
|
say: 'task',
|
||||||
text: JSON.stringify({ tool: 'readFile' }),
|
ts: Date.now() - 2000,
|
||||||
ts: Date.now(),
|
text: 'Initial task'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
renderChatView()
|
|
||||||
|
|
||||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
|
||||||
type: 'askResponse',
|
|
||||||
askResponse: 'yesButtonClicked'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should auto-approve all file listing tool types when alwaysAllowReadOnly is true', () => {
|
// Then send the browser action ask message
|
||||||
const fileListingTools = [
|
mockPostMessage({
|
||||||
'readFile', 'listFiles', 'listFilesTopLevel',
|
alwaysAllowBrowser: true,
|
||||||
'listFilesRecursive', 'listCodeDefinitionNames', 'searchFiles'
|
clineMessages: [
|
||||||
]
|
|
||||||
|
|
||||||
fileListingTools.forEach(tool => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
mockState.clineMessages = [
|
|
||||||
{
|
{
|
||||||
type: 'ask',
|
type: 'say',
|
||||||
ask: 'tool',
|
say: 'task',
|
||||||
text: JSON.stringify({ tool }),
|
ts: Date.now() - 2000,
|
||||||
ts: Date.now(),
|
text: 'Initial task'
|
||||||
}
|
},
|
||||||
]
|
|
||||||
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',
|
type: 'ask',
|
||||||
ask: 'browser_action_launch',
|
ask: 'browser_action_launch',
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
|
||||||
|
partial: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
renderChatView()
|
})
|
||||||
|
|
||||||
|
// Wait for the auto-approval message
|
||||||
|
await waitFor(() => {
|
||||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
type: 'askResponse',
|
type: 'askResponse',
|
||||||
askResponse: 'yesButtonClicked'
|
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('auto-approves read-only tools when alwaysAllowReadOnly is enabled', async () => {
|
||||||
it('should show cancel button while streaming and trigger cancel on click', async () => {
|
render(
|
||||||
mockState.clineMessages = [
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// First hydrate state with initial task
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
clineMessages: [
|
||||||
{
|
{
|
||||||
type: 'say',
|
type: 'say',
|
||||||
say: 'task',
|
say: 'task',
|
||||||
ts: Date.now(),
|
ts: Date.now() - 2000,
|
||||||
},
|
text: 'Initial task'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then send the read-only tool ask message
|
||||||
|
mockPostMessage({
|
||||||
|
alwaysAllowReadOnly: true,
|
||||||
|
clineMessages: [
|
||||||
{
|
{
|
||||||
type: 'say',
|
type: 'say',
|
||||||
say: 'text',
|
say: 'task',
|
||||||
partial: true,
|
ts: Date.now() - 2000,
|
||||||
ts: Date.now(),
|
text: 'Initial task'
|
||||||
}
|
},
|
||||||
]
|
|
||||||
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',
|
type: 'ask',
|
||||||
ask: 'resume_task',
|
ask: 'tool',
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
text: JSON.stringify({ tool: 'readFile', path: 'test.txt' }),
|
||||||
|
partial: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
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 () => {
|
// Wait for the auto-approval message
|
||||||
mockState.clineMessages = [
|
await waitFor(() => {
|
||||||
{
|
|
||||||
type: 'ask',
|
|
||||||
ask: 'api_req_failed',
|
|
||||||
ts: Date.now(),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
renderChatView()
|
|
||||||
|
|
||||||
const retryButton = screen.getByText('Retry')
|
|
||||||
await userEvent.click(retryButton)
|
|
||||||
|
|
||||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
type: 'askResponse',
|
type: 'askResponse',
|
||||||
askResponse: 'yesButtonClicked'
|
askResponse: 'yesButtonClicked'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('auto-approves write tools when alwaysAllowWrite is enabled', async () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<ChatView
|
||||||
|
isHidden={false}
|
||||||
|
showAnnouncement={false}
|
||||||
|
hideAnnouncement={() => {}}
|
||||||
|
showHistoryView={() => {}}
|
||||||
|
/>
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 { memo, useEffect, useState } from "react"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||||
@@ -26,9 +26,13 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
alwaysAllowBrowser,
|
alwaysAllowBrowser,
|
||||||
setAlwaysAllowBrowser,
|
setAlwaysAllowBrowser,
|
||||||
openRouterModels,
|
openRouterModels,
|
||||||
|
setAllowedCommands,
|
||||||
|
allowedCommands,
|
||||||
} = useExtensionState()
|
} = useExtensionState()
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||||
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||||
|
const [commandInput, setCommandInput] = useState("")
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||||
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
||||||
@@ -42,6 +46,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
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 })
|
vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
|
||||||
|
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
|
||||||
onDone()
|
onDone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,22 +56,31 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
setModelIdErrorMessage(undefined)
|
setModelIdErrorMessage(undefined)
|
||||||
}, [apiConfiguration])
|
}, [apiConfiguration])
|
||||||
|
|
||||||
// validate as soon as the component is mounted
|
// Initial validation on mount
|
||||||
/*
|
|
||||||
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.
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// uses someVar and anotherVar
|
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
||||||
}, [someVar])
|
setApiErrorMessage(apiValidationResult)
|
||||||
|
setModelIdErrorMessage(modelIdValidationResult)
|
||||||
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
|
}, [apiConfiguration, openRouterModels])
|
||||||
*/
|
|
||||||
|
|
||||||
const handleResetState = () => {
|
const handleResetState = () => {
|
||||||
vscode.postMessage({ type: "resetState" })
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -149,27 +163,12 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
marginTop: "5px",
|
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
|
⚠️ 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.
|
||||||
without requiring you to click the Approve button.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
|
||||||
<VSCodeCheckbox
|
|
||||||
checked={alwaysAllowExecute}
|
|
||||||
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
|
|
||||||
<span style={{ fontWeight: "500" }}>Always approve execute operations</span>
|
|
||||||
</VSCodeCheckbox>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
fontSize: "12px",
|
|
||||||
marginTop: "5px",
|
|
||||||
color: "var(--vscode-descriptionForeground)",
|
|
||||||
}}>
|
|
||||||
When enabled, Cline will automatically CLI commands without requiring
|
|
||||||
you to click the Approve button.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -183,13 +182,90 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
marginTop: "5px",
|
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
|
⚠️ 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.<br/><br/>NOTE: The checkbox only applies when the model supports computer use.
|
||||||
you to click the Approve button.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 5 }}>
|
||||||
|
<VSCodeCheckbox
|
||||||
|
checked={alwaysAllowExecute}
|
||||||
|
onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Always approve allowed execute operations</span>
|
||||||
|
</VSCodeCheckbox>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
padding: "8px",
|
||||||
|
backgroundColor: "var(--vscode-errorBackground)",
|
||||||
|
border: "1px solid var(--vscode-errorBorder)",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "var(--vscode-errorForeground)",
|
||||||
|
}}>
|
||||||
|
⚠️ 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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{alwaysAllowExecute && (
|
||||||
|
<div style={{ marginBottom: 5 }}>
|
||||||
|
<div style={{ marginBottom: "10px" }}>
|
||||||
|
<span style={{ fontWeight: "500" }}>Allowed Auto-Execute Commands</span>
|
||||||
|
<p style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: "5px",
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
Command prefixes that can be auto-executed when "Always approve execute operations" is enabled.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
|
||||||
|
<VSCodeTextField
|
||||||
|
value={commandInput}
|
||||||
|
onInput={(e: any) => setCommandInput(e.target.value)}
|
||||||
|
placeholder="Enter command prefix (e.g., 'git ')"
|
||||||
|
style={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
<VSCodeButton onClick={handleAddCommand}>
|
||||||
|
Add
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '10px' }}>
|
||||||
|
{(allowedCommands ?? []).map((cmd, index) => (
|
||||||
|
<div key={index} style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px',
|
||||||
|
marginBottom: '5px'
|
||||||
|
}}>
|
||||||
|
<span>{cmd}</span>
|
||||||
|
<VSCodeButton
|
||||||
|
appearance="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index)
|
||||||
|
setAllowedCommands(newCommands)
|
||||||
|
vscode.postMessage({
|
||||||
|
type: "allowedCommands",
|
||||||
|
commands: newCommands
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="codicon codicon-close" />
|
||||||
|
</VSCodeButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{IS_DEV && (
|
{IS_DEV && (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
|
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
|
||||||
@@ -218,8 +294,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
}}>
|
}}>
|
||||||
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
|
||||||
If you have any questions or feedback, feel free to open an issue at{" "}
|
If you have any questions or feedback, feel free to open an issue at{" "}
|
||||||
<VSCodeLink href="https://github.com/cline/cline" style={{ display: "inline" }}>
|
<VSCodeLink href="https://github.com/RooVetGit/Roo-Cline" style={{ display: "inline" }}>
|
||||||
https://github.com/cline/cline
|
https://github.com/RooVetGit/Roo-Cline
|
||||||
</VSCodeLink>
|
</VSCodeLink>
|
||||||
</p>
|
</p>
|
||||||
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
|
<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
|
||||||
|
|||||||
@@ -1,230 +1,221 @@
|
|||||||
import { render, screen, act } from '@testing-library/react'
|
import React from 'react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
import { ExtensionStateContextType } from '../../../context/ExtensionStateContext'
|
|
||||||
import SettingsView from '../SettingsView'
|
import SettingsView from '../SettingsView'
|
||||||
|
import { ExtensionStateContextProvider } from '../../../context/ExtensionStateContext'
|
||||||
import { vscode } from '../../../utils/vscode'
|
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', () => ({
|
jest.mock('../../../utils/vscode', () => ({
|
||||||
vscode: {
|
vscode: {
|
||||||
postMessage: jest.fn()
|
postMessage: jest.fn(),
|
||||||
}
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock validation functions
|
// Mock VSCode components
|
||||||
jest.mock('../../../utils/validate', () => ({
|
|
||||||
validateApiConfiguration: jest.fn(() => undefined),
|
|
||||||
validateModelId: jest.fn(() => undefined)
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock ApiOptions component
|
|
||||||
jest.mock('../ApiOptions', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: () => <div data-testid="mock-api-options" />
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock VS Code components
|
|
||||||
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
jest.mock('@vscode/webview-ui-toolkit/react', () => ({
|
||||||
VSCodeButton: ({ children, onClick }: any) => (
|
VSCodeButton: ({ children, onClick, appearance }: any) => (
|
||||||
<button onClick={onClick}>{children}</button>
|
appearance === 'icon' ?
|
||||||
|
<button onClick={onClick} className="codicon codicon-close" aria-label="Remove command">
|
||||||
|
<span className="codicon codicon-close" />
|
||||||
|
</button> :
|
||||||
|
<button onClick={onClick} data-appearance={appearance}>{children}</button>
|
||||||
),
|
),
|
||||||
VSCodeCheckbox: ({ children, checked, onChange }: any) => (
|
VSCodeCheckbox: ({ children, onChange, checked }: any) => (
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={e => onChange(e)}
|
onChange={(e) => onChange({ target: { checked: e.target.checked } })}
|
||||||
aria-checked={checked}
|
aria-label={typeof children === 'string' ? children : undefined}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</label>
|
</label>
|
||||||
),
|
),
|
||||||
VSCodeTextArea: ({ children, value, onInput }: any) => (
|
VSCodeTextField: ({ value, onInput, placeholder }: any) => (
|
||||||
<textarea
|
<input
|
||||||
data-testid="custom-instructions"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
readOnly
|
onChange={(e) => onInput({ target: { value: e.target.value } })}
|
||||||
aria-label="Custom Instructions"
|
placeholder={placeholder}
|
||||||
>{children}</textarea>
|
/>
|
||||||
),
|
),
|
||||||
VSCodeLink: ({ children, href }: any) => (
|
VSCodeTextArea: () => <textarea />,
|
||||||
<a href={href}>{children}</a>
|
VSCodeLink: () => <a />,
|
||||||
|
VSCodeDropdown: ({ children, value, onChange }: any) => (
|
||||||
|
<select value={value} onChange={onChange}>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
),
|
||||||
|
VSCodeOption: ({ children, value }: any) => (
|
||||||
|
<option value={value}>{children}</option>
|
||||||
|
),
|
||||||
|
VSCodeRadio: ({ children, value, checked, onChange }: any) => (
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
value={value}
|
||||||
|
checked={checked}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
VSCodeRadioGroup: ({ children, value, onChange }: any) => (
|
||||||
|
<div onChange={onChange}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('SettingsView', () => {
|
// Mock window.postMessage to trigger state hydration
|
||||||
const mockOnDone = jest.fn()
|
const mockPostMessage = (state: any) => {
|
||||||
const mockSetAlwaysAllowWrite = jest.fn()
|
window.postMessage({
|
||||||
const mockSetAlwaysAllowReadOnly = jest.fn()
|
type: 'state',
|
||||||
const mockSetCustomInstructions = jest.fn()
|
state: {
|
||||||
const mockSetAlwaysAllowExecute = jest.fn()
|
|
||||||
|
|
||||||
let mockState: ExtensionStateContextType
|
|
||||||
|
|
||||||
const mockOpenRouterModels: Record<string, ModelInfo> = {
|
|
||||||
'claude-3-sonnet': {
|
|
||||||
maxTokens: 200000,
|
|
||||||
contextWindow: 200000,
|
|
||||||
supportsImages: true,
|
|
||||||
supportsComputerUse: true,
|
|
||||||
supportsPromptCache: true,
|
|
||||||
inputPrice: 0.000008,
|
|
||||||
outputPrice: 0.000024,
|
|
||||||
description: "Anthropic's Claude 3 Sonnet model"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
|
|
||||||
mockState = {
|
|
||||||
apiConfiguration: {
|
|
||||||
apiProvider: 'anthropic',
|
|
||||||
apiModelId: 'claude-3-sonnet'
|
|
||||||
},
|
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
customInstructions: 'Test instructions',
|
clineMessages: [],
|
||||||
alwaysAllowReadOnly: true,
|
|
||||||
alwaysAllowWrite: true,
|
|
||||||
alwaysAllowExecute: true,
|
|
||||||
openRouterModels: mockOpenRouterModels,
|
|
||||||
didHydrateState: true,
|
|
||||||
showWelcome: false,
|
|
||||||
theme: 'dark',
|
|
||||||
filePaths: [],
|
|
||||||
taskHistory: [],
|
taskHistory: [],
|
||||||
shouldShowAnnouncement: false,
|
shouldShowAnnouncement: false,
|
||||||
clineMessages: [],
|
allowedCommands: [],
|
||||||
uriScheme: 'vscode',
|
alwaysAllowExecute: false,
|
||||||
|
...state
|
||||||
setAlwaysAllowReadOnly: mockSetAlwaysAllowReadOnly,
|
|
||||||
setAlwaysAllowWrite: mockSetAlwaysAllowWrite,
|
|
||||||
setCustomInstructions: mockSetCustomInstructions,
|
|
||||||
setAlwaysAllowExecute: mockSetAlwaysAllowExecute,
|
|
||||||
setApiConfiguration: jest.fn(),
|
|
||||||
setShowAnnouncement: jest.fn()
|
|
||||||
}
|
}
|
||||||
|
}, '*')
|
||||||
|
}
|
||||||
|
|
||||||
// Mock the useExtensionState hook
|
const renderSettingsView = () => {
|
||||||
jest.spyOn(ExtensionStateContext, 'useExtensionState').mockReturnValue(mockState)
|
const onDone = jest.fn()
|
||||||
})
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
const renderSettingsView = () => {
|
<SettingsView onDone={onDone} />
|
||||||
return render(
|
</ExtensionStateContextProvider>
|
||||||
<SettingsView onDone={mockOnDone} />
|
|
||||||
)
|
)
|
||||||
}
|
// Hydrate initial state
|
||||||
|
mockPostMessage({})
|
||||||
|
return { onDone }
|
||||||
|
}
|
||||||
|
|
||||||
describe('Checkboxes', () => {
|
describe('SettingsView - Allowed Commands', () => {
|
||||||
it('should toggle alwaysAllowWrite checkbox', async () => {
|
beforeEach(() => {
|
||||||
mockState.alwaysAllowWrite = false
|
jest.clearAllMocks()
|
||||||
renderSettingsView()
|
})
|
||||||
|
|
||||||
const writeCheckbox = screen.getByRole('checkbox', {
|
it('shows allowed commands section when alwaysAllowExecute is enabled', () => {
|
||||||
name: /Always approve write operations/i
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(writeCheckbox).not.toBeChecked()
|
|
||||||
await act(async () => {
|
|
||||||
await userEvent.click(writeCheckbox)
|
|
||||||
})
|
|
||||||
expect(mockSetAlwaysAllowWrite).toHaveBeenCalledWith(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should toggle alwaysAllowExecute checkbox', async () => {
|
|
||||||
mockState.alwaysAllowExecute = false
|
|
||||||
renderSettingsView()
|
renderSettingsView()
|
||||||
|
|
||||||
|
// Enable always allow execute
|
||||||
const executeCheckbox = screen.getByRole('checkbox', {
|
const executeCheckbox = screen.getByRole('checkbox', {
|
||||||
name: /Always approve execute operations/i
|
name: /Always approve allowed execute operations/i
|
||||||
|
})
|
||||||
|
fireEvent.click(executeCheckbox)
|
||||||
|
|
||||||
|
// Verify allowed commands section appears
|
||||||
|
expect(screen.getByText(/Allowed Auto-Execute Commands/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText(/Enter command prefix/i)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(executeCheckbox).not.toBeChecked()
|
it('adds new command to the list', () => {
|
||||||
await act(async () => {
|
|
||||||
await userEvent.click(executeCheckbox)
|
|
||||||
})
|
|
||||||
expect(mockSetAlwaysAllowExecute).toHaveBeenCalledWith(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should toggle alwaysAllowReadOnly checkbox', async () => {
|
|
||||||
mockState.alwaysAllowReadOnly = false
|
|
||||||
renderSettingsView()
|
renderSettingsView()
|
||||||
|
|
||||||
const readOnlyCheckbox = screen.getByRole('checkbox', {
|
// Enable always allow execute
|
||||||
name: /Always approve read-only operations/i
|
const executeCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Always approve allowed execute operations/i
|
||||||
})
|
})
|
||||||
|
fireEvent.click(executeCheckbox)
|
||||||
|
|
||||||
expect(readOnlyCheckbox).not.toBeChecked()
|
// Add a new command
|
||||||
await act(async () => {
|
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||||
await userEvent.click(readOnlyCheckbox)
|
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||||
})
|
|
||||||
expect(mockSetAlwaysAllowReadOnly).toHaveBeenCalledWith(true)
|
const addButton = screen.getByText('Add')
|
||||||
|
fireEvent.click(addButton)
|
||||||
|
|
||||||
|
// Verify command was added
|
||||||
|
expect(screen.getByText('npm test')).toBeInTheDocument()
|
||||||
|
|
||||||
|
// Verify VSCode message was sent
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||||
|
type: 'allowedCommands',
|
||||||
|
commands: ['npm test']
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Form Submission', () => {
|
it('removes command from the list', () => {
|
||||||
it('should send correct messages when form is submitted', async () => {
|
|
||||||
renderSettingsView()
|
renderSettingsView()
|
||||||
|
|
||||||
// Submit form
|
// Enable always allow execute
|
||||||
const doneButton = screen.getByRole('button', { name: /Done/i })
|
const executeCheckbox = screen.getByRole('checkbox', {
|
||||||
await act(async () => {
|
name: /Always approve allowed execute operations/i
|
||||||
await userEvent.click(doneButton)
|
|
||||||
})
|
})
|
||||||
|
fireEvent.click(executeCheckbox)
|
||||||
|
|
||||||
// Verify messages were sent in the correct order
|
// Add a command
|
||||||
const calls = (vscode.postMessage as jest.Mock).mock.calls
|
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||||
expect(calls).toHaveLength(5)
|
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||||
|
const addButton = screen.getByText('Add')
|
||||||
|
fireEvent.click(addButton)
|
||||||
|
|
||||||
expect(calls[0][0]).toEqual({
|
// Remove the command
|
||||||
type: 'apiConfiguration',
|
const removeButton = screen.getByRole('button', { name: 'Remove command' })
|
||||||
apiConfiguration: {
|
fireEvent.click(removeButton)
|
||||||
apiProvider: 'anthropic',
|
|
||||||
apiModelId: 'claude-3-sonnet'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls[1][0]).toEqual({
|
// Verify command was removed
|
||||||
type: 'customInstructions',
|
expect(screen.queryByText('npm test')).not.toBeInTheDocument()
|
||||||
text: 'Test instructions'
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls[2][0]).toEqual({
|
// Verify VSCode message was sent
|
||||||
type: 'alwaysAllowReadOnly',
|
expect(vscode.postMessage).toHaveBeenLastCalledWith({
|
||||||
bool: true
|
type: 'allowedCommands',
|
||||||
})
|
commands: []
|
||||||
|
|
||||||
expect(calls[3][0]).toEqual({
|
|
||||||
type: 'alwaysAllowWrite',
|
|
||||||
bool: true
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(calls[4][0]).toEqual({
|
|
||||||
type: 'alwaysAllowExecute',
|
|
||||||
bool: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Verify onDone was called
|
|
||||||
expect(mockOnDone).toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Accessibility', () => {
|
it('prevents duplicate commands', () => {
|
||||||
it('should have accessible form controls', () => {
|
|
||||||
renderSettingsView()
|
renderSettingsView()
|
||||||
|
|
||||||
// Check for proper labels and ARIA attributes
|
// Enable always allow execute
|
||||||
const writeCheckbox = screen.getByRole('checkbox', {
|
const executeCheckbox = screen.getByRole('checkbox', {
|
||||||
name: /Always approve write operations/i
|
name: /Always approve allowed execute operations/i
|
||||||
})
|
})
|
||||||
expect(writeCheckbox).toHaveAttribute('aria-checked')
|
fireEvent.click(executeCheckbox)
|
||||||
|
|
||||||
const textarea = screen.getByRole('textbox', {
|
// Add a command twice
|
||||||
name: /Custom Instructions/i
|
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||||
|
const addButton = screen.getByText('Add')
|
||||||
|
|
||||||
|
// First addition
|
||||||
|
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||||
|
fireEvent.click(addButton)
|
||||||
|
|
||||||
|
// Second addition attempt
|
||||||
|
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||||
|
fireEvent.click(addButton)
|
||||||
|
|
||||||
|
// Verify command appears only once
|
||||||
|
const commands = screen.getAllByText('npm test')
|
||||||
|
expect(commands).toHaveLength(1)
|
||||||
})
|
})
|
||||||
expect(textarea).toBeInTheDocument()
|
|
||||||
|
it('saves allowed commands when clicking Done', () => {
|
||||||
|
const { onDone } = renderSettingsView()
|
||||||
|
|
||||||
|
// Enable always allow execute
|
||||||
|
const executeCheckbox = screen.getByRole('checkbox', {
|
||||||
|
name: /Always approve allowed execute operations/i
|
||||||
})
|
})
|
||||||
|
fireEvent.click(executeCheckbox)
|
||||||
|
|
||||||
|
// Add a command
|
||||||
|
const input = screen.getByPlaceholderText(/Enter command prefix/i)
|
||||||
|
fireEvent.change(input, { target: { value: 'npm test' } })
|
||||||
|
const addButton = screen.getByText('Add')
|
||||||
|
fireEvent.click(addButton)
|
||||||
|
|
||||||
|
// Click Done
|
||||||
|
const doneButton = screen.getByText('Done')
|
||||||
|
fireEvent.click(doneButton)
|
||||||
|
|
||||||
|
// Verify VSCode messages were sent
|
||||||
|
expect(vscode.postMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: 'allowedCommands',
|
||||||
|
commands: ['npm test']
|
||||||
|
}))
|
||||||
|
expect(onDone).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||||||
setAlwaysAllowExecute: (value: boolean) => void
|
setAlwaysAllowExecute: (value: boolean) => void
|
||||||
setAlwaysAllowBrowser: (value: boolean) => void
|
setAlwaysAllowBrowser: (value: boolean) => void
|
||||||
setShowAnnouncement: (value: boolean) => void
|
setShowAnnouncement: (value: boolean) => void
|
||||||
|
setAllowedCommands: (value: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||||
@@ -34,6 +35,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
clineMessages: [],
|
clineMessages: [],
|
||||||
taskHistory: [],
|
taskHistory: [],
|
||||||
shouldShowAnnouncement: false,
|
shouldShowAnnouncement: false,
|
||||||
|
allowedCommands: [],
|
||||||
})
|
})
|
||||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
@@ -121,6 +123,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
|
setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
|
||||||
setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
|
setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
|
||||||
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
|
||||||
|
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, screen, act } from '@testing-library/react'
|
||||||
|
import { ExtensionStateContextProvider, useExtensionState } from '../ExtensionStateContext'
|
||||||
|
|
||||||
|
// Test component that consumes the context
|
||||||
|
const TestComponent = () => {
|
||||||
|
const { allowedCommands, setAllowedCommands } = useExtensionState()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div data-testid="allowed-commands">{JSON.stringify(allowedCommands)}</div>
|
||||||
|
<button
|
||||||
|
data-testid="update-button"
|
||||||
|
onClick={() => setAllowedCommands(['npm install', 'git status'])}
|
||||||
|
>
|
||||||
|
Update Commands
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ExtensionStateContext', () => {
|
||||||
|
it('initializes with empty allowedCommands array', () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(screen.getByTestId('allowed-commands').textContent!)).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates allowedCommands through setAllowedCommands', () => {
|
||||||
|
render(
|
||||||
|
<ExtensionStateContextProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</ExtensionStateContextProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
screen.getByTestId('update-button').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(JSON.parse(screen.getByTestId('allowed-commands').textContent!)).toEqual([
|
||||||
|
'npm install',
|
||||||
|
'git status'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error when used outside provider', () => {
|
||||||
|
// Suppress console.error for this test since we expect an error
|
||||||
|
const consoleSpy = jest.spyOn(console, 'error')
|
||||||
|
consoleSpy.mockImplementation(() => {})
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
render(<TestComponent />)
|
||||||
|
}).toThrow('useExtensionState must be used within an ExtensionStateContextProvider')
|
||||||
|
|
||||||
|
consoleSpy.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user