mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
This commit is contained in:
parent
385b996736
commit
db631dfdfd
@ -131,57 +131,94 @@ function renderPreview(
|
||||
t?: (key: TranslationKey, params?: Record<string, string | number>) => 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 <DiffViewer filePath={filePath} oldString={obj.old_string} newString={obj.new_string} />
|
||||
return (
|
||||
<>
|
||||
<DiffViewer filePath={filePath} oldString={obj.old_string} newString={obj.new_string} />
|
||||
{resultOutput}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (toolName === 'Write' && typeof obj.content === 'string') {
|
||||
return <DiffViewer filePath={filePath} oldString="" newString={obj.content} />
|
||||
return (
|
||||
<>
|
||||
<DiffViewer filePath={filePath} oldString="" newString={obj.content} />
|
||||
{resultOutput}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (toolName === 'Bash' && typeof obj.command === 'string') {
|
||||
return (
|
||||
<TerminalChrome title={typeof obj.description === 'string' ? obj.description : filePath}>
|
||||
<div className="px-3 py-2.5 font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-terminal-fg)]">
|
||||
<span className="text-[var(--color-terminal-accent)]">$</span> {obj.command}
|
||||
</div>
|
||||
</TerminalChrome>
|
||||
<>
|
||||
<TerminalChrome title={typeof obj.description === 'string' ? obj.description : filePath}>
|
||||
<div className="px-3 py-2.5 font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-terminal-fg)]">
|
||||
<span className="text-[var(--color-terminal-accent)]">$</span> {obj.command}
|
||||
</div>
|
||||
</TerminalChrome>
|
||||
{resultOutput}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (toolName === 'Read') {
|
||||
return null
|
||||
return resultOutput
|
||||
}
|
||||
|
||||
if (result) {
|
||||
const text = extractTextContent(result.content)
|
||||
if (text) {
|
||||
return (
|
||||
<>
|
||||
<InlineImageGallery text={text} />
|
||||
<div className={`overflow-hidden rounded-lg border ${
|
||||
result.isError
|
||||
? 'border-[var(--color-error)]/20 bg-[var(--color-error-container)]/60'
|
||||
: 'border-[var(--color-border)] bg-[var(--color-surface)]'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-border)]/60 px-3 py-2 text-[10px] uppercase tracking-[0.18em] text-[var(--color-outline)]">
|
||||
<span>{result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'}</span>
|
||||
<CopyButton
|
||||
text={text}
|
||||
className="rounded-md border border-[var(--color-border)] px-2 py-1 text-[10px] normal-case tracking-normal text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
<CodeViewer code={text} language="plaintext" maxLines={18} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
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, string | number>) => string,
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<InlineImageGallery text={text} />
|
||||
<div className={`overflow-hidden rounded-lg border ${
|
||||
result.isError
|
||||
? 'border-[var(--color-error)]/20 bg-[var(--color-error-container)]/60'
|
||||
: 'border-[var(--color-border)] bg-[var(--color-surface)]'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between border-b border-[var(--color-border)]/60 px-3 py-2 text-[10px] uppercase tracking-[0.18em] text-[var(--color-outline)]">
|
||||
<span>{result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'}</span>
|
||||
<CopyButton
|
||||
text={text}
|
||||
className="rounded-md border border-[var(--color-border)] px-2 py-1 text-[10px] normal-case tracking-normal text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
{result.isError ? (
|
||||
<pre className="max-h-[420px] overflow-auto whitespace-pre-wrap break-words bg-[var(--color-code-bg)] px-3 py-2 font-[var(--font-mono)] text-[12px] leading-[1.45] text-[var(--color-error)]">
|
||||
{text}
|
||||
</pre>
|
||||
) : (
|
||||
<CodeViewer code={text} language="plaintext" maxLines={18} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function renderDetails(
|
||||
toolName: string,
|
||||
obj: Record<string, unknown>,
|
||||
|
||||
@ -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 = [
|
||||
'<tool_use_error>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(
|
||||
<ToolCallBlock
|
||||
toolName="Bash"
|
||||
input={{ command: 'bun run check:server' }}
|
||||
result={{ content: fullError, isError: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
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 = [
|
||||
'<tool_use_error>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(
|
||||
<ToolCallBlock
|
||||
toolName="Read"
|
||||
input={{}}
|
||||
result={{ content: fullError, isError: true }}
|
||||
/>,
|
||||
)
|
||||
|
||||
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(
|
||||
<ToolCallBlock
|
||||
toolName="Edit"
|
||||
input={{
|
||||
file_path: '/tmp/example.ts',
|
||||
old_string: 'const enabled = false',
|
||||
new_string: 'const enabled = true',
|
||||
}}
|
||||
result={{
|
||||
content: [
|
||||
'InputValidationError: Edit failed due to the following issues:',
|
||||
'The provided old_string was not found in the file.',
|
||||
].join('\n'),
|
||||
isError: true,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
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(
|
||||
<ToolCallBlock
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user