mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
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:
parent
9b0f3b2435
commit
a65f5d72d4
49 changed files with 1976 additions and 533 deletions
|
|
@ -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>`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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í",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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í",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@
|
|||
"organization_share_link_copied": "संगठन साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!",
|
||||
"public_share_link_copied": "सार्वजनिक साझाकरण लिंक क्लिपबोर्ड में कॉपी किया गया!",
|
||||
"mode_exported": "मोड '{{mode}}' सफलतापूर्वक निर्यात किया गया",
|
||||
"mode_imported": "मोड सफलतापूर्वक आयात किया गया"
|
||||
"mode_imported": "मोड सफलतापूर्वक आयात किया गया",
|
||||
"command_whitelisted": "कमांड पैटर्न '{{pattern}}' को अनुमत कमांड सूची में जोड़ा गया है"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "हां",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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ì",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@
|
|||
"organization_share_link_copied": "組織共有リンクがクリップボードにコピーされました!",
|
||||
"public_share_link_copied": "公開共有リンクがクリップボードにコピーされました!",
|
||||
"mode_exported": "モード「{{mode}}」が正常にエクスポートされました",
|
||||
"mode_imported": "モードが正常にインポートされました"
|
||||
"mode_imported": "モードが正常にインポートされました",
|
||||
"command_whitelisted": "コマンドパターン '{{pattern}}' が許可されたコマンドリストに追加されました"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "はい",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@
|
|||
"organization_share_link_copied": "조직 공유 링크가 클립보드에 복사되었습니다!",
|
||||
"public_share_link_copied": "공개 공유 링크가 클립보드에 복사되었습니다!",
|
||||
"mode_exported": "'{{mode}}' 모드가 성공적으로 내보내졌습니다",
|
||||
"mode_imported": "모드를 성공적으로 가져왔습니다"
|
||||
"mode_imported": "모드를 성공적으로 가져왔습니다",
|
||||
"command_whitelisted": "명령 패턴 '{{pattern}}'이(가) 허용된 명령 목록에 추가되었습니다"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "예",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@
|
|||
"organization_share_link_copied": "Ссылка для совместного доступа организации скопирована в буфер обмена!",
|
||||
"public_share_link_copied": "Публичная ссылка для совместного доступа скопирована в буфер обмена!",
|
||||
"mode_exported": "Режим '{{mode}}' успешно экспортирован",
|
||||
"mode_imported": "Режим успешно импортирован"
|
||||
"mode_imported": "Режим успешно импортирован",
|
||||
"command_whitelisted": "Шаблон команды '{{pattern}}' добавлен в список разрешенных команд"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Да",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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ó",
|
||||
|
|
|
|||
|
|
@ -106,7 +106,8 @@
|
|||
"organization_share_link_copied": "组织分享链接已复制到剪贴板!",
|
||||
"public_share_link_copied": "公开分享链接已复制到剪贴板!",
|
||||
"mode_exported": "模式 '{{mode}}' 已成功导出",
|
||||
"mode_imported": "模式已成功导入"
|
||||
"mode_imported": "模式已成功导入",
|
||||
"command_whitelisted": "命令模式 '{{pattern}}' 已添加到允许的命令列表中"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "是",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@
|
|||
"organization_share_link_copied": "組織分享連結已複製到剪貼簿!",
|
||||
"public_share_link_copied": "公開分享連結已複製到剪貼簿!",
|
||||
"mode_exported": "模式 '{{mode}}' 已成功匯出",
|
||||
"mode_imported": "模式已成功匯入"
|
||||
"mode_imported": "模式已成功匯入",
|
||||
"command_whitelisted": "命令模式 '{{pattern}}' 已新增至允許的命令清單中"
|
||||
},
|
||||
"answers": {
|
||||
"yes": "是",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
60
src/shared/commandParsing.ts
Normal file
60
src/shared/commandParsing.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
308
src/shared/commandPatterns.ts
Normal file
308
src/shared/commandPatterns.ts
Normal 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`
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
60
webview-ui/src/components/chat/CommandPatternSelector.tsx
Normal file
60
webview-ui/src/components/chat/CommandPatternSelector.tsx
Normal 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"
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
webview-ui/src/utils/extract-command-pattern.ts
Normal file
2
webview-ui/src/utils/extract-command-pattern.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Re-export from shared location
|
||||
export { extractCommandPattern, getPatternDescription } from "@roo/commandPatterns"
|
||||
Loading…
Add table
Reference in a new issue