fix: streamline tool failure feedback

This commit is contained in:
lofyer
2026-08-11 16:39:17 +08:00
parent fde18c1568
commit 19a4469561
2 changed files with 83 additions and 4 deletions
+52
View File
@@ -1276,6 +1276,58 @@ describe('App', () => {
expect(screen.getByText('项目:默认项目')).toHaveClass('scope-badge')
})
it('keeps a tool failure in details and hides retry after continuing', async () => {
render(<App />)
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
target: { value: '读取演示文稿' }
})
fireEvent.click(await screen.findByLabelText('发送'))
await waitFor(() => expect(run).toHaveBeenCalledOnce())
const request = run.mock.calls[0]?.[0]
if (!request) {
throw new Error('Missing request')
}
const toolError =
'Cannot read binary file: D:\\workspace\\presentation.pptx'
const runtimeError = `OpenCode 工具执行失败(call-1):${toolError}`
act(() => {
agentListener?.({
requestId: request.requestId,
type: 'tool',
callId: 'call-1',
name: 'read',
state: 'failed',
summary: 'OpenCode 工具:read',
input: '{"path":"D:\\\\workspace\\\\presentation.pptx"}',
error: toolError
})
agentListener?.({
requestId: request.requestId,
type: 'error',
status: 'failed',
message: runtimeError
})
})
expect(screen.getByText(toolError)).toBeInTheDocument()
expect(screen.queryByText(runtimeError)).not.toBeInTheDocument()
expect(
screen.getByRole('button', { name: '重新编辑并发送' })
).toBeInTheDocument()
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
target: { value: '继续处理' }
})
fireEvent.click(screen.getByLabelText('发送'))
await waitFor(() => expect(run).toHaveBeenCalledTimes(2))
expect(
screen.queryByRole('button', { name: '重新编辑并发送' })
).not.toBeInTheDocument()
})
it('submits knowledge scope without eager search or prompt injection and merges runtime references', async () => {
const libraryId = '11111111-1111-4111-8111-111111111111'
vi.mocked(api.knowledge.getSnapshot).mockResolvedValueOnce({
+31 -4
View File
@@ -454,6 +454,20 @@ function terminalizeMessageToolBlocks(
)
}
function isErrorRepresentedByFailedTool(
tools: ToolActivity[] | undefined,
errorMessage: string
): boolean {
return Boolean(
tools?.some(
(tool) =>
tool.state === 'failed' &&
((tool.error && errorMessage.includes(tool.error)) ||
(tool.callId && errorMessage.includes(tool.callId)))
)
)
}
type MessageBlockRenderItem =
| {
kind: 'block'
@@ -2687,6 +2701,12 @@ function App(): React.JSX.Element {
status: terminalStatus
})
updateMessage(run.conversationId, run.messageId, (message) => {
const representedToolError =
event.type === 'error' &&
isErrorRepresentedByFailedTool(
message.tools,
event.message
)
const toolTerminalState =
event.type === 'error'
? event.status === 'cancelled'
@@ -2694,13 +2714,18 @@ function App(): React.JSX.Element {
: ('failed' as const)
: undefined
const fallbackError =
event.type === 'error' && !message.content
event.type === 'error' &&
!representedToolError &&
!message.content
? event.message.slice(0, maxMessageContentLength)
: ''
return {
...message,
state: event.type === 'error' ? 'error' : 'complete',
status: event.type === 'error' ? event.message : undefined,
status:
event.type === 'error' && !representedToolError
? event.message
: undefined,
approval: undefined,
question: undefined,
tools: toolTerminalState
@@ -5594,7 +5619,9 @@ function App(): React.JSX.Element {
{message.status}
</div>
)}
{message.state === 'error' && (
{message.state === 'error' &&
messageIndex ===
activeConversation.messages.length - 1 && (
<button
className="message-retry"
onClick={() => {
@@ -5609,7 +5636,7 @@ function App(): React.JSX.Element {
>
{t('chat.retry')}
</button>
)}
)}
</div>
</article>
))}