Skip to content

Commit 5fac683

Browse files
committed
i18n and error ux
1 parent 0322513 commit 5fac683

39 files changed

Lines changed: 232 additions & 5 deletions

src/core/tools/ExecuteCommandTool.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ interface ExecuteCommandParams {
6060
timeout?: number | null
6161
}
6262

63+
export function formatDcgBlockedMessage(reason?: string, ruleId?: string): string {
64+
if (reason && ruleId) {
65+
return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId })
66+
}
67+
68+
if (reason) {
69+
return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason })
70+
}
71+
72+
if (ruleId) {
73+
return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId })
74+
}
75+
76+
return t("tools:executeCommand.destructiveCommandGuard.blocked")
77+
}
78+
6379
export function resolveAgentTimeoutMs(timeoutSeconds: number | null | undefined): number {
6480
const requestedAgentTimeout = typeof timeoutSeconds === "number" && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 0
6581

@@ -132,10 +148,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
132148
const dcgResult = await runDcg(binaryPath, canonicalCommand, workingDirectory)
133149
dcgBlocked = dcgResult.decision === "deny"
134150
if (dcgResult.decision === "deny") {
135-
await task.say(
136-
"text",
137-
`Destructive Command Guard blocked this command${dcgResult.reason ? `: ${dcgResult.reason}` : "."}${dcgResult.ruleId ? ` (Rule: ${dcgResult.ruleId})` : ""}`,
138-
)
151+
await task.say("error", formatDcgBlockedMessage(dcgResult.reason, dcgResult.ruleId))
139152
}
140153
}
141154

src/core/tools/__tests__/executeCommandTool.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ vitest.mock("../../../integrations/terminal/TerminalRegistry", () => ({
4545
vitest.mock("../../task/Task")
4646
vitest.mock("../../prompts/responses")
4747

48+
const mockRunDcg = vitest.fn()
49+
const mockGetDcgBinaryPath = vitest.fn()
50+
51+
vitest.mock("../../../services/destructive-command-guard", () => ({
52+
runDcg: mockRunDcg,
53+
getDcgBinaryPath: mockGetDcgBinaryPath,
54+
}))
55+
4856
// Import the module
4957
import * as executeCommandModule from "../ExecuteCommandTool"
5058
const { executeCommandTool } = executeCommandModule
@@ -96,6 +104,8 @@ describe("executeCommandTool", () => {
96104
mockAskApproval = vitest.fn().mockResolvedValue(true)
97105
mockHandleError = vitest.fn().mockResolvedValue(undefined)
98106
mockPushToolResult = vitest.fn()
107+
mockRunDcg.mockResolvedValue({ decision: "allow" })
108+
mockGetDcgBinaryPath.mockReturnValue("/test/storage/dcg")
99109

100110
// Setup vscode config mock
101111
const mockConfig = {
@@ -199,6 +209,46 @@ describe("executeCommandTool", () => {
199209
})
200210

201211
describe("Error handling", () => {
212+
it.each([
213+
[undefined, undefined, "executeCommand.destructiveCommandGuard.blocked"],
214+
["matches a destructive pattern", undefined, "executeCommand.destructiveCommandGuard.blockedWithReason"],
215+
[undefined, "recursive-delete", "executeCommand.destructiveCommandGuard.blockedWithRule"],
216+
[
217+
"matches a destructive pattern",
218+
"recursive-delete",
219+
"executeCommand.destructiveCommandGuard.blockedWithReasonAndRule",
220+
],
221+
])("selects the localized DCG block message for reason %s and rule %s", (reason, ruleId, expected) => {
222+
expect(executeCommandModule.formatDcgBlockedMessage(reason, ruleId)).toBe(expected)
223+
})
224+
225+
it("shows a DCG block message as an error before requesting explicit approval", async () => {
226+
const provider = await mockCline.providerRef.deref()
227+
provider.context = { globalStorageUri: { fsPath: "/test/storage" } }
228+
provider.getState.mockResolvedValue({
229+
destructiveCommandGuardEnabled: true,
230+
terminalShellIntegrationDisabled: true,
231+
})
232+
mockRunDcg.mockResolvedValue({
233+
decision: "deny",
234+
reason: "matches a destructive pattern",
235+
ruleId: "recursive-delete",
236+
})
237+
mockAskApproval.mockResolvedValue(false)
238+
239+
await executeCommandTool.handle(mockCline as unknown as Task, mockToolUse, {
240+
askApproval: mockAskApproval as unknown as AskApproval,
241+
handleError: mockHandleError as unknown as HandleError,
242+
pushToolResult: mockPushToolResult as unknown as PushToolResult,
243+
})
244+
245+
expect(mockCline.say).toHaveBeenCalledWith(
246+
"error",
247+
"executeCommand.destructiveCommandGuard.blockedWithReasonAndRule",
248+
)
249+
expect(mockAskApproval).toHaveBeenCalledWith("command", "echo test", undefined, true)
250+
})
251+
202252
it("should handle missing command parameter", async () => {
203253
// Setup
204254
mockToolUse.params.command = undefined

src/core/webview/webviewMessageHandler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,9 @@ export const webviewMessageHandler = async (
688688
} catch (error) {
689689
message.updatedSettings.destructiveCommandGuardEnabled = false
690690
vscode.window.showErrorMessage(
691-
`Unable to enable Destructive Command Guard: ${error instanceof Error ? error.message : String(error)}`,
691+
t("common:errors.destructive_command_guard_enable_failed", {
692+
error: error instanceof Error ? error.message : String(error),
693+
}),
692694
)
693695
}
694696
}

src/i18n/locales/ca/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/ca/tools.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/de/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/de/tools.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"url_fetch_failed": "Failed to fetch URL content: {{error}}",
7373
"url_fetch_error_with_url": "Error fetching content for {{url}}: {{error}}",
7474
"command_timeout": "Command execution timed out after {{seconds}} seconds",
75+
"destructive_command_guard_enable_failed": "Unable to enable Destructive Command Guard: {{error}}",
7576
"share_task_failed": "Failed to share task. Please try again.",
7677
"share_no_active_task": "No active task to share",
7778
"share_auth_required": "Authentication required. Please sign in to share tasks.",

src/i18n/locales/en/tools.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
"codebaseSearch": {
1414
"approval": "Searching for '{{query}}' in codebase..."
1515
},
16+
"executeCommand": {
17+
"destructiveCommandGuard": {
18+
"blocked": "Destructive Command Guard blocked this command.",
19+
"blockedWithReason": "Destructive Command Guard blocked this command. Message from DCG: {{reason}}",
20+
"blockedWithRule": "Destructive Command Guard blocked this command. (Rule: {{ruleId}})",
21+
"blockedWithReasonAndRule": "Destructive Command Guard blocked this command. Message from DCG: {{reason}} (Rule: {{ruleId}})"
22+
}
23+
},
1624
"newTask": {
1725
"errors": {
1826
"policy_restriction": "Failed to create new task due to policy restrictions."

src/i18n/locales/es/common.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)