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 && (
- )}
+ )}
))}