From 19a446956125af89b4c6b8e69884b8e4e0221a98 Mon Sep 17 00:00:00 2001 From: lofyer Date: Tue, 11 Aug 2026 16:39:17 +0800 Subject: [PATCH] fix: streamline tool failure feedback --- src/renderer/src/App.test.tsx | 52 +++++++++++++++++++++++++++++++++++ src/renderer/src/App.tsx | 35 ++++++++++++++++++++--- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index c32fc0c..15feb27 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -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() + + 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({ diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c1b9447..885f832 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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} )} - {message.state === 'error' && ( + {message.state === 'error' && + messageIndex === + activeConversation.messages.length - 1 && ( - )} + )} ))}