mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +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,
|
t?: (key: TranslationKey, params?: Record<string, string | number>) => string,
|
||||||
) {
|
) {
|
||||||
const filePath = typeof obj.file_path === 'string' ? obj.file_path : 'file'
|
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') {
|
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') {
|
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') {
|
if (toolName === 'Bash' && typeof obj.command === 'string') {
|
||||||
return (
|
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)]">
|
<TerminalChrome title={typeof obj.description === 'string' ? obj.description : filePath}>
|
||||||
<span className="text-[var(--color-terminal-accent)]">$</span> {obj.command}
|
<div className="px-3 py-2.5 font-[var(--font-mono)] text-[11px] leading-[1.3] text-[var(--color-terminal-fg)]">
|
||||||
</div>
|
<span className="text-[var(--color-terminal-accent)]">$</span> {obj.command}
|
||||||
</TerminalChrome>
|
</div>
|
||||||
|
</TerminalChrome>
|
||||||
|
{resultOutput}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolName === 'Read') {
|
if (toolName === 'Read') {
|
||||||
return null
|
return resultOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (resultOutput) return resultOutput
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
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(
|
function renderDetails(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
obj: Record<string, unknown>,
|
obj: Record<string, unknown>,
|
||||||
|
|||||||
@ -159,6 +159,84 @@ describe('chat blocks', () => {
|
|||||||
expect(container.textContent).toContain('fatal: unrecognized argument: --no-stat')
|
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', () => {
|
it('expands tool errors so full Computer Use gate messages are readable', () => {
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ToolCallBlock
|
<ToolCallBlock
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user