Skip to content

Commit 83ed11f

Browse files
author
Zoo (VP)
committed
fix(error-interception): show errors to user in UI alongside AI guidance
Add cline.say('error') calls to 3 bypass paths in presentAssistantMessage.ts where intercepted errors were sent only to the AI model but never shown to the user in the Zoo Code chat UI: - Structural preflight path (CWD_OBJECT_MISUSE, NESTED_PARAM_OVERFLOW) - Validation catch path (modeRestriction, unknownTool, fileRestriction) - Missing nativeArgs path (PARAM_MISSING, INVALID_JSON_ARGUMENTS) This ensures both user visibility AND AI guidance happen simultaneously, matching the core design principle: errors must never be hidden from users.
1 parent 3761994 commit 83ed11f

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/core/assistant-message/__tests__/presentAssistantMessage-error-interception.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
252252
"execute_command",
253253
expect.stringContaining("CWD_OBJECT_MISUSE"),
254254
)
255+
// User must see the error in the UI (design principle: both must happen).
256+
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
257+
expect(errorSayCalls.length).toBe(1)
258+
expect(errorSayCalls[0][1]).toContain("CWD_OBJECT_MISUSE")
255259
})
256260

257261
it("blocks tool_use with NESTED_PARAM_OVERFLOW and pushes guided tool_result", async () => {
@@ -286,6 +290,10 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
286290
"read_file",
287291
expect.stringContaining("NESTED_PARAM_OVERFLOW"),
288292
)
293+
// User must see the error in the UI (design principle: both must happen).
294+
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
295+
expect(errorSayCalls.length).toBe(1)
296+
expect(errorSayCalls[0][1]).toContain("NESTED_PARAM_OVERFLOW")
289297
})
290298

291299
it("escalates to STRUCTURAL_MISUSE_REPEAT on second identical misuse", async () => {
@@ -402,6 +410,9 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
402410
expect.stringContaining("missing nativeArgs"),
403411
)
404412
expect(mockTask.didAlreadyUseTool).toBe(false)
413+
// User must see the error in the UI (design principle: both must happen).
414+
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
415+
expect(errorSayCalls.length).toBe(1)
405416
})
406417
})
407418

@@ -500,6 +511,7 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
500511
id: toolCallId,
501512
name: "read_file",
502513
params: { path: "x.txt" },
514+
nativeArgs: { path: "x.txt" },
503515
partial: false,
504516
},
505517
]
@@ -513,6 +525,10 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
513525
expect(toolResult.is_error).toBe(true)
514526
expect(mockTask.consecutiveMistakeCount).toBe(1)
515527
expect(mockTask.didAlreadyUseTool).toBe(false)
528+
// User must see the error in the UI (design principle: both must happen).
529+
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
530+
expect(errorSayCalls.length).toBe(1)
531+
expect(errorSayCalls[0][1]).toContain("not allowed in")
516532
})
517533

518534
it("classifies 'Unknown tool' as unknownTool and pushes tool_result", async () => {
@@ -528,6 +544,7 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
528544
id: toolCallId,
529545
name: "fake_tool_xyz",
530546
params: {},
547+
nativeArgs: {},
531548
partial: false,
532549
},
533550
]
@@ -540,6 +557,10 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
540557
expect(toolResult).toBeDefined()
541558
expect(toolResult.is_error).toBe(true)
542559
expect(mockTask.consecutiveMistakeCount).toBe(1)
560+
// User must see the error in the UI (design principle: both must happen).
561+
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
562+
expect(errorSayCalls.length).toBe(1)
563+
expect(errorSayCalls[0][1]).toContain("Unknown tool")
543564
})
544565

545566
it("classifies 'File restriction' as fileRestriction and pushes tool_result", async () => {
@@ -555,6 +576,7 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
555576
id: toolCallId,
556577
name: "write_to_file",
557578
params: { path: ".git/config", content: "bad" },
579+
nativeArgs: { path: ".git/config", content: "bad" },
558580
partial: false,
559581
},
560582
]
@@ -566,6 +588,10 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
566588
)
567589
expect(toolResult).toBeDefined()
568590
expect(toolResult.is_error).toBe(true)
591+
// User must see the error in the UI (design principle: both must happen).
592+
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
593+
expect(errorSayCalls.length).toBe(1)
594+
expect(errorSayCalls[0][1]).toContain("File restriction")
569595
})
570596
})
571597

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,10 @@ export async function presentAssistantMessage(cline: Task) {
553553
// continue gracefully.
554554
const missingArgsGuide = getTaskErrorState(cline).consumePendingNativeProtocolGuide()
555555
const missingArgsBase = guided ?? formatResponse.toolError(errorMessage)
556+
// Show the invalid/missing args error to the user in the UI chat.
557+
// The AI receives the guided payload below; the user must also
558+
// see what went wrong (design principle: both must happen).
559+
await cline.say("error", `Invalid tool call: ${errorMessage}`)
556560
cline.pushToolResultToUserContent({
557561
type: "tool_result",
558562
tool_use_id: sanitizeToolUseId(toolCallId),
@@ -770,6 +774,10 @@ export async function presentAssistantMessage(cline: Task) {
770774

771775
const structuralGuide = taskErrorState.consumePendingNativeProtocolGuide()
772776
const structuralBase = guided ?? formatResponse.toolError(errorMessage)
777+
// Show the structural error to the user in the UI chat.
778+
// The AI receives the guided payload below; the user must also
779+
// see what went wrong (design principle: both must happen).
780+
await cline.say("error", `[${variant}] ${errorMessage}`)
773781
cline.pushToolResultToUserContent({
774782
type: "tool_result",
775783
tool_use_id: sanitizeToolUseId(toolCallId),
@@ -845,6 +853,10 @@ export async function presentAssistantMessage(cline: Task) {
845853
// Push tool_result directly without setting didAlreadyUseTool
846854
const validationGuide = getTaskErrorState(cline).consumePendingNativeProtocolGuide()
847855
const validationBase = guided ? `${guided}\n\n${errorMessage}` : errorMessage
856+
// Show the validation error to the user in the UI chat.
857+
// The AI receives the guided payload below; the user must also
858+
// see what went wrong (design principle: both must happen).
859+
await cline.say("error", `Validation error: ${errorMessage}`)
848860
cline.pushToolResultToUserContent({
849861
type: "tool_result",
850862
tool_use_id: sanitizeToolUseId(toolCallId),

0 commit comments

Comments
 (0)