Skip to content

Commit 47fda76

Browse files
committed
i18n and error ux
1 parent 2149733 commit 47fda76

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
@@ -51,6 +51,22 @@ interface ExecuteCommandParams {
5151
timeout?: number | null
5252
}
5353

54+
export function formatDcgBlockedMessage(reason?: string, ruleId?: string): string {
55+
if (reason && ruleId) {
56+
return t("tools:executeCommand.destructiveCommandGuard.blockedWithReasonAndRule", { reason, ruleId })
57+
}
58+
59+
if (reason) {
60+
return t("tools:executeCommand.destructiveCommandGuard.blockedWithReason", { reason })
61+
}
62+
63+
if (ruleId) {
64+
return t("tools:executeCommand.destructiveCommandGuard.blockedWithRule", { ruleId })
65+
}
66+
67+
return t("tools:executeCommand.destructiveCommandGuard.blocked")
68+
}
69+
5470
export function resolveAgentTimeoutMs(timeoutSeconds: number | null | undefined): number {
5571
const requestedAgentTimeout = typeof timeoutSeconds === "number" && timeoutSeconds > 0 ? timeoutSeconds * 1000 : 0
5672

@@ -123,10 +139,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
123139
const dcgResult = await runDcg(binaryPath, canonicalCommand, workingDirectory)
124140
dcgBlocked = dcgResult.decision === "deny"
125141
if (dcgResult.decision === "deny") {
126-
await task.say(
127-
"text",
128-
`Destructive Command Guard blocked this command${dcgResult.reason ? `: ${dcgResult.reason}` : "."}${dcgResult.ruleId ? ` (Rule: ${dcgResult.ruleId})` : ""}`,
129-
)
142+
await task.say("error", formatDcgBlockedMessage(dcgResult.reason, dcgResult.ruleId))
130143
}
131144
}
132145

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

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

47+
const mockRunDcg = vitest.fn()
48+
const mockGetDcgBinaryPath = vitest.fn()
49+
50+
vitest.mock("../../../services/destructive-command-guard", () => ({
51+
runDcg: mockRunDcg,
52+
getDcgBinaryPath: mockGetDcgBinaryPath,
53+
}))
54+
4755
// Import the module
4856
import * as executeCommandModule from "../ExecuteCommandTool"
4957
const { executeCommandTool } = executeCommandModule
@@ -94,6 +102,8 @@ describe("executeCommandTool", () => {
94102
mockAskApproval = vitest.fn().mockResolvedValue(true)
95103
mockHandleError = vitest.fn().mockResolvedValue(undefined)
96104
mockPushToolResult = vitest.fn()
105+
mockRunDcg.mockResolvedValue({ decision: "allow" })
106+
mockGetDcgBinaryPath.mockReturnValue("/test/storage/dcg")
97107

98108
// Setup vscode config mock
99109
const mockConfig = {
@@ -197,6 +207,46 @@ describe("executeCommandTool", () => {
197207
})
198208

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

src/core/webview/webviewMessageHandler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,9 @@ export const webviewMessageHandler = async (
684684
} catch (error) {
685685
message.updatedSettings.destructiveCommandGuardEnabled = false
686686
vscode.window.showErrorMessage(
687-
`Unable to enable Destructive Command Guard: ${error instanceof Error ? error.message : String(error)}`,
687+
t("common:errors.destructive_command_guard_enable_failed", {
688+
error: error instanceof Error ? error.message : String(error),
689+
}),
688690
)
689691
}
690692
}

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
@@ -11,6 +11,14 @@
1111
"codebaseSearch": {
1212
"approval": "Searching for '{{query}}' in codebase..."
1313
},
14+
"executeCommand": {
15+
"destructiveCommandGuard": {
16+
"blocked": "Destructive Command Guard blocked this command.",
17+
"blockedWithReason": "Destructive Command Guard blocked this command. Message from DCG: {{reason}}",
18+
"blockedWithRule": "Destructive Command Guard blocked this command. (Rule: {{ruleId}})",
19+
"blockedWithReasonAndRule": "Destructive Command Guard blocked this command. Message from DCG: {{reason}} (Rule: {{ruleId}})"
20+
}
21+
},
1422
"newTask": {
1523
"errors": {
1624
"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)