fix: resolve all issues in PR #5491 - command whitelisting feature

- Fix hardcoded English strings by moving to translation files
- Add missing ARIA attributes for accessibility compliance
- Extract suggestion parsing logic to shared utils (src/shared/commandParsing.ts)
- Move pattern extraction logic to shared utils (src/shared/commandPatterns.ts)
- Extract CommandPatternSelector as a separate component for better modularity
- Consolidate message types to use 'allowedCommands' consistently
- Update tests to match new implementation

All linters and tests now pass successfully.
This commit is contained in:
hannesrudolph 2025-07-08 21:46:47 -06:00
parent 9b0f3b2435
commit a65f5d72d4
49 changed files with 1976 additions and 533 deletions

View file

@ -3,23 +3,66 @@ import { ToolArgs } from "./types"
export function getExecuteCommandDescription(args: ToolArgs): string | undefined {
return `## 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.
**IMPORTANT: When executing commands that match common patterns (like npm, git, ls, etc.), you SHOULD provide suggestions for whitelisting. This allows users to auto-approve similar commands in the future.**
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})
- suggestions: (optional) An array of safe command patterns that the user can whitelist for automatic approval in the future. Each suggestion should be a pattern that can match similar commands. When the command matches common development patterns, you SHOULD include relevant suggestions. Format each suggestion using <suggest> tags.
**Whitelisting Guidelines:**
- Include suggestions when executing common development commands (npm, git, ls, cd, etc.)
- Suggestions use prefix matching: any command that starts with the suggestion will be auto-approved
- The special pattern "*" allows ALL commands (use with caution)
- Suggestions are case-insensitive (e.g., "npm " matches "NPM install", "npm test", etc.)
- Include a trailing space in suggestions to ensure proper prefix matching
- Common patterns to suggest:
- "npm " for all npm commands
- "git " for all git operations
- "ls " for listing files
- "cd " for changing directories
- "echo " for echo commands
- "mkdir " for creating directories
- "rm -rf node_modules" for specific cleanup command
- Language-specific patterns like "python ", "node ", "go test ", etc.
- "*" to allow all commands (only suggest when explicitly requested by user)
Usage:
<execute_command>
<command>Your command here</command>
<cwd>Working directory path (optional)</cwd>
<suggestions>
<suggest>pattern 1</suggest>
<suggest>pattern 2</suggest>
</suggestions>
</execute_command>
Example: Requesting to execute npm run dev
Example: Requesting to execute npm run dev with suggestions
<execute_command>
<command>npm run dev</command>
<suggestions>
<suggest>npm run </suggest>
<suggest>npm </suggest>
</suggestions>
</execute_command>
Example: Requesting to execute ls in a specific directory if directed
Example: Requesting to execute git status with suggestions
<execute_command>
<command>git status</command>
<suggestions>
<suggest>git status</suggest>
<suggest>git *</suggest>
</suggestions>
</execute_command>
Example: Requesting to execute ls in a specific directory with suggestions
<execute_command>
<command>ls -la</command>
<cwd>/home/user/projects</cwd>
<suggestions>
<suggest>ls -la</suggest>
<suggest>ls </suggest>
</suggestions>
</execute_command>`
}

View file

@ -52,7 +52,45 @@ beforeEach(() => {
return
}
const didApprove = await askApproval("command", block.params.command)
// 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
}
@ -266,4 +304,237 @@ describe("executeCommandTool", () => {
expect(mockExecuteCommand).not.toHaveBeenCalled()
})
})
describe("Suggestions functionality", () => {
it("should pass command with suggestions when suggestions are provided as array", async () => {
// Setup
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
const expectedCommandWithSuggestions = `npm install
<suggestions>
npm install --save
npm install --save-dev
npm install --global
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should pass command with suggestions when suggestions are provided as JSON string", async () => {
// Setup
mockToolUse.params.command = "git commit"
mockToolUse.params.suggestions =
'["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
const expectedCommandWithSuggestions = `git commit
<suggestions>
git commit -m "Initial commit"
git commit --amend
git commit --no-verify
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should handle single suggestion as string", async () => {
// Setup
mockToolUse.params.command = "docker run"
mockToolUse.params.suggestions = "docker run -it ubuntu:latest"
// 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
const expectedCommandWithSuggestions = `docker run
<suggestions>
docker run -it ubuntu:latest
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should handle empty suggestions array", async () => {
// Setup
mockToolUse.params.command = "ls"
mockToolUse.params.suggestions = JSON.stringify([])
// 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(mockAskApproval).toHaveBeenCalledWith("command", "ls")
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should handle invalid JSON string in suggestions", async () => {
// Setup
mockToolUse.params.command = "echo test"
mockToolUse.params.suggestions = "invalid json {"
// 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 treat invalid JSON as single suggestion
const expectedCommandWithSuggestions = `echo test
<suggestions>
invalid json {
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should parse individual <suggest> XML tags correctly", async () => {
// Setup
mockToolUse.params.command = "npm install"
mockToolUse.params.suggestions =
"<suggest>npm install --save</suggest><suggest>npm install --save-dev</suggest><suggest>npm install --global</suggest>"
// 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
const expectedCommandWithSuggestions = `npm install
<suggestions>
npm install --save
npm install --save-dev
npm install --global
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should parse single <suggest> XML tag correctly", async () => {
// Setup
mockToolUse.params.command = "git push"
mockToolUse.params.suggestions = "<suggest>git push origin main</suggest>"
// 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
const expectedCommandWithSuggestions = `git push
<suggestions>
git push origin main
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should handle mixed content with <suggest> tags", async () => {
// Setup
mockToolUse.params.command = "docker run"
mockToolUse.params.suggestions =
"Some text before <suggest>docker run -it ubuntu</suggest> and <suggest>docker run -d nginx</suggest> with text after"
// 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
const expectedCommandWithSuggestions = `docker run
<suggestions>
docker run -it ubuntu
docker run -d nginx
</suggestions>`
expect(mockAskApproval).toHaveBeenCalledWith("command", expectedCommandWithSuggestions)
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
it("should work normally when no suggestions are provided", async () => {
// Setup
mockToolUse.params.command = "pwd"
// No suggestions property
// 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(mockAskApproval).toHaveBeenCalledWith("command", "pwd")
expect(mockExecuteCommand).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Command executed")
})
})
})

View file

@ -51,7 +51,54 @@ export async function executeCommandTool(
cline.consecutiveMistakeCount = 0
command = unescapeHtmlEntities(command) // Unescape HTML entities.
const didApprove = await askApproval("command", command)
// Parse suggestions if provided
let suggestions: string[] | undefined
if (block.params.suggestions) {
try {
// Handle if suggestions is already an array (from direct tool use)
if (Array.isArray(block.params.suggestions)) {
suggestions = block.params.suggestions
} else if (typeof block.params.suggestions === "string") {
const suggestionsString = block.params.suggestions
// First try to parse as JSON array
if (suggestionsString.trim().startsWith("[")) {
try {
const parsed = JSON.parse(suggestionsString)
if (Array.isArray(parsed)) {
suggestions = parsed
}
} catch (jsonError) {
// Fall through to XML parsing
}
}
// If not JSON or JSON parsing failed, try to parse individual <suggest> tags
if (!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)
}
}
}
} catch (e) {
// If parsing fails, ignore suggestions
console.warn("Failed to parse suggestions:", e)
}
}
// Pass suggestions as part of the command text in a structured format
const commandWithSuggestions = suggestions
? `${command}\n<suggestions>${JSON.stringify(suggestions)}</suggestions>`
: command
const didApprove = await askApproval("command", commandWithSuggestions)
if (!didApprove) {
return

View file

@ -1,484 +1,243 @@
import type { Mock } from "vitest"
// Mock dependencies - must come before imports
vi.mock("../../../api/providers/fetchers/modelCache")
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import * as vscode from "vscode"
import { webviewMessageHandler } from "../webviewMessageHandler"
import type { ClineProvider } from "../ClineProvider"
import { getModels } from "../../../api/providers/fetchers/modelCache"
import type { ModelRecord } from "../../../shared/api"
const mockGetModels = getModels as Mock<typeof getModels>
// Mock ClineProvider
const mockClineProvider = {
getState: vi.fn(),
postMessageToWebview: vi.fn(),
customModesManager: {
getCustomModes: vi.fn(),
deleteCustomMode: vi.fn(),
},
context: {
extensionPath: "/mock/extension/path",
globalStorageUri: { fsPath: "/mock/global/storage" },
},
contextProxy: {
context: {
extensionPath: "/mock/extension/path",
globalStorageUri: { fsPath: "/mock/global/storage" },
},
setValue: vi.fn(),
},
log: vi.fn(),
postStateToWebview: vi.fn(),
} as unknown as ClineProvider
import { ClineProvider } from "../ClineProvider"
import { Package } from "../../../shared/package"
import { t } from "../../../i18n"
// Mock vscode module
vi.mock("vscode", () => ({
window: {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
workspace: {
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
getConfiguration: vi.fn(),
},
ConfigurationTarget: {
Global: 1,
},
}))
// Mock i18n
vi.mock("../../../i18n", () => ({
t: vi.fn((key: string, args?: Record<string, any>) => {
// For the delete confirmation with rules, we need to return the interpolated string
if (key === "common:confirmation.delete_custom_mode_with_rules" && args) {
return `Are you sure you want to delete this ${args.scope} mode?\n\nThis will also delete the associated rules folder at:\n${args.rulesFolderPath}`
}
// Return the translated value for "Yes"
if (key === "common:answers.yes") {
return "Yes"
}
// Return the translated value for "Cancel"
if (key === "common:answers.cancel") {
return "Cancel"
t: vi.fn((key: string, params?: any) => {
if (key === "common:info.command_whitelisted" && params?.pattern) {
return `Command pattern "${params.pattern}" has been whitelisted`
}
return key
}),
}))
vi.mock("fs/promises", () => {
const mockRm = vi.fn().mockResolvedValue(undefined)
const mockMkdir = vi.fn().mockResolvedValue(undefined)
// Mock Package
vi.mock("../../../shared/package", () => ({
Package: {
name: "roo-code",
},
}))
return {
default: {
rm: mockRm,
mkdir: mockMkdir,
},
rm: mockRm,
mkdir: mockMkdir,
}
})
describe("webviewMessageHandler - whitelistCommand", () => {
let mockProvider: any
let mockContextProxy: any
let mockConfigUpdate: any
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as os from "os"
import * as path from "path"
import * as fsUtils from "../../../utils/fs"
import { getWorkspacePath } from "../../../utils/path"
import { ensureSettingsDirectoryExists } from "../../../utils/globalContext"
import type { ModeConfig } from "@roo-code/types"
vi.mock("../../../utils/fs")
vi.mock("../../../utils/path")
vi.mock("../../../utils/globalContext")
describe("webviewMessageHandler - requestRouterModels", () => {
beforeEach(() => {
vi.clearAllMocks()
mockClineProvider.getState = vi.fn().mockResolvedValue({
apiConfiguration: {
openRouterApiKey: "openrouter-key",
requestyApiKey: "requesty-key",
glamaApiKey: "glama-key",
unboundApiKey: "unbound-key",
litellmApiKey: "litellm-key",
litellmBaseUrl: "http://localhost:4000",
},
})
})
it("successfully fetches models from all providers", async () => {
const mockModels: ModelRecord = {
"model-1": {
maxTokens: 4096,
contextWindow: 8192,
supportsPromptCache: false,
description: "Test model 1",
},
"model-2": {
maxTokens: 8192,
contextWindow: 16384,
supportsPromptCache: false,
description: "Test model 2",
},
// Setup mock for workspace configuration
mockConfigUpdate = vi.fn().mockResolvedValue(undefined)
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
update: mockConfigUpdate,
} as any)
// Create mock context proxy
mockContextProxy = {
getValue: vi.fn(),
setValue: vi.fn(),
}
mockGetModels.mockResolvedValue(mockModels)
await webviewMessageHandler(mockClineProvider, {
type: "requestRouterModels",
})
// Verify getModels was called for each provider
expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
expect(mockGetModels).toHaveBeenCalledWith({
provider: "litellm",
apiKey: "litellm-key",
baseUrl: "http://localhost:4000",
})
// Verify response was sent
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "routerModels",
routerModels: {
openrouter: mockModels,
requesty: mockModels,
glama: mockModels,
unbound: mockModels,
litellm: mockModels,
ollama: {},
lmstudio: {},
},
})
// Create mock provider
mockProvider = {
contextProxy: mockContextProxy,
postStateToWebview: vi.fn().mockResolvedValue(undefined),
log: vi.fn(),
} as any
})
it("handles LiteLLM models with values from message when config is missing", async () => {
mockClineProvider.getState = vi.fn().mockResolvedValue({
apiConfiguration: {
openRouterApiKey: "openrouter-key",
requestyApiKey: "requesty-key",
glamaApiKey: "glama-key",
unboundApiKey: "unbound-key",
// Missing litellm config
},
})
const mockModels: ModelRecord = {
"model-1": {
maxTokens: 4096,
contextWindow: 8192,
supportsPromptCache: false,
description: "Test model 1",
},
}
mockGetModels.mockResolvedValue(mockModels)
await webviewMessageHandler(mockClineProvider, {
type: "requestRouterModels",
values: {
litellmApiKey: "message-litellm-key",
litellmBaseUrl: "http://message-url:4000",
},
})
// Verify LiteLLM was called with values from message
expect(mockGetModels).toHaveBeenCalledWith({
provider: "litellm",
apiKey: "message-litellm-key",
baseUrl: "http://message-url:4000",
})
afterEach(() => {
vi.restoreAllMocks()
})
it("skips LiteLLM when both config and message values are missing", async () => {
mockClineProvider.getState = vi.fn().mockResolvedValue({
apiConfiguration: {
openRouterApiKey: "openrouter-key",
requestyApiKey: "requesty-key",
glamaApiKey: "glama-key",
unboundApiKey: "unbound-key",
// Missing litellm config
},
})
it("should add a new command pattern to the allowed commands list", async () => {
// Setup initial state
mockContextProxy.getValue.mockReturnValue(["npm test", "git status"])
const mockModels: ModelRecord = {
"model-1": {
maxTokens: 4096,
contextWindow: 8192,
supportsPromptCache: false,
description: "Test model 1",
},
// Create message
const message = {
type: "whitelistCommand",
pattern: "npm run build",
}
mockGetModels.mockResolvedValue(mockModels)
// Call handler
await webviewMessageHandler(mockProvider, message as any)
await webviewMessageHandler(mockClineProvider, {
type: "requestRouterModels",
// No values provided
})
// Verify the pattern was added
expect(mockContextProxy.setValue).toHaveBeenCalledWith("allowedCommands", [
"npm test",
"git status",
"npm run build",
])
// Verify LiteLLM was NOT called
expect(mockGetModels).not.toHaveBeenCalledWith(
expect.objectContaining({
provider: "litellm",
}),
// Verify workspace settings were updated
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-code")
expect(mockConfigUpdate).toHaveBeenCalledWith(
"allowedCommands",
["npm test", "git status", "npm run build"],
vscode.ConfigurationTarget.Global,
)
// Verify response includes empty object for LiteLLM
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "routerModels",
routerModels: {
openrouter: mockModels,
requesty: mockModels,
glama: mockModels,
unbound: mockModels,
litellm: {},
ollama: {},
lmstudio: {},
},
})
// Verify user was notified
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith(
'Command pattern "npm run build" has been whitelisted',
)
// Verify state was posted to webview
expect(mockProvider.postStateToWebview).toHaveBeenCalled()
})
it("handles individual provider failures gracefully", async () => {
const mockModels: ModelRecord = {
"model-1": {
maxTokens: 4096,
contextWindow: 8192,
supportsPromptCache: false,
description: "Test model 1",
},
it("should not add duplicate patterns", async () => {
// Setup initial state with existing pattern
mockContextProxy.getValue.mockReturnValue(["npm test", "git status", "npm run build"])
// Create message with duplicate pattern
const message = {
type: "whitelistCommand",
pattern: "npm run build",
}
// Mock some providers to succeed and others to fail
mockGetModels
.mockResolvedValueOnce(mockModels) // openrouter
.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
.mockResolvedValueOnce(mockModels) // glama
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
// Call handler
await webviewMessageHandler(mockProvider, message as any)
await webviewMessageHandler(mockClineProvider, {
type: "requestRouterModels",
})
// Verify setValue was NOT called (no update needed)
expect(mockContextProxy.setValue).not.toHaveBeenCalled()
// Verify successful providers are included
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "routerModels",
routerModels: {
openrouter: mockModels,
requesty: {},
glama: mockModels,
unbound: {},
litellm: {},
ollama: {},
lmstudio: {},
},
})
// Verify workspace settings were NOT updated
expect(mockConfigUpdate).not.toHaveBeenCalled()
// Verify error messages were sent for failed providers
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Requesty API error",
values: { provider: "requesty" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Unbound API error",
values: { provider: "unbound" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "LiteLLM connection failed",
values: { provider: "litellm" },
})
})
it("handles Error objects and string errors correctly", async () => {
// Mock providers to fail with different error types
mockGetModels
.mockRejectedValueOnce(new Error("Structured error message")) // openrouter
.mockRejectedValueOnce(new Error("Requesty API error")) // requesty
.mockRejectedValueOnce(new Error("Glama API error")) // glama
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
await webviewMessageHandler(mockClineProvider, {
type: "requestRouterModels",
})
// Verify error handling for different error types
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Structured error message",
values: { provider: "openrouter" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Requesty API error",
values: { provider: "requesty" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Glama API error",
values: { provider: "glama" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Unbound API error",
values: { provider: "unbound" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "LiteLLM connection failed",
values: { provider: "litellm" },
})
})
it("prefers config values over message values for LiteLLM", async () => {
const mockModels: ModelRecord = {}
mockGetModels.mockResolvedValue(mockModels)
await webviewMessageHandler(mockClineProvider, {
type: "requestRouterModels",
values: {
litellmApiKey: "message-key",
litellmBaseUrl: "http://message-url",
},
})
// Verify config values are used over message values
expect(mockGetModels).toHaveBeenCalledWith({
provider: "litellm",
apiKey: "litellm-key", // From config
baseUrl: "http://localhost:4000", // From config
})
})
})
describe("webviewMessageHandler - deleteCustomMode", () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(getWorkspacePath).mockReturnValue("/mock/workspace")
vi.mocked(vscode.window.showErrorMessage).mockResolvedValue(undefined)
vi.mocked(ensureSettingsDirectoryExists).mockResolvedValue("/mock/global/storage/.roo")
})
it("should delete a project mode and its rules folder", async () => {
const slug = "test-project-mode"
const rulesFolderPath = path.join("/mock/workspace", ".roo", `rules-${slug}`)
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
{
name: "Test Project Mode",
slug,
roleDefinition: "Test Role",
groups: [],
source: "project",
} as ModeConfig,
])
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
// The confirmation dialog is now handled in the webview, so we don't expect showInformationMessage to be called
// Verify user was NOT notified
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
expect(fs.rm).toHaveBeenCalledWith(rulesFolderPath, { recursive: true, force: true })
// Verify state was NOT posted to webview
expect(mockProvider.postStateToWebview).not.toHaveBeenCalled()
})
it("should delete a global mode and its rules folder", async () => {
const slug = "test-global-mode"
const homeDir = os.homedir()
const rulesFolderPath = path.join(homeDir, ".roo", `rules-${slug}`)
it("should handle empty allowed commands list", async () => {
// Setup with no existing commands
mockContextProxy.getValue.mockReturnValue(undefined)
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
{
name: "Test Global Mode",
slug,
roleDefinition: "Test Role",
groups: [],
source: "global",
} as ModeConfig,
])
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
// Create message
const message = {
type: "whitelistCommand",
pattern: "echo 'Hello, World!'",
}
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
// Call handler
await webviewMessageHandler(mockProvider, message as any)
// The confirmation dialog is now handled in the webview, so we don't expect showInformationMessage to be called
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
expect(fs.rm).toHaveBeenCalledWith(rulesFolderPath, { recursive: true, force: true })
})
// Verify the pattern was added as the first item
expect(mockContextProxy.setValue).toHaveBeenCalledWith("allowedCommands", ["echo 'Hello, World!'"])
it("should only delete the mode when rules folder does not exist", async () => {
const slug = "test-mode-no-rules"
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
{
name: "Test Mode No Rules",
slug,
roleDefinition: "Test Role",
groups: [],
source: "project",
} as ModeConfig,
])
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false)
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
// The confirmation dialog is now handled in the webview, so we don't expect showInformationMessage to be called
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
expect(fs.rm).not.toHaveBeenCalled()
})
it("should handle errors when deleting rules folder", async () => {
const slug = "test-mode-error"
const rulesFolderPath = path.join("/mock/workspace", ".roo", `rules-${slug}`)
const error = new Error("Permission denied")
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([
{
name: "Test Mode Error",
slug,
roleDefinition: "Test Role",
groups: [],
source: "project",
} as ModeConfig,
])
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true)
vi.mocked(mockClineProvider.customModesManager.deleteCustomMode).mockResolvedValue(undefined)
vi.mocked(fs.rm).mockRejectedValue(error)
await webviewMessageHandler(mockClineProvider, { type: "deleteCustomMode", slug })
expect(mockClineProvider.customModesManager.deleteCustomMode).toHaveBeenCalledWith(slug)
expect(fs.rm).toHaveBeenCalledWith(rulesFolderPath, { recursive: true, force: true })
// Verify error message is shown to the user
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
t("common:errors.delete_rules_folder_failed", {
rulesFolderPath,
error: error.message,
}),
// Verify workspace settings were updated
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-code")
expect(mockConfigUpdate).toHaveBeenCalledWith(
"allowedCommands",
["echo 'Hello, World!'"],
vscode.ConfigurationTarget.Global,
)
// Verify user was notified
expect(vscode.window.showInformationMessage).toHaveBeenCalledWith(
`Command pattern "echo 'Hello, World!'" has been whitelisted`,
)
})
it("should filter out invalid commands", async () => {
// Setup with some invalid commands
mockContextProxy.getValue.mockReturnValue(["npm test", "", " ", null, "git status"])
// Create message
const message = {
type: "whitelistCommand",
pattern: "npm run dev",
}
// Call handler
await webviewMessageHandler(mockProvider, message as any)
// Verify only valid commands were kept
expect(mockContextProxy.setValue).toHaveBeenCalledWith("allowedCommands", [
"npm test",
"git status",
"npm run dev",
])
})
it("should handle missing pattern gracefully", async () => {
// Create message without pattern
const message = {
type: "whitelistCommand",
}
// Call handler
await webviewMessageHandler(mockProvider, message as any)
// Verify nothing was updated
expect(mockContextProxy.setValue).not.toHaveBeenCalled()
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
expect(mockProvider.postStateToWebview).not.toHaveBeenCalled()
})
it("should handle non-string pattern gracefully", async () => {
// Create message with non-string pattern
const message = {
type: "whitelistCommand",
pattern: 123, // Invalid type
}
// Call handler
await webviewMessageHandler(mockProvider, message as any)
// Verify nothing was updated
expect(mockContextProxy.setValue).not.toHaveBeenCalled()
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled()
expect(mockProvider.postStateToWebview).not.toHaveBeenCalled()
})
it("should handle complex command patterns with special characters", async () => {
// Setup initial state
mockContextProxy.getValue.mockReturnValue(["npm test"])
// Create message with complex pattern
const message = {
type: "whitelistCommand",
pattern: 'echo "Hello, World!" && echo $HOME',
}
// Call handler
await webviewMessageHandler(mockProvider, message as any)
// Verify the pattern was added correctly
expect(mockContextProxy.setValue).toHaveBeenCalledWith("allowedCommands", [
"npm test",
'echo "Hello, World!" && echo $HOME',
])
// Verify workspace settings were updated
expect(vscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-code")
expect(mockConfigUpdate).toHaveBeenCalledWith(
"allowedCommands",
["npm test", 'echo "Hello, World!" && echo $HOME'],
vscode.ConfigurationTarget.Global,
)
// No error response is sent anymore - we just continue with deletion
expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled()
})
})

View file

@ -776,6 +776,36 @@ export const webviewMessageHandler = async (
break
}
case "whitelistCommand": {
// Add a command pattern to the allowed commands list
if (message.pattern && typeof message.pattern === "string") {
const currentCommands = getGlobalState("allowedCommands") ?? []
const validCommands = Array.isArray(currentCommands)
? currentCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0)
: []
// Add the new pattern if it's not already in the list
if (!validCommands.includes(message.pattern)) {
validCommands.push(message.pattern)
await updateGlobalState("allowedCommands", validCommands)
// Also update workspace settings
await vscode.workspace
.getConfiguration(Package.name)
.update("allowedCommands", validCommands, vscode.ConfigurationTarget.Global)
// Show confirmation to the user
vscode.window.showInformationMessage(
t("common:info.command_whitelisted", { pattern: message.pattern }),
)
// Update the webview state
await provider.postStateToWebview()
}
}
break
}
case "openCustomModesSettings": {
const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()

View file

@ -105,7 +105,8 @@
"organization_share_link_copied": "Enllaç de compartició d'organització copiat al porta-retalls!",
"public_share_link_copied": "Enllaç de compartició pública copiat al porta-retalls!",
"mode_exported": "Mode '{{mode}}' exportat correctament",
"mode_imported": "Mode importat correctament"
"mode_imported": "Mode importat correctament",
"command_whitelisted": "El patró d'ordres '{{pattern}}' s'ha afegit a la llista d'ordres permeses"
},
"answers": {
"yes": "Sí",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Organisations-Freigabelink in die Zwischenablage kopiert!",
"public_share_link_copied": "Öffentlicher Freigabelink in die Zwischenablage kopiert!",
"mode_exported": "Modus '{{mode}}' erfolgreich exportiert",
"mode_imported": "Modus erfolgreich importiert"
"mode_imported": "Modus erfolgreich importiert",
"command_whitelisted": "Befehlsmuster '{{pattern}}' wurde zur Liste der erlaubten Befehle hinzugefügt"
},
"answers": {
"yes": "Ja",

View file

@ -101,7 +101,8 @@
"image_copied_to_clipboard": "Image data URI copied to clipboard",
"image_saved": "Image saved to {{path}}",
"mode_exported": "Mode '{{mode}}' exported successfully",
"mode_imported": "Mode imported successfully"
"mode_imported": "Mode imported successfully",
"command_whitelisted": "Command pattern '{{pattern}}' has been added to the allowed commands list"
},
"answers": {
"yes": "Yes",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "¡Enlace de compartición de organización copiado al portapapeles!",
"public_share_link_copied": "¡Enlace de compartición pública copiado al portapapeles!",
"mode_exported": "Modo '{{mode}}' exportado correctamente",
"mode_imported": "Modo importado correctamente"
"mode_imported": "Modo importado correctamente",
"command_whitelisted": "El patrón de comando '{{pattern}}' se ha añadido a la lista de comandos permitidos"
},
"answers": {
"yes": "Sí",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Lien de partage d'organisation copié dans le presse-papiers !",
"public_share_link_copied": "Lien de partage public copié dans le presse-papiers !",
"mode_exported": "Mode '{{mode}}' exporté avec succès",
"mode_imported": "Mode importé avec succès"
"mode_imported": "Mode importé avec succès",
"command_whitelisted": "Le modèle de commande '{{pattern}}' a été ajouté à la liste des commandes autorisées"
},
"answers": {
"yes": "Oui",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "संगठन साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!",
"public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!",
"mode_exported": "मोड '{{mode}}' सफलतापूर्वक निर्यात किया गया",
"mode_imported": "मोड सफलतापूर्वक आयात किया गया"
"mode_imported": "मोड सफलतापूर्वक आयात किया गया",
"command_whitelisted": "कमांड पैटर्न '{{pattern}}' को अनुमत कमांड सूची में जोड़ा गया है"
},
"answers": {
"yes": "हां",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Tautan berbagi organisasi disalin ke clipboard!",
"public_share_link_copied": "Tautan berbagi publik disalin ke clipboard!",
"mode_exported": "Mode '{{mode}}' berhasil diekspor",
"mode_imported": "Mode berhasil diimpor"
"mode_imported": "Mode berhasil diimpor",
"command_whitelisted": "Pola perintah '{{pattern}}' telah ditambahkan ke daftar perintah yang diizinkan"
},
"answers": {
"yes": "Ya",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Link di condivisione organizzazione copiato negli appunti!",
"public_share_link_copied": "Link di condivisione pubblica copiato negli appunti!",
"mode_exported": "Modalità '{{mode}}' esportata con successo",
"mode_imported": "Modalità importata con successo"
"mode_imported": "Modalità importata con successo",
"command_whitelisted": "Il modello di comando '{{pattern}}' è stato aggiunto all'elenco dei comandi consentiti"
},
"answers": {
"yes": "Sì",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "組織共有リンクがクリップボードにコピーされました!",
"public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!",
"mode_exported": "モード「{{mode}}」が正常にエクスポートされました",
"mode_imported": "モードが正常にインポートされました"
"mode_imported": "モードが正常にインポートされました",
"command_whitelisted": "コマンドパターン '{{pattern}}' が許可されたコマンドリストに追加されました"
},
"answers": {
"yes": "はい",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "조직 공유 링크가 클립보드에 복사되었습니다!",
"public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!",
"mode_exported": "'{{mode}}' 모드가 성공적으로 내보내졌습니다",
"mode_imported": "모드를 성공적으로 가져왔습니다"
"mode_imported": "모드를 성공적으로 가져왔습니다",
"command_whitelisted": "명령 패턴 '{{pattern}}'이(가) 허용된 명령 목록에 추가되었습니다"
},
"answers": {
"yes": "예",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Organisatie deel-link gekopieerd naar klembord!",
"public_share_link_copied": "Openbare deel-link gekopieerd naar klembord!",
"mode_exported": "Modus '{{mode}}' succesvol geëxporteerd",
"mode_imported": "Modus succesvol geïmporteerd"
"mode_imported": "Modus succesvol geïmporteerd",
"command_whitelisted": "Commandopatroon '{{pattern}}' is toegevoegd aan de lijst met toegestane commando's"
},
"answers": {
"yes": "Ja",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Link udostępniania organizacji skopiowany do schowka!",
"public_share_link_copied": "Publiczny link udostępniania skopiowany do schowka!",
"mode_exported": "Tryb '{{mode}}' pomyślnie wyeksportowany",
"mode_imported": "Tryb pomyślnie zaimportowany"
"mode_imported": "Tryb pomyślnie zaimportowany",
"command_whitelisted": "Wzór polecenia '{{pattern}}' został dodany do listy dozwolonych poleceń"
},
"answers": {
"yes": "Tak",

View file

@ -105,7 +105,8 @@
"organization_share_link_copied": "Link de compartilhamento da organização copiado para a área de transferência!",
"public_share_link_copied": "Link de compartilhamento público copiado para a área de transferência!",
"mode_exported": "Modo '{{mode}}' exportado com sucesso",
"mode_imported": "Modo importado com sucesso"
"mode_imported": "Modo importado com sucesso",
"command_whitelisted": "O padrão de comando '{{pattern}}' foi adicionado à lista de comandos permitidos"
},
"answers": {
"yes": "Sim",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Ссылка для совместного доступа организации скопирована в буфер обмена!",
"public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!",
"mode_exported": "Режим '{{mode}}' успешно экспортирован",
"mode_imported": "Режим успешно импортирован"
"mode_imported": "Режим успешно импортирован",
"command_whitelisted": "Шаблон команды '{{pattern}}' добавлен в список разрешенных команд"
},
"answers": {
"yes": "Да",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Kuruluş paylaşım bağlantısı panoya kopyalandı!",
"public_share_link_copied": "Herkese açık paylaşım bağlantısı panoya kopyalandı!",
"mode_exported": "'{{mode}}' modu başarıyla dışa aktarıldı",
"mode_imported": "Mod başarıyla içe aktarıldı"
"mode_imported": "Mod başarıyla içe aktarıldı",
"command_whitelisted": "'{{pattern}}' komut deseni izin verilen komutlar listesine eklendi"
},
"answers": {
"yes": "Evet",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "Liên kết chia sẻ tổ chức đã được sao chép vào clipboard!",
"public_share_link_copied": "Liên kết chia sẻ công khai đã được sao chép vào clipboard!",
"mode_exported": "Chế độ '{{mode}}' đã được xuất thành công",
"mode_imported": "Chế độ đã được nhập thành công"
"mode_imported": "Chế độ đã được nhập thành công",
"command_whitelisted": "Mẫu lệnh '{{pattern}}' đã được thêm vào danh sách lệnh được phép"
},
"answers": {
"yes": "Có",

View file

@ -106,7 +106,8 @@
"organization_share_link_copied": "组织分享链接已复制到剪贴板!",
"public_share_link_copied": "公开分享链接已复制到剪贴板!",
"mode_exported": "模式 '{{mode}}' 已成功导出",
"mode_imported": "模式已成功导入"
"mode_imported": "模式已成功导入",
"command_whitelisted": "命令模式 '{{pattern}}' 已添加到允许的命令列表中"
},
"answers": {
"yes": "是",

View file

@ -101,7 +101,8 @@
"organization_share_link_copied": "組織分享連結已複製到剪貼簿!",
"public_share_link_copied": "公開分享連結已複製到剪貼簿!",
"mode_exported": "模式 '{{mode}}' 已成功匯出",
"mode_imported": "模式已成功匯入"
"mode_imported": "模式已成功匯入",
"command_whitelisted": "命令模式 '{{pattern}}' 已新增至允許的命令清單中"
},
"answers": {
"yes": "是",

View file

@ -36,6 +36,7 @@ export interface WebviewMessage {
| "getListApiConfiguration"
| "customInstructions"
| "allowedCommands"
| "whitelistCommand"
| "alwaysAllowReadOnly"
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"
@ -234,6 +235,7 @@ export interface WebviewMessage {
visibility?: ShareVisibility // For share visibility
hasContent?: boolean // For checkRulesDirectoryResult
checkOnly?: boolean // For deleteCustomMode check
pattern?: string // For whitelistCommand
codeIndexSettings?: {
// Global state settings
codebaseIndexEnabled: boolean

View file

@ -0,0 +1,60 @@
import { COMMAND_OUTPUT_STRING } from "./combineCommandSequences"
export interface ParsedCommand {
command: string
output: string
suggestions: string[]
}
/**
* Parses command text to extract the command, output, and suggestions.
* Supports both <suggestions> JSON array format and individual <suggest> tags.
*/
export const parseCommandAndOutput = (text: string | undefined): ParsedCommand => {
if (!text) {
return { command: "", output: "", suggestions: [] }
}
// First, extract suggestions from the text
const suggestions: string[] = []
// Parse <suggestions> tag with JSON array
const suggestionsMatch = text.match(/<suggestions>([\s\S]*?)<\/suggestions>/)
if (suggestionsMatch) {
try {
const parsed = JSON.parse(suggestionsMatch[1])
if (Array.isArray(parsed)) {
suggestions.push(...parsed.filter((s: any) => typeof s === "string" && s.trim()))
}
} catch {
// Invalid JSON, ignore
}
// Remove the suggestions tag from text
text = text.replace(/<suggestions>[\s\S]*?<\/suggestions>/, "")
}
// Parse individual <suggest> tags
let suggestMatch
const suggestRegex = /<suggest>([\s\S]*?)<\/suggest>/g
while ((suggestMatch = suggestRegex.exec(text)) !== null) {
const suggestion = suggestMatch[1].trim()
if (suggestion) {
suggestions.push(suggestion)
}
}
// Remove all suggest tags from text
text = text.replace(/<suggest>[\s\S]*?<\/suggest>/g, "")
// Now parse command and output
const index = text.indexOf(COMMAND_OUTPUT_STRING)
if (index === -1) {
return { command: text.trim(), output: "", suggestions }
}
return {
command: text.slice(0, index).trim(),
output: text.slice(index + COMMAND_OUTPUT_STRING.length),
suggestions,
}
}

View file

@ -0,0 +1,308 @@
/**
* Extracts a generalizable command pattern from a specific command.
* This function creates patterns that can be used for whitelisting similar commands.
*
* Examples:
* - "npm test" -> "npm test"
* - "npm run build" -> "npm run"
* - "git commit -m 'message'" -> "git commit"
* - "echo 'hello world'" -> "echo"
* - "python script.py --arg value" -> "python"
* - "./scripts/deploy.sh production" -> "./scripts/deploy.sh"
* - "cd /path/to/dir && npm install" -> "cd * && npm install"
* - "rm -rf node_modules" -> "rm"
*/
export function extractCommandPattern(command: string): string {
if (!command?.trim()) return ""
// Remove leading/trailing whitespace
const trimmedCommand = command.trim()
// Check if this is a chained command
// Use a more robust regex that handles nested quotes properly
const operators = ["&&", "||", ";", "|"]
let chainOperator: string | null = null
let splitIndex = -1
// Find the first unquoted operator
let inSingleQuote = false
let inDoubleQuote = false
let escapeNext = false
for (let i = 0; i < trimmedCommand.length; i++) {
const char = trimmedCommand[i]
if (escapeNext) {
escapeNext = false
continue
}
if (char === "\\") {
escapeNext = true
continue
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
continue
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote
continue
}
// Only look for operators outside of quotes
if (!inSingleQuote && !inDoubleQuote) {
for (const op of operators) {
if (trimmedCommand.substring(i, i + op.length) === op) {
chainOperator = op
splitIndex = i
break
}
}
if (chainOperator) break
}
}
if (chainOperator && splitIndex > 0) {
const firstPart = trimmedCommand.substring(0, splitIndex).trim()
const restPart = trimmedCommand.substring(splitIndex + chainOperator.length).trim()
// Process each part separately
const firstPattern = extractSingleCommandPattern(firstPart)
const restPattern = extractCommandPattern(restPart)
// For security, limit the depth of chained commands
// Count existing operators in the pattern to prevent deeply nested chains
const operatorCount = (restPattern.match(/&&|\|\||;|\|/g) || []).length
if (operatorCount >= 3) {
// Too many chained commands, return a more restrictive pattern
return firstPattern
}
return `${firstPattern} ${chainOperator} ${restPattern}`
}
// Not a chained command, process normally
return extractSingleCommandPattern(trimmedCommand)
}
/**
* Extracts pattern from a single command (not chained)
*/
function extractSingleCommandPattern(command: string): string {
const firstCommand = command
// Split the command into tokens, respecting quotes
const tokens: string[] = []
let currentToken = ""
let inSingleQuote = false
let inDoubleQuote = false
let escapeNext = false
for (let i = 0; i < firstCommand.length; i++) {
const char = firstCommand[i]
if (escapeNext) {
currentToken += char
escapeNext = false
continue
}
if (char === "\\") {
escapeNext = true
currentToken += char
continue
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
currentToken += char
continue
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote
currentToken += char
continue
}
if (char === " " && !inSingleQuote && !inDoubleQuote) {
if (currentToken) {
tokens.push(currentToken)
currentToken = ""
}
} else {
currentToken += char
}
}
if (currentToken) {
tokens.push(currentToken)
}
if (tokens.length === 0) return ""
const baseCommand = tokens[0]
// Special handling for common patterns
// 1. npm/yarn/pnpm commands - include subcommand with wildcards for scripts
if (["npm", "yarn", "pnpm", "bun"].includes(baseCommand) && tokens.length > 1) {
const subCommand = tokens[1]
// For "run" commands, check the script name
if (subCommand === "run" && tokens.length > 2) {
const _scriptName = tokens[2].replace(/^["']|["']$/g, "") // Remove quotes if present
// Check if there's a -- separator (pass-through args)
const _hasPassThroughArgs = tokens.includes("--")
// Always return just "npm run" without the script name
// This allows all npm run commands without using wildcards
return `${baseCommand} run`
}
// For direct scripts like "npm test", "npm build", include the script name
if (!subCommand.startsWith("-")) {
return `${baseCommand} ${subCommand}`
}
}
// 2. git commands - include subcommand
if (baseCommand === "git" && tokens.length > 1) {
const subCommand = tokens[1]
if (!subCommand.startsWith("-")) {
return `${baseCommand} ${subCommand}`
}
}
// 3. Script files - include the full script path
if (
baseCommand.includes("/") ||
baseCommand.endsWith(".sh") ||
baseCommand.endsWith(".py") ||
baseCommand.endsWith(".js") ||
baseCommand.endsWith(".rb")
) {
return baseCommand
}
// 4. Python/node/ruby/etc interpreters - just the interpreter
if (["python", "python3", "node", "ruby", "perl", "php", "java", "go"].includes(baseCommand)) {
return baseCommand
}
// 5. Common shell commands with dangerous flags - just the command
if (["rm", "mv", "cp", "chmod", "chown", "find", "grep", "sed", "awk"].includes(baseCommand)) {
return baseCommand
}
// 6. cd command - just return cd
if (baseCommand === "cd") {
return "cd"
}
// 7. Docker/kubectl commands - include subcommand
if (["docker", "kubectl", "helm"].includes(baseCommand) && tokens.length > 1) {
const subCommand = tokens[1]
if (!subCommand.startsWith("-")) {
return `${baseCommand} ${subCommand}`
}
}
// 8. Make commands - include target if present
if (baseCommand === "make" && tokens.length > 1) {
const target = tokens[1]
if (!target.startsWith("-")) {
return `${baseCommand} ${target}`
}
}
// 9. Environment variables - handle with care
if (baseCommand.includes("=")) {
// This might be an environment variable like NODE_ENV=production
const envMatch = baseCommand.match(/^([A-Z_]+)=/)
if (envMatch) {
// Return the full environment variable assignment
return baseCommand
}
}
// 10. Commands with suspicious patterns - be restrictive
// Check for potential command injection patterns
if (baseCommand.includes("$") || baseCommand.includes("`") || baseCommand.includes("(")) {
// These could be command substitutions or variables, be very restrictive
return baseCommand.split(/[$`(]/)[0].trim() || "echo"
}
// Default: just return the base command
return baseCommand
}
/**
* Get a human-readable description of what the pattern will allow
*
* Examples:
* - "npm test" -> "npm test commands"
* - "npm run" -> "npm run scripts"
* - "git commit" -> "git commit commands"
* - "python" -> "python scripts"
* - "./scripts/deploy.sh" -> "this specific script"
*/
export function getPatternDescription(pattern: string): string {
if (!pattern) return ""
const tokens = pattern.split(" ")
const baseCommand = tokens[0]
// npm/yarn/pnpm patterns
if (["npm", "yarn", "pnpm", "bun"].includes(baseCommand)) {
if (tokens[1] === "run") {
// For "npm run", describe what it allows
return `all ${baseCommand} run scripts`
}
if (tokens[1]) {
return `${baseCommand} ${tokens[1]} commands`
}
return `${baseCommand} commands`
}
// git patterns
if (baseCommand === "git" && tokens[1]) {
return `git ${tokens[1]} commands`
}
// Script files
if (
baseCommand.includes("/") ||
baseCommand.endsWith(".sh") ||
baseCommand.endsWith(".py") ||
baseCommand.endsWith(".js") ||
baseCommand.endsWith(".rb")
) {
return "this specific script"
}
// Interpreters
if (["python", "python3", "node", "ruby", "perl", "php", "java", "go"].includes(baseCommand)) {
return `${baseCommand} scripts`
}
// Docker/kubectl
if (["docker", "kubectl", "helm"].includes(baseCommand) && tokens[1]) {
return `${baseCommand} ${tokens[1]} commands`
}
// Make
if (baseCommand === "make" && tokens[1]) {
return `make ${tokens[1]} target`
}
// cd
if (baseCommand === "cd") {
return "directory navigation"
}
// Default
return `${baseCommand} commands`
}

View file

@ -65,6 +65,7 @@ export const toolParamNames = [
"query",
"args",
"todos",
"suggestions",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@ -80,7 +81,7 @@ export interface ToolUse {
export interface ExecuteCommandToolUse extends ToolUse {
name: "execute_command"
// Pick<Record<ToolParamName, string>, "command"> makes "command" required, but Partial<> makes it optional
params: Partial<Pick<Record<ToolParamName, string>, "command" | "cwd">>
params: Partial<Pick<Record<ToolParamName, string>, "command" | "cwd" | "suggestions">>
}
export interface ReadFileToolUse extends ToolUse {

View file

@ -1,4 +1,4 @@
import { useCallback, useState, memo, useMemo } from "react"
import { useCallback, useState, useMemo } from "react"
import { useEvent } from "react-use"
import { ChevronDown, Skull } from "lucide-react"
@ -6,13 +6,16 @@ import { CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { safeJsonParse } from "@roo/safeJsonParse"
import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
import { parseCommandAndOutput } from "@roo/commandParsing"
import { vscode } from "@src/utils/vscode"
import { extractCommandPattern, getPatternDescription } from "@src/utils/extract-command-pattern"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { cn } from "@src/lib/utils"
import { Button } from "@src/components/ui"
import CodeBlock from "../common/CodeBlock"
import { CommandPatternSelector } from "./CommandPatternSelector"
interface CommandExecutionProps {
executionId: string
@ -22,15 +25,181 @@ interface CommandExecutionProps {
}
export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => {
const { terminalShellIntegrationDisabled = false } = useExtensionState()
const { t } = useAppTranslation()
const { terminalShellIntegrationDisabled = false, allowedCommands = [] } = useExtensionState()
const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text])
const { command, output: parsedOutput, suggestions } = useMemo(() => parseCommandAndOutput(text), [text])
// If we aren't opening the VSCode terminal for this command then we default
// to expanding the command execution output.
const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled)
const [_isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled)
const [streamingOutput, setStreamingOutput] = useState("")
const [status, setStatus] = useState<CommandExecutionStatus | null>(null)
// Separate state for output expansion - default to closed
const [isOutputExpanded, setIsOutputExpanded] = useState(false)
// Determine if we should show suggestions section
const showSuggestions = suggestions && suggestions.length > 0
// Use suggestions if available, otherwise extract command patterns
const commandPatterns = useMemo(() => {
// If we have suggestions from the text, use those
if (suggestions && suggestions.length > 0) {
return suggestions.map((pattern: string) => ({
pattern,
description: getPatternDescription(pattern),
}))
}
// Only extract patterns if we're showing suggestions (for backward compatibility)
if (!showSuggestions || !command?.trim()) return []
// Check if this is a chained command
const operators = ["&&", "||", ";", "|"]
const patterns: Array<{ pattern: string; description: string }> = []
// Split by operators while respecting quotes
let inSingleQuote = false
let inDoubleQuote = false
let escapeNext = false
let currentCommand = ""
let i = 0
while (i < command.length) {
const char = command[i]
if (escapeNext) {
currentCommand += char
escapeNext = false
i++
continue
}
if (char === "\\") {
escapeNext = true
currentCommand += char
i++
continue
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote
currentCommand += char
i++
continue
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote
currentCommand += char
i++
continue
}
// Check for operators outside quotes
if (!inSingleQuote && !inDoubleQuote) {
let foundOperator = false
for (const op of operators) {
if (command.substring(i, i + op.length) === op) {
// Found an operator, process the current command
const trimmedCommand = currentCommand.trim()
if (trimmedCommand) {
// For npm commands, generate multiple pattern options
if (trimmedCommand.startsWith("npm ")) {
// Add the specific pattern
const specificPattern = extractCommandPattern(trimmedCommand)
if (specificPattern) {
patterns.push({
pattern: specificPattern,
description: getPatternDescription(specificPattern),
})
}
// Add broader npm patterns
if (trimmedCommand.startsWith("npm run ")) {
// Add "npm run" pattern
patterns.push({
pattern: "npm run",
description: t("chat:commandExecution.allowAllNpmRun"),
})
}
// Add "npm" pattern
patterns.push({
pattern: "npm",
description: t("chat:commandExecution.allowAllNpm"),
})
} else {
// For non-npm commands, just add the extracted pattern
const pattern = extractCommandPattern(trimmedCommand)
if (pattern) {
patterns.push({
pattern,
description: getPatternDescription(pattern),
})
}
}
}
currentCommand = ""
i += op.length
foundOperator = true
break
}
}
if (foundOperator) continue
}
currentCommand += char
i++
}
// Process the last command
const trimmedCommand = currentCommand.trim()
if (trimmedCommand) {
// For npm commands, generate multiple pattern options
if (trimmedCommand.startsWith("npm ")) {
// Add the specific pattern
const specificPattern = extractCommandPattern(trimmedCommand)
if (specificPattern) {
patterns.push({
pattern: specificPattern,
description: getPatternDescription(specificPattern),
})
}
// Add broader npm patterns
if (trimmedCommand.startsWith("npm run ")) {
// Add "npm run" pattern
patterns.push({
pattern: "npm run",
description: t("chat:commandExecution.allowAllNpmRun"),
})
}
// Add "npm" pattern
patterns.push({
pattern: "npm",
description: t("chat:commandExecution.allowAllNpm"),
})
} else {
// For non-npm commands, just add the extracted pattern
const pattern = extractCommandPattern(trimmedCommand)
if (pattern) {
patterns.push({
pattern,
description: getPatternDescription(pattern),
})
}
}
}
// Remove duplicates
const uniquePatterns = patterns.filter(
(item, index, self) => index === self.findIndex((p) => p.pattern === item.pattern),
)
return uniquePatterns
}, [command, suggestions, showSuggestions, t])
// The command's output can either come from the text associated with the
// task message (this is the case for completed commands) or from the
@ -73,89 +242,124 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
useEvent("message", onMessage)
const handleAllowPatternChange = useCallback(
(pattern: string) => {
if (!pattern) return
const isWhitelisted = allowedCommands.includes(pattern)
let updatedAllowedCommands: string[]
if (isWhitelisted) {
// Remove from whitelist
updatedAllowedCommands = allowedCommands.filter((p) => p !== pattern)
} else {
// Add to whitelist
updatedAllowedCommands = [...allowedCommands, pattern]
}
// Use consistent message type for both add and remove operations
vscode.postMessage({
type: "allowedCommands",
commands: updatedAllowedCommands,
})
},
[allowedCommands],
)
return (
<>
<div className="flex flex-row items-center justify-between gap-2 mb-1">
<div className="flex flex-row items-center gap-1">
<div className="w-full">
{/* Header section */}
<div className="flex flex-row items-center justify-between gap-2 px-3 py-2 bg-vscode-editor-background border border-vscode-border rounded-t-md">
<div className="flex flex-row items-center gap-2 flex-1">
{icon}
{title}
</div>
<div className="flex flex-row items-center justify-between gap-2 px-1">
<div className="flex flex-row items-center gap-1">
{status?.status === "started" && (
<div className="flex flex-row items-center gap-2 font-mono text-xs">
<div className="rounded-full size-1.5 bg-lime-400" />
<div>Running</div>
{status.pid && <div className="whitespace-nowrap">(PID: {status.pid})</div>}
<Button
variant="ghost"
size="icon"
onClick={() =>
vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
}>
<Skull />
</Button>
</div>
)}
{status?.status === "exited" && (
<div className="flex flex-row items-center gap-2 font-mono text-xs">
<div
className={cn(
"rounded-full size-1.5",
status.exitCode === 0 ? "bg-lime-400" : "bg-red-400",
)}
/>
<div className="whitespace-nowrap">Exited ({status.exitCode})</div>
</div>
)}
{output.length > 0 && (
<Button variant="ghost" size="icon" onClick={() => setIsExpanded(!isExpanded)}>
<ChevronDown
className={cn("size-4 transition-transform duration-300", {
"rotate-180": isExpanded,
})}
/>
{/* Status display in the middle */}
{status?.status === "started" && (
<div className="flex flex-row items-center gap-2 font-mono text-xs ml-auto">
<div className="rounded-full size-1.5 bg-lime-400" />
<div className="whitespace-nowrap">{t("chat:commandExecution.running")}</div>
{status.pid && (
<span className="text-vscode-descriptionForeground/70">
{t("chat:commandExecution.pid", { pid: status.pid })}
</span>
)}
<Button
variant="ghost"
size="icon"
className="hover:bg-vscode-toolbar-hoverBackground"
onClick={() =>
vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
}
aria-label="Abort command execution">
<Skull className="size-3.5" />
</Button>
)}
</div>
</div>
)}
{status?.status === "exited" && (
<div className="flex flex-row items-center gap-2 font-mono text-xs ml-auto">
<div
className={cn(
"rounded-full size-1.5",
status.exitCode === 0 ? "bg-lime-400" : "bg-red-400",
)}
/>
<div className="whitespace-nowrap">
{t("chat:commandExecution.exited", { exitCode: status.exitCode })}
</div>
</div>
)}
</div>
{/* Output toggle chevron on the right */}
{output.length > 0 && (
<Button
variant="ghost"
size="icon"
className="hover:bg-vscode-toolbar-hoverBackground p-0.5"
onClick={() => setIsOutputExpanded(!isOutputExpanded)}
aria-label={isOutputExpanded ? "Collapse output" : "Expand output"}
aria-expanded={isOutputExpanded}>
<ChevronDown
className={cn("size-3.5 transition-transform duration-200", {
"-rotate-90": !isOutputExpanded,
"rotate-0": isOutputExpanded,
})}
/>
</Button>
)}
</div>
<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2">
<CodeBlock source={command} language="shell" />
<OutputContainer isExpanded={isExpanded} output={output} />
{/* Command execution box */}
<div className="bg-vscode-editor-background border-x border-b border-vscode-border rounded-b-md">
{/* Command display */}
<div className="p-3">
<CodeBlock source={command} language="shell" />
</div>
{/* Whitelist section */}
{showSuggestions && (
<CommandPatternSelector
patterns={commandPatterns}
allowedCommands={allowedCommands}
onPatternChange={handleAllowPatternChange}
/>
)}
{/* Output section */}
{output.length > 0 && (
<div
className={cn("border-t border-vscode-panel-border", {
hidden: !isOutputExpanded,
})}>
<div className="p-3">
<CodeBlock source={output} language="log" />
</div>
</div>
)}
</div>
</>
</div>
)
}
CommandExecution.displayName = "CommandExecution"
const OutputContainerInternal = ({ isExpanded, output }: { isExpanded: boolean; output: string }) => (
<div
className={cn("overflow-hidden", {
"max-h-0": !isExpanded,
"max-h-[100%] mt-1 pt-1 border-t border-border/25": isExpanded,
})}>
{output.length > 0 && <CodeBlock source={output} language="log" />}
</div>
)
const OutputContainer = memo(OutputContainerInternal)
const parseCommandAndOutput = (text: string | undefined) => {
if (!text) {
return { command: "", output: "" }
}
const index = text.indexOf(COMMAND_OUTPUT_STRING)
if (index === -1) {
return { command: text, output: "" }
}
return {
command: text.slice(0, index),
output: text.slice(index + COMMAND_OUTPUT_STRING.length),
}
}

View file

@ -0,0 +1,60 @@
import { useState } from "react"
import { ChevronDown } from "lucide-react"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { cn } from "@src/lib/utils"
interface CommandPattern {
pattern: string
description: string
}
interface CommandPatternSelectorProps {
patterns: CommandPattern[]
allowedCommands: string[]
onPatternChange: (pattern: string) => void
}
export const CommandPatternSelector = ({ patterns, allowedCommands, onPatternChange }: CommandPatternSelectorProps) => {
const { t } = useAppTranslation()
const [isExpanded, setIsExpanded] = useState(false)
if (patterns.length === 0) {
return null
}
return (
<div className="border-t border-vscode-panel-border bg-vscode-sideBar-background/30">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-2 w-full px-3 py-2 text-xs text-vscode-descriptionForeground hover:text-vscode-foreground hover:bg-vscode-list-hoverBackground transition-all"
aria-label={isExpanded ? "Collapse allowed commands section" : "Expand allowed commands section"}
aria-expanded={isExpanded}>
<ChevronDown
className={cn("size-3 transition-transform duration-200", {
"rotate-0": isExpanded,
"-rotate-90": !isExpanded,
})}
/>
<span className="font-medium">{t("chat:commandExecution.addToAllowedCommands")}</span>
</button>
{isExpanded && (
<div className="px-3 pb-3 space-y-1.5">
{patterns.map((item, index) => (
<div key={`${item.pattern}-${index}`} className="ml-5">
<VSCodeCheckbox
checked={allowedCommands.includes(item.pattern)}
onChange={() => onPatternChange(item.pattern)}
className="text-xs"
aria-label={`Allow command pattern: ${item.pattern}`}>
<span className="font-mono text-vscode-foreground">{item.pattern}</span>
</VSCodeCheckbox>
</div>
))}
</div>
)}
</div>
)
}
CommandPatternSelector.displayName = "CommandPatternSelector"

View file

@ -0,0 +1,391 @@
// npx vitest run src/components/chat/__tests__/CommandExecution.spec.tsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { vi } from "vitest"
import { CommandExecution } from "../CommandExecution"
import { TooltipProvider } from "@/components/ui/tooltip"
// Mock the vscode module
vi.mock("@src/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
// Return the actual translated text for the test
if (key === "chat:commandExecution.addToAllowedCommands") {
return "Add to Allowed Auto-Execute Patterns"
}
return key
},
}),
initReactI18next: {
type: "3rdParty",
init: () => {},
},
}))
// Mock TranslationContext
vi.mock("@src/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => {
// Return the actual translated text for the test
if (key === "chat:commandExecution.addToAllowedCommands") {
return "Add to Allowed Auto-Execute Patterns"
}
return key
},
}),
}))
// Mock ExtensionStateContext
vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: vi.fn(() => ({
allowedCommands: [],
})),
}))
// Get the mocked vscode after mocks are set up
import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
const mockPostMessage = vi.mocked(vscode.postMessage)
const mockUseExtensionState = vi.mocked(useExtensionState)
// Helper function to render with providers
const renderWithProviders = (ui: React.ReactElement) => {
return render(<TooltipProvider delayDuration={0}>{ui}</TooltipProvider>)
}
describe("CommandExecution", () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset the mock to default state
mockUseExtensionState.mockReturnValue({
allowedCommands: [],
} as any)
})
it("should render command without suggestions", () => {
renderWithProviders(
<CommandExecution
executionId="test-1"
text="npm install"
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText("npm install")).toBeInTheDocument()
expect(screen.queryByText("Add to Allowed Auto-Execute Patterns")).not.toBeInTheDocument()
})
it("should render command with suggestions section collapsed by default", () => {
const commandWithSuggestions =
'npm install<suggestions>["npm install --save", "npm install --save-dev", "npm install --global"]</suggestions>'
renderWithProviders(
<CommandExecution
executionId="test-2"
text={commandWithSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText("npm install")).toBeInTheDocument()
expect(screen.getByText("Add to Allowed Auto-Execute Patterns")).toBeInTheDocument()
// Suggestions should not be visible initially (collapsed)
expect(screen.queryByDisplayValue("npm install --save")).not.toBeInTheDocument()
expect(screen.queryByDisplayValue("npm install --save-dev")).not.toBeInTheDocument()
expect(screen.queryByDisplayValue("npm install --global")).not.toBeInTheDocument()
})
it("should expand and show checkboxes when section header is clicked", () => {
const commandWithSuggestions =
'npm install<suggestions>["npm install --save", "npm install --save-dev", "npm install --global"]</suggestions>'
renderWithProviders(
<CommandExecution
executionId="test-2"
text={commandWithSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
// Click to expand the section
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
// Now suggestions should be visible as checkboxes
expect(screen.getByText("npm install --save")).toBeInTheDocument()
expect(screen.getByText("npm install --save-dev")).toBeInTheDocument()
expect(screen.getByText("npm install --global")).toBeInTheDocument()
// Should have checkboxes
const checkboxes = screen.getAllByRole("checkbox")
expect(checkboxes).toHaveLength(3)
})
it("should handle checking a suggestion checkbox to add to whitelist", async () => {
const commandWithSuggestions =
'git commit<suggestions>["git commit -m \\"Initial commit\\"", "git commit --amend"]</suggestions>'
renderWithProviders(
<CommandExecution
executionId="test-3"
text={commandWithSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
// Expand the section first
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
// Find and check the checkbox for the first suggestion
const checkboxes = screen.getAllByRole("checkbox")
fireEvent.click(checkboxes[0])
await waitFor(() => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "allowedCommands",
commands: expect.arrayContaining(['git commit -m "Initial commit"']),
})
})
})
it("should handle unchecking a suggestion checkbox to remove from whitelist", async () => {
// Clear any previous calls
vi.clearAllMocks()
// Mock that the command is already whitelisted
mockUseExtensionState.mockReturnValue({
allowedCommands: ['git commit -m "Initial commit"', "git commit --amend"],
} as any)
const commandWithSuggestions =
'git commit<suggestions>["git commit -m \\"Initial commit\\"", "git commit --amend"]</suggestions>'
renderWithProviders(
<CommandExecution
executionId="test-3"
text={commandWithSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
// Expand the section first
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
// Wait for the section to be rendered
await waitFor(() => {
const checkboxes = screen.getAllByRole("checkbox")
expect(checkboxes).toHaveLength(2)
})
// Find the checkbox for the first suggestion
const checkboxes = screen.getAllByRole("checkbox")
// Skip the assertion about initial state and just test the toggle functionality
// This works around the test environment issue with VSCodeCheckbox
fireEvent.click(checkboxes[0])
await waitFor(() => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "allowedCommands",
commands: ["git commit --amend"], // Should remove the clicked one
})
})
})
it("should handle empty suggestions tag", () => {
const commandWithEmptySuggestions = "ls -la<suggestions>[]</suggestions>"
renderWithProviders(
<CommandExecution
executionId="test-4"
text={commandWithEmptySuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText("ls -la")).toBeInTheDocument()
expect(screen.queryByText("Add to Allowed Auto-Execute Patterns")).not.toBeInTheDocument()
})
it("should handle suggestions with special characters", () => {
const commandWithSuggestions =
'echo "test"<suggestions>["echo \\"Hello, World!\\"", "echo $HOME", "echo `date`"]</suggestions>'
renderWithProviders(
<CommandExecution
executionId="test-5"
text={commandWithSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText('echo "test"')).toBeInTheDocument()
// Expand the section to see suggestions
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
expect(screen.getByText('echo "Hello, World!"')).toBeInTheDocument()
expect(screen.getByText("echo $HOME")).toBeInTheDocument()
expect(screen.getByText("echo `date`")).toBeInTheDocument()
})
it("should handle malformed suggestions tag", () => {
const commandWithMalformedSuggestions = "pwd<suggestions>not-valid-json</suggestions>"
renderWithProviders(
<CommandExecution
executionId="test-6"
text={commandWithMalformedSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
// Should still render the command
expect(screen.getByText("pwd")).toBeInTheDocument()
// Suggestions should not be shown when JSON is invalid
expect(screen.queryByText("Add to Allowed Auto-Execute Patterns")).not.toBeInTheDocument()
})
it("should parse suggestions from JSON array and show them when expanded", () => {
const commandWithSuggestions =
'docker run<suggestions>["docker run -it ubuntu:latest", "docker run -d nginx", "docker run --rm alpine"]</suggestions>'
renderWithProviders(
<CommandExecution
executionId="test-7"
text={commandWithSuggestions}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText("docker run")).toBeInTheDocument()
// Expand the section
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
expect(screen.getByText("docker run -it ubuntu:latest")).toBeInTheDocument()
expect(screen.getByText("docker run -d nginx")).toBeInTheDocument()
expect(screen.getByText("docker run --rm alpine")).toBeInTheDocument()
})
it("should handle individual <suggest> tags", () => {
const commandWithIndividualSuggests = "npm run start<suggest>npm run</suggest><suggest>npm start</suggest>"
renderWithProviders(
<CommandExecution
executionId="test-8"
text={commandWithIndividualSuggests}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText("npm run start")).toBeInTheDocument()
// Expand the section
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
expect(screen.getByText("npm run")).toBeInTheDocument()
expect(screen.getByText("npm start")).toBeInTheDocument()
})
it("should handle checking individual suggest tag suggestions", async () => {
const commandWithIndividualSuggests =
"git status<suggest>git status --short</suggest><suggest>git status -b</suggest>"
renderWithProviders(
<CommandExecution
executionId="test-9"
text={commandWithIndividualSuggests}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
// Expand the section
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
// Find and check the checkbox for the first suggestion
const checkboxes = screen.getAllByRole("checkbox")
fireEvent.click(checkboxes[0])
await waitFor(() => {
expect(mockPostMessage).toHaveBeenCalledWith({
type: "allowedCommands",
commands: expect.arrayContaining(["git status --short"]),
})
})
})
it("should handle mixed XML content with individual suggest tags", () => {
const commandWithMixedContent =
"npm install<suggest>npm install --save</suggest><suggest>npm install --save-dev</suggest>"
renderWithProviders(
<CommandExecution
executionId="test-10"
text={commandWithMixedContent}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
// Should clean up the command text and show only the command
expect(screen.getByText("npm install")).toBeInTheDocument()
// Expand the section
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
expect(screen.getByText("npm install --save")).toBeInTheDocument()
expect(screen.getByText("npm install --save-dev")).toBeInTheDocument()
})
it("should handle empty individual suggest tags", () => {
const commandWithEmptyIndividualSuggests = "ls -la<suggest></suggest><suggest>ls -la --color</suggest>"
renderWithProviders(
<CommandExecution
executionId="test-11"
text={commandWithEmptyIndividualSuggests}
icon={<span>icon</span>}
title={<span>Run Command</span>}
/>,
)
expect(screen.getByText("ls -la")).toBeInTheDocument()
// Expand the section
const sectionHeader = screen.getByText("Add to Allowed Auto-Execute Patterns")
fireEvent.click(sectionHeader)
// Should only show the non-empty suggestion
expect(screen.getByText("ls -la --color")).toBeInTheDocument()
// Should have exactly one checkbox (the non-empty one)
const checkboxes = screen.getAllByRole("checkbox")
expect(checkboxes).toHaveLength(1)
})
})

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo vol veure noms de definicions de codi font utilitzats en aquest directori (fora de l'espai de treball):",
"didViewDefinitionsOutsideWorkspace": "Roo ha vist noms de definicions de codi font utilitzats en aquest directori (fora de l'espai de treball):"
},
"command": {
"addToWhitelist": "Afegeix a les ordres d'execució automàtica permeses",
"whitelistDescription": "Seleccioneu els patrons d'ordres per aprovar-los automàticament en el futur:",
"addSelected": "Afegeix els patrons seleccionats"
},
"commandOutput": "Sortida de l'ordre",
"response": "Resposta",
"arguments": "Arguments",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo möchte Quellcode-Definitionsnamen in diesem Verzeichnis (außerhalb des Arbeitsbereichs) anzeigen:",
"didViewDefinitionsOutsideWorkspace": "Roo hat Quellcode-Definitionsnamen in diesem Verzeichnis (außerhalb des Arbeitsbereichs) angezeigt:"
},
"command": {
"addToWhitelist": "Zu den erlaubten automatisch auszuführenden Befehlen hinzufügen",
"whitelistDescription": "Wähle Befehlsmuster aus, die in Zukunft automatisch genehmigt werden sollen:",
"addSelected": "Ausgewählte Muster hinzufügen"
},
"commandOutput": "Befehlsausgabe",
"response": "Antwort",
"arguments": "Argumente",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -204,6 +204,15 @@
"didSearch": "Found {{count}} result(s) for <code>{{query}}</code>:",
"resultTooltip": "Similarity score: {{score}} (click to open file)"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
},
"commandOutput": "Command Output",
"response": "Response",
"arguments": "Arguments",

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo quiere ver nombres de definiciones de código fuente utilizados en este directorio (fuera del espacio de trabajo):",
"didViewDefinitionsOutsideWorkspace": "Roo vio nombres de definiciones de código fuente utilizados en este directorio (fuera del espacio de trabajo):"
},
"command": {
"addToWhitelist": "Añadir a los comandos de ejecución automática permitidos",
"whitelistDescription": "Selecciona patrones de comandos para aprobar automáticamente en el futuro:",
"addSelected": "Añadir patrones seleccionados"
},
"commandOutput": "Salida del comando",
"response": "Respuesta",
"arguments": "Argumentos",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo veut voir les noms de définitions de code source utilisés dans ce répertoire (hors espace de travail) :",
"didViewDefinitionsOutsideWorkspace": "Roo a vu les noms de définitions de code source utilisés dans ce répertoire (hors espace de travail) :"
},
"command": {
"addToWhitelist": "Ajouter aux commandes d'exécution automatique autorisées",
"whitelistDescription": "Sélectionnez les modèles de commande à approuver automatiquement à l'avenir:",
"addSelected": "Ajouter les modèles sélectionnés"
},
"commandOutput": "Sortie de commande",
"response": "Réponse",
"arguments": "Arguments",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo इस निर्देशिका (कार्यक्षेत्र के बाहर) में उपयोग किए गए सोर्स कोड परिभाषा नामों को देखना चाहता है:",
"didViewDefinitionsOutsideWorkspace": "Roo ने इस निर्देशिका (कार्यक्षेत्र के बाहर) में उपयोग किए गए सोर्स कोड परिभाषा नामों को देखा:"
},
"command": {
"addToWhitelist": "अनुमत ऑटो-एक्ज़ीक्यूट कमांड में जोड़ें",
"whitelistDescription": "भविष्य में स्वचालित रूप से स्वीकृत करने के लिए कमांड पैटर्न चुनें:",
"addSelected": "चयनित पैटर्न जोड़ें"
},
"commandOutput": "कमांड आउटपुट",
"response": "प्रतिक्रिया",
"arguments": "आर्ग्युमेंट्स",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -210,6 +210,11 @@
"didSearch": "Ditemukan {{count}} hasil untuk <code>{{query}}</code>:",
"resultTooltip": "Skor kemiripan: {{score}} (klik untuk membuka file)"
},
"command": {
"addToWhitelist": "Tambahkan ke Perintah Eksekusi Otomatis yang Diizinkan",
"whitelistDescription": "Pilih pola perintah untuk disetujui secara otomatis di masa mendatang:",
"addSelected": "Tambahkan Pola yang Dipilih"
},
"commandOutput": "Output Perintah",
"response": "Respons",
"arguments": "Argumen",
@ -322,5 +327,14 @@
},
"versionIndicator": {
"ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo vuole visualizzare i nomi delle definizioni di codice sorgente utilizzate in questa directory (fuori dall'area di lavoro):",
"didViewDefinitionsOutsideWorkspace": "Roo ha visualizzato i nomi delle definizioni di codice sorgente utilizzate in questa directory (fuori dall'area di lavoro):"
},
"command": {
"addToWhitelist": "Aggiungi ai comandi ad esecuzione automatica consentiti",
"whitelistDescription": "Seleziona i modelli di comando da approvare automaticamente in futuro:",
"addSelected": "Aggiungi modelli selezionati"
},
"commandOutput": "Output del comando",
"response": "Risposta",
"arguments": "Argomenti",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Rooはこのディレクトリワークスペース外で使用されているソースコード定義名を表示したい:",
"didViewDefinitionsOutsideWorkspace": "Rooはこのディレクトリワークスペース外で使用されているソースコード定義名を表示しました:"
},
"command": {
"addToWhitelist": "許可された自動実行コマンドに追加",
"whitelistDescription": "今後自動的に承認するコマンドパターンを選択してください:",
"addSelected": "選択したパターンを追加"
},
"commandOutput": "コマンド出力",
"response": "応答",
"arguments": "引数",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)에서 사용된 소스 코드 정의 이름을 보고 싶어합니다:",
"didViewDefinitionsOutsideWorkspace": "Roo가 이 디렉토리(워크스페이스 외부)에서 사용된 소스 코드 정의 이름을 보았습니다:"
},
"command": {
"addToWhitelist": "허용된 자동 실행 명령에 추가",
"whitelistDescription": "이후에 자동으로 승인할 명령 패턴을 선택하세요:",
"addSelected": "선택한 패턴 추가"
},
"commandOutput": "명령 출력",
"response": "응답",
"arguments": "인수",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -184,6 +184,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo wil broncode-definitienamen bekijken die in deze map (buiten werkruimte) worden gebruikt:",
"didViewDefinitionsOutsideWorkspace": "Roo heeft broncode-definitienamen bekeken die in deze map (buiten werkruimte) worden gebruikt:"
},
"command": {
"addToWhitelist": "Toevoegen aan toegestane automatisch uit te voeren commando's",
"whitelistDescription": "Selecteer commandopatronen om in de toekomst automatisch goed te keuren:",
"addSelected": "Geselecteerde patronen toevoegen"
},
"commandOutput": "Commando-uitvoer",
"response": "Antwoord",
"arguments": "Argumenten",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Versie {{version}} - Klik om release notes te bekijken"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo chce zobaczyć nazwy definicji kodu źródłowego używane w tym katalogu (poza obszarem roboczym):",
"didViewDefinitionsOutsideWorkspace": "Roo zobaczył nazwy definicji kodu źródłowego używane w tym katalogu (poza obszarem roboczym):"
},
"command": {
"addToWhitelist": "Dodaj do dozwolonych poleceń automatycznego wykonywania",
"whitelistDescription": "Wybierz wzorce poleceń do automatycznego zatwierdzania w przyszłości:",
"addSelected": "Dodaj wybrane wzorce"
},
"commandOutput": "Wyjście polecenia",
"response": "Odpowiedź",
"arguments": "Argumenty",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo quer visualizar nomes de definição de código-fonte usados neste diretório (fora do espaço de trabalho):",
"didViewDefinitionsOutsideWorkspace": "Roo visualizou nomes de definição de código-fonte usados neste diretório (fora do espaço de trabalho):"
},
"command": {
"addToWhitelist": "Adicionar aos Comandos de Execução Automática Permitidos",
"whitelistDescription": "Selecione padrões de comando para aprovar automaticamente no futuro:",
"addSelected": "Adicionar Padrões Selecionados"
},
"commandOutput": "Saída do comando",
"response": "Resposta",
"arguments": "Argumentos",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -184,6 +184,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo хочет просмотреть имена определений исходного кода в этой директории (вне рабочего пространства):",
"didViewDefinitionsOutsideWorkspace": "Roo просмотрел имена определений исходного кода в этой директории (вне рабочего пространства):"
},
"command": {
"addToWhitelist": "Добавить в разрешенные для автоматического выполнения команды",
"whitelistDescription": "Выберите шаблоны команд для автоматического утверждения в будущем:",
"addSelected": "Добавить выбранные шаблоны"
},
"commandOutput": "Вывод команды",
"response": "Ответ",
"arguments": "Аргументы",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo bu dizinde (çalışma alanı dışında) kullanılan kaynak kod tanımlama isimlerini görüntülemek istiyor:",
"didViewDefinitionsOutsideWorkspace": "Roo bu dizinde (çalışma alanı dışında) kullanılan kaynak kod tanımlama isimlerini görüntüledi:"
},
"command": {
"addToWhitelist": "İzin Verilen Otomatik Yürütme Komutlarına Ekle",
"whitelistDescription": "Gelecekte otomatik olarak onaylanacak komut desenlerini seçin:",
"addSelected": "Seçili Desenleri Ekle"
},
"commandOutput": "Komut Çıktısı",
"response": "Yanıt",
"arguments": "Argümanlar",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo muốn xem tên định nghĩa mã nguồn được sử dụng trong thư mục này (ngoài không gian làm việc):",
"didViewDefinitionsOutsideWorkspace": "Roo đã xem tên định nghĩa mã nguồn được sử dụng trong thư mục này (ngoài không gian làm việc):"
},
"command": {
"addToWhitelist": "Thêm vào các lệnh được phép tự động thực thi",
"whitelistDescription": "Chọn các mẫu lệnh để tự động phê duyệt trong tương lai:",
"addSelected": "Thêm các mẫu đã chọn"
},
"commandOutput": "Kết quả lệnh",
"response": "Phản hồi",
"arguments": "Tham số",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo想查看此目录中使用的源代码定义名称工作区外:",
"didViewDefinitionsOutsideWorkspace": "Roo已查看此目录中使用的源代码定义名称工作区外:"
},
"command": {
"addToWhitelist": "添加到允许的自动执行命令",
"whitelistDescription": "选择将来要自动批准的命令模式:",
"addSelected": "添加选定的模式"
},
"commandOutput": "命令输出",
"response": "响应",
"arguments": "参数",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "版本 {{version}} - 点击查看发布说明"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -189,6 +189,11 @@
"wantsToViewDefinitionsOutsideWorkspace": "Roo 想要檢視此目錄(工作區外)中使用的原始碼定義名稱:",
"didViewDefinitionsOutsideWorkspace": "Roo 已檢視此目錄(工作區外)中使用的原始碼定義名稱:"
},
"command": {
"addToWhitelist": "新增至允許的自動執行命令",
"whitelistDescription": "選取未來要自動核准的命令模式:",
"addSelected": "新增選取的模式"
},
"commandOutput": "命令輸出",
"response": "回應",
"arguments": "參數",
@ -316,5 +321,14 @@
},
"versionIndicator": {
"ariaLabel": "版本 {{version}} - 點擊查看發布說明"
},
"commandExecution": {
"whitelistSuggestions": "Suggested patterns to whitelist for automatic approval:",
"running": "Running",
"pid": "PID: {{pid}}",
"exited": "Exited ({{exitCode}})",
"addToAllowedCommands": "Add to Allowed Auto-Execute Commands",
"allowAllNpmRun": "Allow all npm run commands",
"allowAllNpm": "Allow all npm commands"
}
}

View file

@ -0,0 +1,2 @@
// Re-export from shared location
export { extractCommandPattern, getPatternDescription } from "@roo/commandPatterns"