From db631dfdfd063bd4efbfbfc48874d40c90672514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 9 Jun 2026 21:07:32 +0800 Subject: [PATCH] fix(desktop): show full tool error details (#625) Surface expanded error output for tool cards whose previews previously returned before rendering the result body. Keep successful Bash/Read/Edit/Write outputs hidden as before while exposing error details with wrapping and copy support. Tested: cd desktop && bun run test -- src/components/chat/chatBlocks.test.tsx Tested: bun test src/server/__tests__/conversations.test.ts -t "should switch from bypass permissions back to default without restarting" --timeout=20000 Tested: bun run verify Confidence: high Scope-risk: narrow --- desktop/src/components/chat/ToolCallBlock.tsx | 101 ++++++++++++------ .../src/components/chat/chatBlocks.test.tsx | 78 ++++++++++++++ 2 files changed, 147 insertions(+), 32 deletions(-) diff --git a/desktop/src/components/chat/ToolCallBlock.tsx b/desktop/src/components/chat/ToolCallBlock.tsx index 794fae62..9f17da6d 100644 --- a/desktop/src/components/chat/ToolCallBlock.tsx +++ b/desktop/src/components/chat/ToolCallBlock.tsx @@ -131,57 +131,94 @@ function renderPreview( t?: (key: TranslationKey, params?: Record) => string, ) { const filePath = typeof obj.file_path === 'string' ? obj.file_path : 'file' + const resultText = getVisibleResultText(toolName, result) + const resultOutput = result && resultText ? renderResultOutput(result, resultText, t) : null if (toolName === 'Edit' && typeof obj.old_string === 'string' && typeof obj.new_string === 'string') { - return + return ( + <> + + {resultOutput} + + ) } if (toolName === 'Write' && typeof obj.content === 'string') { - return + return ( + <> + + {resultOutput} + + ) } if (toolName === 'Bash' && typeof obj.command === 'string') { return ( - -
- $ {obj.command} -
-
+ <> + +
+ $ {obj.command} +
+
+ {resultOutput} + ) } if (toolName === 'Read') { - return null + return resultOutput } - if (result) { - const text = extractTextContent(result.content) - if (text) { - return ( - <> - -
-
- {result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'} - -
- -
- - ) - } - } + if (resultOutput) return resultOutput return null } +function getVisibleResultText( + toolName: string, + result?: { content: unknown; isError: boolean } | null, +): string | null { + if (!result) return null + const text = extractTextContent(result.content) + if (!text) return null + + if (result.isError) return text + if (toolName === 'Bash' || toolName === 'Read' || toolName === 'Edit' || toolName === 'Write') return null + return text +} + +function renderResultOutput( + result: { content: unknown; isError: boolean }, + text: string, + t?: (key: TranslationKey, params?: Record) => string, +) { + return ( + <> + +
+
+ {result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'} + +
+ {result.isError ? ( +
+            {text}
+          
+ ) : ( + + )} +
+ + ) +} + function renderDetails( toolName: string, obj: Record, diff --git a/desktop/src/components/chat/chatBlocks.test.tsx b/desktop/src/components/chat/chatBlocks.test.tsx index 2b2e0c0d..ad7d05be 100644 --- a/desktop/src/components/chat/chatBlocks.test.tsx +++ b/desktop/src/components/chat/chatBlocks.test.tsx @@ -159,6 +159,84 @@ describe('chat blocks', () => { expect(container.textContent).toContain('fatal: unrecognized argument: --no-stat') }) + it('shows full bash error output when the tool block is expanded', () => { + const lines = Array.from({ length: 8 }, (_, index) => `detail line ${index + 1}`) + const fullError = [ + 'InputValidationError: Bash failed due to the following issues: The required parameter `description` is missing.', + ...lines, + 'final remediation hint: provide a concise command description.', + ].join('\n') + const { container } = render( + , + ) + + expect(container.textContent).toContain('InputValidationError') + expect(container.textContent).not.toContain('final remediation hint') + + fireEvent.click(screen.getByRole('button')) + + expect(container.textContent).toContain('Error Output') + expect(container.textContent).toContain('detail line 8') + expect(container.textContent).toContain('final remediation hint') + }) + + it('shows read tool validation errors when the tool block is expanded', () => { + const fullError = [ + 'InputValidationError: Read failed due to the following issues:', + 'The required parameter `file_path` is missing.', + 'The provided limit must be greater than 0.', + ].join('\n') + const { container } = render( + , + ) + + expect(container.textContent).toContain('Read') + expect(container.textContent).not.toContain('file_path') + + fireEvent.click(screen.getByRole('button')) + + expect(container.textContent).toContain('Error Output') + expect(container.textContent).toContain('file_path') + expect(container.textContent).toContain('limit must be greater than 0') + }) + + it('keeps edit previews while showing edit tool error output when expanded', () => { + const { container } = render( + , + ) + + expect(container.textContent).toContain('Edit') + expect(container.textContent).not.toContain('old_string was not found') + + fireEvent.click(screen.getByRole('button')) + + expect(container.textContent).toContain('example.ts') + expect(container.textContent).toContain('Error Output') + expect(container.textContent).toContain('old_string was not found') + }) + it('expands tool errors so full Computer Use gate messages are readable', () => { const { container } = render(