Skip to content

Commit 4b98fc0

Browse files
author
Zoo (VP)
committed
feat(error-interception): user-friendly error UI with structured detail view
- Change MessageTransformer output from raw JSON to <error_details> block - Add CATEGORY_TITLES mapping for all 11 error categories - Add helper functions: getCategoryTitle, getErrorTitleFromGuided, formatErrorDetails - Update all 6 cline.say('error') paths in presentAssistantMessage to use user-friendly titles - Update ToolErrorInterceptor circuit breaker to use <error_details> format - Update all test assertions from JSON.parse to string-content checks - All 174 tests pass across 8 test files
1 parent 1b5f9dc commit 4b98fc0

11 files changed

Lines changed: 553 additions & 194 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Code Light Task Report
2+
3+
## Task Summary
4+
5+
Rebuilt VSIX package and installed it after the AI guidance UI fix.
6+
7+
## Actions Taken
8+
9+
1. Ran `pnpm bundle --production` in `src/` — esbuild completed successfully (1932 files bundled).
10+
2. Ran `npx vsce package --no-dependencies --out ../bin` — packaged `zoo-code-3.72.0.vsix`.
11+
3. Installed via `code --install-extension bin\zoo-code-3.72.0.vsix --force` — extension successfully installed.
12+
13+
## Result
14+
15+
**Success** — VSIX built and installed without errors.
16+
17+
## VSIX Details
18+
19+
- **Path**: `bin/zoo-code-3.72.0.vsix`
20+
- **Size**: 34,761,246 bytes (~33.15 MB)
21+
- **Version**: 3.72.0
22+
- **File count**: 1932 files
23+
24+
## Build Output Highlights
25+
26+
- 924 asset files (2.32 MB)
27+
- 175 dist files (97.69 MB uncompressed)
28+
- 801 webview-ui files (56.5 MB uncompressed)
29+
- 126 locale files
30+
- 36 tree-sitter language WASMs
31+
- 4 esbuild-wasm files
32+
- Note: `url.parse()` deprecation warning from Node.js v24 (non-blocking, cosmetic only)
33+
34+
## Issues Discovered
35+
36+
- Engine warning: wanted Node `20.20.2`, current `v24.16.0`. Build succeeds but is unsupported per `package.json` engine constraint.
37+
38+
## Next Step Recommendations
39+
40+
- Reload VS Code window to activate the newly installed extension.
41+
- Verify the AI guidance UI fix is visible and functional.
42+
43+
## Affected File List
44+
45+
- `bin/zoo-code-3.72.0.vsix` (generated artifact)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Code Task Report
2+
3+
## Task Summary
4+
5+
Show AI guidance (WHAT/WHY/NEXT JSON) in Zoo Code UI alongside error messages for all 3 error interception paths in `presentAssistantMessage.ts`.
6+
7+
## Actions Taken
8+
9+
1. Read [`presentAssistantMessage.ts`](src/core/assistant-message/presentAssistantMessage.ts:559) to locate the 3 `cline.say("error", ...)` call sites.
10+
2. Applied surgical edits to all 3 error paths:
11+
- **Line ~559 (Missing nativeArgs)**: Now appends `\n\n--- AI Guidance ---\n${guided}` when `guided` is truthy.
12+
- **Line ~780 (Structural preflight)**: Same pattern, using `[${variant}]` prefix.
13+
- **Line ~859 (Validation catch)**: Same pattern, using `Validation error:` prefix.
14+
3. Ran the test suite: `cd src; npx vitest run core/assistant-message/__tests__/presentAssistantMessage-error-interception.spec.ts` — all 19 tests passed.
15+
4. Verified no test assertions reference the exact error message format (no updates needed).
16+
5. Committed: `fix(error-interception): show AI guidance (WHAT/WHY/NEXT) to user in UI`.
17+
6. Pushed to `myk1yt/feature/combined-all-features`.
18+
19+
## Result
20+
21+
✅ Success. All 3 error paths now show both the error category message AND the structured AI guidance (WHAT/WHY/NEXT) to the user in the UI chat. When `guided` is undefined (no interceptor output), the original error message is shown unchanged.
22+
23+
## Issues Discovered
24+
25+
None.
26+
27+
## Next Step Recommendations
28+
29+
- Consider adding UI tests that assert the `--- AI Guidance ---` separator appears in the rendered error message when guidance is present.
30+
31+
## Affected File List
32+
33+
- `src/core/assistant-message/presentAssistantMessage.ts`
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Code Mode Task Report
2+
3+
## Task Summary
4+
5+
Make all error interception patterns user-friendly with clickable detail view. Changed the error output format from raw JSON to a structured `<error_details>` block, and added user-friendly titles for each error category displayed in the chat UI via `cline.say("error", ...)`.
6+
7+
## Actions Taken
8+
9+
### 1. MessageTransformer.ts — Core format change
10+
11+
- Added `CATEGORY_TITLES` mapping: each `ErrorCategory` now has a concise, user-friendly title (e.g., `PARAM_TYPE_MISMATCH` → "Tool Call Format Error", `FILE_NOT_FOUND` → "File Not Found").
12+
- Added exported helpers: `getCategoryTitle()`, `extractCategoryFromGuided()`, `getErrorTitleFromGuided()`, `formatErrorDetails()`.
13+
- Replaced `serializePayload()` (JSON.stringify) with `formatPayloadAsDetails()` — produces a human-readable `<error_details>` block:
14+
```
15+
<error_details>
16+
Type: guided_tool_error
17+
Category: PARAM_TYPE_MISMATCH
18+
What: ...
19+
Why: ...
20+
Next:
21+
1. ...
22+
2. ...
23+
Retryable: true
24+
Pattern: EI/PARAM_TYPE_MISMATCH/002
25+
Occurrence: 1
26+
</error_details>
27+
```
28+
- Updated `fitPayloadWithinByteLimit()` → `fitDetailsWithinByteLimit()` with the same truncation strategy (reduce next items, then truncate why/what).
29+
30+
### 2. ToolErrorInterceptor.ts — Circuit breaker format
31+
32+
- Replaced `CIRCUIT_OPEN_MESSAGE` (plain object, JSON.stringify'd) with `CIRCUIT_OPEN_DETAILS` (pre-formatted `<error_details>` string via `formatErrorDetails()`).
33+
- Updated both circuit-open return paths to use `CIRCUIT_OPEN_DETAILS` directly.
34+
35+
### 3. presentAssistantMessage.ts — User-friendly titles in UI
36+
37+
- Added import of `getErrorTitleFromGuided` from error-interception module.
38+
- Updated all 6 `cline.say("error", ...)` paths to use the user-friendly title + `<error_details>` content:
39+
1. Missing tool_use.id (INVALID_TOOL_PROTOCOL)
40+
2. Missing nativeArgs / invalid JSON (PARAM_MISSING / INVALID_JSON_ARGUMENTS)
41+
3. Structural preflight — CWD_OBJECT_MISUSE / NESTED_PARAM_OVERFLOW
42+
4. Validation catch (type mismatch, mode restriction, etc.)
43+
5. Custom tool arg validation failure
44+
6. Unknown tool error
45+
46+
### 4. index.ts — New exports
47+
48+
- Exported `extractCategoryFromGuided`, `formatErrorDetails`, `getCategoryTitle`, `getErrorTitleFromGuided`.
49+
50+
### 5. Test updates
51+
52+
- **MessageTransformer.spec.ts**: Rewrote all assertions from `JSON.parse()` + field checks to string-content checks (`toContain`). Added new test suite for category title helpers.
53+
- **ToolErrorInterceptor.spec.ts**: Replaced all 13 `JSON.parse()` assertions with string-content checks.
54+
- **presentAssistantMessage-error-interception.spec.ts**: Updated CONTEXT_OVERFLOW test from JSON.parse to string checks. Updated CWD_OBJECT_MISUSE and NESTED_PARAM_OVERFLOW assertions to check for `PARAM_TYPE_MISMATCH` category + pattern ID instead of variant names.
55+
- **presentAssistantMessage-unknown-tool.spec.ts**: Updated `say("error")` assertion to check for guided title instead of raw i18n string.
56+
57+
## Result
58+
59+
✅ Success — All 174 tests pass across 8 test files (0 failures).
60+
61+
## Issues Discovered
62+
63+
None. The format change was clean and all existing test assertions were updated to match the new `<error_details>` format.
64+
65+
## Next Step Recommendations
66+
67+
- Consider adding integration tests that verify the `<error_details>` format is parseable by the AI model in actual conversation flows.
68+
- The `isErrorResult()` method in ToolErrorInterceptor already checks for `<error_details>` prefix (line 297), so the new format is compatible with the existing error detection logic.
69+
70+
## Affected File List
71+
72+
- `src/core/tools/error-interception/MessageTransformer.ts` (modified — core format change)
73+
- `src/core/tools/error-interception/ToolErrorInterceptor.ts` (modified — circuit breaker format)
74+
- `src/core/tools/error-interception/index.ts` (modified — new exports)
75+
- `src/core/assistant-message/presentAssistantMessage.ts` (modified — user-friendly titles in 6 error paths)
76+
- `src/core/tools/error-interception/__tests__/MessageTransformer.spec.ts` (modified — test assertions)
77+
- `src/core/tools/error-interception/__tests__/ToolErrorInterceptor.spec.ts` (modified — test assertions)
78+
- `src/core/assistant-message/__tests__/presentAssistantMessage-error-interception.spec.ts` (modified — test assertions)
79+
- `src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts` (modified — test assertion)

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

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,8 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
255255
// User must see the error in the UI (design principle: both must happen).
256256
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
257257
expect(errorSayCalls.length).toBe(1)
258-
expect(errorSayCalls[0][1]).toContain("CWD_OBJECT_MISUSE")
258+
expect(errorSayCalls[0][1]).toContain("PARAM_TYPE_MISMATCH")
259+
expect(errorSayCalls[0][1]).toContain("EI/PARAM_TYPE_MISMATCH/002")
259260
})
260261

261262
it("blocks tool_use with NESTED_PARAM_OVERFLOW and pushes guided tool_result", async () => {
@@ -293,7 +294,8 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
293294
// User must see the error in the UI (design principle: both must happen).
294295
const errorSayCalls = mockTask.say.mock.calls.filter((c: any[]) => c[0] === "error")
295296
expect(errorSayCalls.length).toBe(1)
296-
expect(errorSayCalls[0][1]).toContain("NESTED_PARAM_OVERFLOW")
297+
expect(errorSayCalls[0][1]).toContain("PARAM_TYPE_MISMATCH")
298+
expect(errorSayCalls[0][1]).toContain("EI/PARAM_TYPE_MISMATCH/003")
297299
})
298300

299301
it("escalates to STRUCTURAL_MISUSE_REPEAT on second identical misuse", async () => {
@@ -496,7 +498,7 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
496498
)
497499
})
498500
})
499-
501+
500502
describe("validation error classification (validateToolUse catch)", () => {
501503
it("classifies 'not allowed in' as modeRestriction and pushes tool_result", async () => {
502504
const { validateToolUse } = await import("../../tools/validateToolUse")
@@ -594,7 +596,7 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
594596
expect(errorSayCalls[0][1]).toContain("File restriction")
595597
})
596598
})
597-
599+
598600
describe("tool repetition detection", () => {
599601
it("pushes guided error and asks user when repetition is detected", async () => {
600602
const toolCallId = "call_repetition"
@@ -608,7 +610,7 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
608610
partial: false,
609611
},
610612
]
611-
613+
612614
// Mock repetition detector to block execution
613615
mockTask.toolRepetitionDetector.check.mockReturnValue({
614616
allowExecution: false,
@@ -617,22 +619,19 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
617619
messageDetail: "Tool {toolName} has been repeated too many times.",
618620
},
619621
})
620-
622+
621623
// Mock user response
622624
mockTask.ask.mockResolvedValue({ response: "yesButtonClicked" })
623-
625+
624626
await presentAssistantMessage(mockTask)
625-
627+
626628
// Should have called ask for user confirmation
627-
expect(mockTask.ask).toHaveBeenCalledWith(
628-
"tool_repetition_limit",
629-
expect.stringContaining("read_file"),
630-
)
629+
expect(mockTask.ask).toHaveBeenCalledWith("tool_repetition_limit", expect.stringContaining("read_file"))
631630
// Should NOT have set didAlreadyUseTool (the tool was blocked)
632631
expect(mockTask.didAlreadyUseTool).toBe(false)
633632
})
634633
})
635-
634+
636635
describe("unknown tool handling", () => {
637636
it("pushes guided tool_result for unknown tool and does not set didAlreadyUseTool", async () => {
638637
const toolCallId = "call_unknown_tool_handler"
@@ -646,9 +645,9 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
646645
partial: false,
647646
},
648647
]
649-
648+
650649
await presentAssistantMessage(mockTask)
651-
650+
652651
const toolResult = mockTask.userMessageContent.find(
653652
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
654653
)
@@ -661,17 +660,17 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
661660
)
662661
expect(mockTask.didAlreadyUseTool).toBe(false)
663662
})
664-
663+
665664
describe("INVALID_JSON_ARGUMENTS - concatenated JSON tool call", () => {
666665
it("produces guided tool_result with INVALID_JSON_ARGUMENTS when parse error is recorded", async () => {
667666
const toolCallId = "call_concat_json_001"
668-
667+
669668
// Simulate NativeToolCallParser having recorded a JSON.parse
670669
// failure for this tool call (e.g. concatenated JSON objects).
671670
vi.mocked(NativeToolCallParser.consumeParseError).mockReturnValue(
672671
"Unexpected non-whitespace character after JSON at position 42",
673672
)
674-
673+
675674
mockTask.assistantMessageContent = [
676675
{
677676
type: "tool_use",
@@ -682,9 +681,9 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
682681
partial: false,
683682
},
684683
]
685-
684+
686685
await presentAssistantMessage(mockTask)
687-
686+
688687
const toolResult = mockTask.userMessageContent.find(
689688
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
690689
)
@@ -704,13 +703,13 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
704703
// Should NOT have set didAlreadyUseTool
705704
expect(mockTask.didAlreadyUseTool).toBe(false)
706705
})
707-
706+
708707
it("falls back to PARAM_MISSING when no parse error is recorded", async () => {
709708
const toolCallId = "call_missing_args_002"
710-
709+
711710
// No parse error recorded — simulate the original missing-args path
712711
vi.mocked(NativeToolCallParser.consumeParseError).mockReturnValue(undefined)
713-
712+
714713
mockTask.assistantMessageContent = [
715714
{
716715
type: "tool_use",
@@ -720,9 +719,9 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
720719
partial: false,
721720
},
722721
]
723-
722+
724723
await presentAssistantMessage(mockTask)
725-
724+
726725
const toolResult = mockTask.userMessageContent.find(
727726
(item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
728727
)
@@ -732,37 +731,35 @@ describe("presentAssistantMessage - Error Interception Integration", () => {
732731
expect(String(toolResult.content)).toContain("PARAM_MISSING")
733732
expect(String(toolResult.content)).not.toContain("INVALID_JSON_ARGUMENTS")
734733
})
735-
734+
736735
describe("CONTEXT_OVERFLOW guidance", () => {
737736
it("produces guided_runtime_error payload with correct guidance for context overflow", () => {
738737
// Directly test the interceptor's transformError path for a
739738
// context overflow signal, simulating an API request that was
740739
// rejected due to context window limits.
741740
const interceptor = new ToolErrorInterceptor()
742741
const task = {}
743-
742+
744743
const signal = {
745744
source: "api_request" as const,
746745
stage: "api" as const,
747746
taskId: "ei-task-id",
748747
metadata: { contextWindowExceeded: true },
749748
}
750-
749+
751750
const message = interceptor.transformError(task, signal)
752751
expect(message).toBeDefined()
753-
754-
const parsed = JSON.parse(message!)
755-
expect(parsed.version).toBe(1)
756-
expect(parsed.status).toBe("error")
757-
expect(parsed.type).toBe("guided_runtime_error")
758-
expect(parsed.category).toBe("CONTEXT_OVERFLOW")
759-
expect(parsed.retryable).toBe(true)
760-
expect(parsed.pattern_id).toBe("EI/CONTEXT_OVERFLOW/001")
761-
expect(parsed.what).toContain("context")
762-
expect(parsed.what).toContain("exceeded")
763-
expect(parsed.next.length).toBeGreaterThan(0)
764-
expect(parsed.next.some((n: string) => n.includes("summary"))).toBe(true)
765-
expect(parsed.next.some((n: string) => n.includes("Do not repeat"))).toBe(true)
752+
753+
expect(message).toContain("<error_details>")
754+
expect(message).toContain("</error_details>")
755+
expect(message).toContain("Type: guided_runtime_error")
756+
expect(message).toContain("Category: CONTEXT_OVERFLOW")
757+
expect(message).toContain("Retryable: true")
758+
expect(message).toContain("Pattern: EI/CONTEXT_OVERFLOW/001")
759+
expect(message.toLowerCase()).toContain("context")
760+
expect(message.toLowerCase()).toContain("exceeded")
761+
expect(message).toContain("summary")
762+
expect(message).toContain("Do not repeat")
766763
})
767764
})
768765
})

src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
106106
expect.stringContaining("Unknown tool"),
107107
)
108108

109-
// Verify error message was shown to user (uses i18n key)
110-
expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError")
109+
// Verify error message was shown to user with guided details
110+
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("Tool Call Format Error"))
111111
})
112112

113113
it("should fail fast when tool_use is missing id (legacy/XML-style tool call)", async () => {
@@ -127,9 +127,7 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
127127
// Should not execute tool; should surface a clear error message.
128128
const textBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "text")
129129
expect(textBlocks.length).toBeGreaterThan(0)
130-
expect(textBlocks.some((b: any) => String(b.text).includes("guided_tool_error"))).toBe(
131-
true,
132-
)
130+
expect(textBlocks.some((b: any) => String(b.text).includes("guided_tool_error"))).toBe(true)
133131

134132
// Verify consecutiveMistakeCount was incremented
135133
expect(mockTask.consecutiveMistakeCount).toBe(1)

0 commit comments

Comments
 (0)