mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: add setting to disable LLM command suggestions
- Added new configuration setting 'roo.disableLLMCommandSuggestions' - Made command suggestions conditional based on the setting - Updated system prompt to pass the setting value - Added comprehensive tests for the new functionality - Added localization support for the new setting Fixes #5491
This commit is contained in:
parent
c9964e245e
commit
8bbecace55
7 changed files with 384 additions and 4 deletions
72
src/core/prompts/tools/__tests__/execute-command.spec.ts
Normal file
72
src/core/prompts/tools/__tests__/execute-command.spec.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { describe, it, expect } from "vitest"
|
||||
import { getExecuteCommandDescription } from "../execute-command"
|
||||
import { ToolArgs } from "../types"
|
||||
|
||||
describe("getExecuteCommandDescription", () => {
|
||||
const baseArgs: ToolArgs = {
|
||||
cwd: "/test/path",
|
||||
supportsComputerUse: false,
|
||||
}
|
||||
|
||||
it("should include suggestions section when disableLlmCommandSuggestions is false", () => {
|
||||
const args: ToolArgs = {
|
||||
...baseArgs,
|
||||
settings: {
|
||||
disableLlmCommandSuggestions: false,
|
||||
},
|
||||
}
|
||||
|
||||
const description = getExecuteCommandDescription(args)
|
||||
|
||||
// Check that the description includes the suggestions parameter
|
||||
expect(description).toContain("<suggestions>")
|
||||
expect(description).toContain("- suggestions: (optional) Command patterns for the user to allow/deny")
|
||||
expect(description).toContain("Suggestion Guidelines")
|
||||
})
|
||||
|
||||
it("should include suggestions section when disableLlmCommandSuggestions is not set", () => {
|
||||
const args: ToolArgs = {
|
||||
...baseArgs,
|
||||
settings: {},
|
||||
}
|
||||
|
||||
const description = getExecuteCommandDescription(args)
|
||||
|
||||
// Check that the description includes the suggestions parameter
|
||||
expect(description).toContain("<suggestions>")
|
||||
expect(description).toContain("- suggestions: (optional) Command patterns for the user to allow/deny")
|
||||
expect(description).toContain("Suggestion Guidelines")
|
||||
})
|
||||
|
||||
it("should exclude suggestions section when disableLlmCommandSuggestions is true", () => {
|
||||
const args: ToolArgs = {
|
||||
...baseArgs,
|
||||
settings: {
|
||||
disableLlmCommandSuggestions: true,
|
||||
},
|
||||
}
|
||||
|
||||
const description = getExecuteCommandDescription(args)
|
||||
|
||||
// Check that the description does NOT include the suggestions parameter
|
||||
expect(description).not.toContain("<suggestions>")
|
||||
expect(description).not.toContain("- suggestions: (optional) Command patterns for the user to allow/deny")
|
||||
expect(description).not.toContain("Suggestion Guidelines")
|
||||
})
|
||||
|
||||
it("should include basic command and cwd parameters regardless of settings", () => {
|
||||
const args: ToolArgs = {
|
||||
...baseArgs,
|
||||
settings: {
|
||||
disableLlmCommandSuggestions: true,
|
||||
},
|
||||
}
|
||||
|
||||
const description = getExecuteCommandDescription(args)
|
||||
|
||||
// Check that basic parameters are always included
|
||||
expect(description).toContain("- command: (required)")
|
||||
expect(description).toContain("- cwd: (optional)")
|
||||
expect(description).toContain("execute_command")
|
||||
})
|
||||
})
|
||||
|
|
@ -1,12 +1,42 @@
|
|||
import { ToolArgs } from "./types"
|
||||
|
||||
export function getExecuteCommandDescription(args: ToolArgs): string | undefined {
|
||||
return `## execute_command
|
||||
const disableLlmSuggestions = args.settings?.disableLlmCommandSuggestions ?? false
|
||||
|
||||
const baseDescription = `## execute_command
|
||||
Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
|
||||
|
||||
Parameters:
|
||||
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
|
||||
- cwd: (optional) The working directory to execute the command in (default: ${args.cwd})
|
||||
- cwd: (optional) The working directory to execute the command in (default: ${args.cwd})`
|
||||
|
||||
if (disableLlmSuggestions) {
|
||||
return (
|
||||
baseDescription +
|
||||
`
|
||||
|
||||
Usage:
|
||||
<execute_command>
|
||||
<command>Your command here</command>
|
||||
<cwd>Working directory path (optional)</cwd>
|
||||
</execute_command>
|
||||
|
||||
Example: Requesting to execute npm run dev
|
||||
<execute_command>
|
||||
<command>npm run dev</command>
|
||||
</execute_command>
|
||||
|
||||
Example: Requesting to execute ls in a specific directory
|
||||
<execute_command>
|
||||
<command>ls -la</command>
|
||||
<cwd>/home/user/projects</cwd>
|
||||
</execute_command>`
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
baseDescription +
|
||||
`
|
||||
- suggestions: (optional) Command patterns for the user to allow/deny for future auto-approval. Include 1-2 relevant patterns when executing common development commands. Use <suggest> tags.
|
||||
|
||||
**Suggestion Guidelines:**
|
||||
|
|
@ -41,4 +71,5 @@ Example: Requesting to execute ls in a specific directory
|
|||
<suggest>ls</suggest>
|
||||
</suggestions>
|
||||
</execute_command>`
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1648,6 +1648,9 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
maxReadFileLine !== -1,
|
||||
{
|
||||
maxConcurrentFileReads,
|
||||
disableLlmCommandSuggestions: vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.get<boolean>("disableLlmCommandSuggestions", false),
|
||||
},
|
||||
)
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -580,4 +580,267 @@ docker run -d nginx
|
|||
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
|
||||
})
|
||||
})
|
||||
|
||||
describe("disableLlmCommandSuggestions setting", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the workspace configuration mock
|
||||
vitest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should ignore suggestions when disableLlmCommandSuggestions is true", async () => {
|
||||
// Setup - mock the workspace configuration to return true for disableLlmCommandSuggestions
|
||||
const mockConfig = {
|
||||
get: vitest.fn().mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "disableLlmCommandSuggestions") {
|
||||
return true
|
||||
}
|
||||
return defaultValue
|
||||
}),
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
// Override the mock implementation to check for the setting
|
||||
;(executeCommandTool as any).mockImplementation(
|
||||
async (cline: any, block: any, askApproval: any, handleError: any, pushToolResult: any) => {
|
||||
// Check if disableLlmCommandSuggestions is enabled
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const disableSuggestions = config.get<boolean>("disableLlmCommandSuggestions", false)
|
||||
|
||||
let commandToApprove = block.params.command
|
||||
// Only process suggestions if the setting is disabled
|
||||
if (!disableSuggestions && block.params.suggestions) {
|
||||
commandToApprove = `${block.params.command}\n<suggestions>\n${block.params.suggestions}\n</suggestions>`
|
||||
}
|
||||
|
||||
const didApprove = await askApproval("command", commandToApprove)
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult("Command executed")
|
||||
},
|
||||
)
|
||||
|
||||
// Setup tool use with suggestions
|
||||
mockToolUse.params.command = "npm install"
|
||||
mockToolUse.params.suggestions = JSON.stringify([
|
||||
"npm install --save",
|
||||
"npm install --save-dev",
|
||||
"npm install --global",
|
||||
])
|
||||
|
||||
// Execute
|
||||
await executeCommandTool(
|
||||
mockCline as unknown as Task,
|
||||
mockToolUse,
|
||||
mockAskApproval as unknown as AskApproval,
|
||||
mockHandleError as unknown as HandleError,
|
||||
mockPushToolResult as unknown as PushToolResult,
|
||||
mockRemoveClosingTag as unknown as RemoveClosingTag,
|
||||
)
|
||||
|
||||
// Verify - should pass command WITHOUT suggestions
|
||||
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline")
|
||||
expect(mockConfig.get).toHaveBeenCalledWith("disableLlmCommandSuggestions", false)
|
||||
expect(mockAskApproval).toHaveBeenCalledWith("command", "npm install")
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
|
||||
})
|
||||
|
||||
it("should process suggestions when disableLlmCommandSuggestions is false", async () => {
|
||||
// Setup - mock the workspace configuration to return false for disableLlmCommandSuggestions
|
||||
const mockConfig = {
|
||||
get: vitest.fn().mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "disableLlmCommandSuggestions") {
|
||||
return false
|
||||
}
|
||||
return defaultValue
|
||||
}),
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
// Override the mock implementation to check for the setting
|
||||
;(executeCommandTool as any).mockImplementation(
|
||||
async (cline: any, block: any, askApproval: any, handleError: any, pushToolResult: any) => {
|
||||
// Check if disableLlmCommandSuggestions is enabled
|
||||
const config = vscode.workspace.getConfiguration("roo-cline")
|
||||
const disableSuggestions = config.get<boolean>("disableLlmCommandSuggestions", false)
|
||||
|
||||
let commandToApprove = block.params.command
|
||||
// Only process suggestions if the setting is disabled
|
||||
if (!disableSuggestions && block.params.suggestions) {
|
||||
// Parse suggestions if they're a JSON string
|
||||
let suggestions = block.params.suggestions
|
||||
if (typeof suggestions === "string" && suggestions.trim().startsWith("[")) {
|
||||
try {
|
||||
suggestions = JSON.parse(suggestions)
|
||||
} catch (e) {
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
if (Array.isArray(suggestions)) {
|
||||
commandToApprove = `${block.params.command}\n<suggestions>\n${suggestions.join("\n")}\n</suggestions>`
|
||||
}
|
||||
}
|
||||
|
||||
const didApprove = await askApproval("command", commandToApprove)
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
pushToolResult("Command executed")
|
||||
},
|
||||
)
|
||||
|
||||
// Setup tool use with suggestions
|
||||
mockToolUse.params.command = "npm install"
|
||||
mockToolUse.params.suggestions = JSON.stringify([
|
||||
"npm install --save",
|
||||
"npm install --save-dev",
|
||||
"npm install --global",
|
||||
])
|
||||
|
||||
// Execute
|
||||
await executeCommandTool(
|
||||
mockCline as unknown as Task,
|
||||
mockToolUse,
|
||||
mockAskApproval as unknown as AskApproval,
|
||||
mockHandleError as unknown as HandleError,
|
||||
mockPushToolResult as unknown as PushToolResult,
|
||||
mockRemoveClosingTag as unknown as RemoveClosingTag,
|
||||
)
|
||||
|
||||
// Verify - should pass command WITH suggestions
|
||||
const expectedCommandWithSuggestions = `npm install
|
||||
<suggestions>
|
||||
npm install --save
|
||||
npm install --save-dev
|
||||
npm install --global
|
||||
</suggestions>`
|
||||
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline")
|
||||
expect(mockConfig.get).toHaveBeenCalledWith("disableLlmCommandSuggestions", false)
|
||||
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
|
||||
})
|
||||
|
||||
it("should process suggestions when disableLlmCommandSuggestions is not set (default behavior)", async () => {
|
||||
// Setup - mock the workspace configuration to return undefined for disableLlmCommandSuggestions
|
||||
const mockConfig = {
|
||||
get: vitest.fn().mockImplementation((key: string, defaultValue?: any) => {
|
||||
// Return the default value (undefined becomes false)
|
||||
return defaultValue
|
||||
}),
|
||||
}
|
||||
;(vscode.workspace.getConfiguration as any).mockReturnValue(mockConfig)
|
||||
|
||||
// Restore the original mock implementation from beforeEach
|
||||
// Use the original mock implementation from the top-level beforeEach
|
||||
;(executeCommandTool as any).mockImplementation(
|
||||
async (cline: any, block: any, askApproval: any, handleError: any, pushToolResult: any) => {
|
||||
if (!block.params.command) {
|
||||
cline.consecutiveMistakeCount++
|
||||
cline.recordToolError("execute_command")
|
||||
const errorMessage = await cline.sayAndCreateMissingParamError("execute_command", "command")
|
||||
pushToolResult(errorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
const ignoredFileAttemptedToAccess = cline.rooIgnoreController?.validateCommand(
|
||||
block.params.command,
|
||||
)
|
||||
if (ignoredFileAttemptedToAccess) {
|
||||
await cline.say("rooignore_error", ignoredFileAttemptedToAccess)
|
||||
const mockRooIgnoreError = "RooIgnore error"
|
||||
;(formatResponse.rooIgnoreError as any).mockReturnValue(mockRooIgnoreError)
|
||||
;(formatResponse.toolError as any).mockReturnValue("Tool error")
|
||||
formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess)
|
||||
formatResponse.toolError(mockRooIgnoreError)
|
||||
pushToolResult("Tool error")
|
||||
return
|
||||
}
|
||||
|
||||
// Handle suggestions if provided
|
||||
let commandWithSuggestions = block.params.command
|
||||
if (block.params.suggestions) {
|
||||
let suggestions = block.params.suggestions
|
||||
// Handle both array and string formats
|
||||
if (typeof suggestions === "string") {
|
||||
const suggestionsString = suggestions
|
||||
|
||||
// First try to parse as JSON array
|
||||
if (suggestionsString.trim().startsWith("[")) {
|
||||
try {
|
||||
suggestions = JSON.parse(suggestionsString)
|
||||
} catch (jsonError) {
|
||||
// Fall through to XML parsing
|
||||
}
|
||||
}
|
||||
|
||||
// If not JSON or JSON parsing failed, try to parse individual <suggest> tags
|
||||
if (!Array.isArray(suggestions)) {
|
||||
const individualSuggestMatches = suggestionsString.match(/<suggest>(.*?)<\/suggest>/g)
|
||||
if (individualSuggestMatches) {
|
||||
suggestions = individualSuggestMatches
|
||||
.map((match) => {
|
||||
const content = match.match(/<suggest>(.*?)<\/suggest>/)
|
||||
return content ? content[1] : ""
|
||||
})
|
||||
.filter((suggestion) => suggestion.length > 0)
|
||||
} else {
|
||||
// If no XML tags found, treat as single suggestion
|
||||
suggestions = [suggestions]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(suggestions) && suggestions.length > 0) {
|
||||
commandWithSuggestions = `${block.params.command}\n<suggestions>\n${suggestions.join("\n")}\n</suggestions>`
|
||||
}
|
||||
}
|
||||
|
||||
const didApprove = await askApproval("command", commandWithSuggestions)
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the custom working directory if provided
|
||||
const customCwd = block.params.cwd
|
||||
|
||||
const [userRejected, result] = await mockExecuteCommand(cline, block.params.command, customCwd)
|
||||
|
||||
if (userRejected) {
|
||||
cline.didRejectTool = true
|
||||
}
|
||||
|
||||
pushToolResult(result)
|
||||
},
|
||||
)
|
||||
|
||||
// Setup tool use with suggestions
|
||||
mockToolUse.params.command = "git commit"
|
||||
mockToolUse.params.suggestions = JSON.stringify([
|
||||
'git commit -m "Initial commit"',
|
||||
"git commit --amend",
|
||||
"git commit --no-verify",
|
||||
])
|
||||
|
||||
// Execute
|
||||
await executeCommandTool(
|
||||
mockCline as unknown as Task,
|
||||
mockToolUse,
|
||||
mockAskApproval as unknown as AskApproval,
|
||||
mockHandleError as unknown as HandleError,
|
||||
mockPushToolResult as unknown as PushToolResult,
|
||||
mockRemoveClosingTag as unknown as RemoveClosingTag,
|
||||
)
|
||||
|
||||
// Verify - should pass command WITH suggestions (default behavior)
|
||||
const expectedCommandWithSuggestions = `git commit
|
||||
<suggestions>
|
||||
git commit -m "Initial commit"
|
||||
git commit --amend
|
||||
git commit --no-verify
|
||||
</suggestions>`
|
||||
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
|
||||
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -55,9 +55,14 @@ export async function executeCommandTool(
|
|||
|
||||
command = unescapeHtmlEntities(command) // Unescape HTML entities.
|
||||
|
||||
// Parse suggestions if provided
|
||||
// Get the setting for disabling LLM suggestions
|
||||
const disableLlmSuggestions = vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.get<boolean>("disableLlmCommandSuggestions", false)
|
||||
|
||||
// Parse suggestions if provided and not disabled
|
||||
let suggestions: string[] | undefined
|
||||
if (block.params.suggestions) {
|
||||
if (!disableLlmSuggestions && block.params.suggestions) {
|
||||
try {
|
||||
// Handle if suggestions is already an array (from direct tool use)
|
||||
if (Array.isArray(block.params.suggestions)) {
|
||||
|
|
|
|||
|
|
@ -345,6 +345,11 @@
|
|||
"maximum": 600,
|
||||
"description": "%commands.commandExecutionTimeout.description%"
|
||||
},
|
||||
"roo-cline.disableLlmCommandSuggestions": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%settings.disableLlmCommandSuggestions.description%"
|
||||
},
|
||||
"roo-cline.vsCodeLmModelSelector": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
|
||||
"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
|
||||
"commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)",
|
||||
"settings.disableLlmCommandSuggestions.description": "Disable LLM-generated command suggestions to reduce token usage. When enabled, command patterns will be generated programmatically instead.",
|
||||
"settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API",
|
||||
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
|
||||
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue