fix: streamline tool failure feedback
This commit is contained in:
@@ -1276,6 +1276,58 @@ describe('App', () => {
|
|||||||
expect(screen.getByText('项目:默认项目')).toHaveClass('scope-badge')
|
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 () => {
|
it('submits knowledge scope without eager search or prompt injection and merges runtime references', async () => {
|
||||||
const libraryId = '11111111-1111-4111-8111-111111111111'
|
const libraryId = '11111111-1111-4111-8111-111111111111'
|
||||||
vi.mocked(api.knowledge.getSnapshot).mockResolvedValueOnce({
|
vi.mocked(api.knowledge.getSnapshot).mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -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 =
|
type MessageBlockRenderItem =
|
||||||
| {
|
| {
|
||||||
kind: 'block'
|
kind: 'block'
|
||||||
@@ -2687,6 +2701,12 @@ function App(): React.JSX.Element {
|
|||||||
status: terminalStatus
|
status: terminalStatus
|
||||||
})
|
})
|
||||||
updateMessage(run.conversationId, run.messageId, (message) => {
|
updateMessage(run.conversationId, run.messageId, (message) => {
|
||||||
|
const representedToolError =
|
||||||
|
event.type === 'error' &&
|
||||||
|
isErrorRepresentedByFailedTool(
|
||||||
|
message.tools,
|
||||||
|
event.message
|
||||||
|
)
|
||||||
const toolTerminalState =
|
const toolTerminalState =
|
||||||
event.type === 'error'
|
event.type === 'error'
|
||||||
? event.status === 'cancelled'
|
? event.status === 'cancelled'
|
||||||
@@ -2694,13 +2714,18 @@ function App(): React.JSX.Element {
|
|||||||
: ('failed' as const)
|
: ('failed' as const)
|
||||||
: undefined
|
: undefined
|
||||||
const fallbackError =
|
const fallbackError =
|
||||||
event.type === 'error' && !message.content
|
event.type === 'error' &&
|
||||||
|
!representedToolError &&
|
||||||
|
!message.content
|
||||||
? event.message.slice(0, maxMessageContentLength)
|
? event.message.slice(0, maxMessageContentLength)
|
||||||
: ''
|
: ''
|
||||||
return {
|
return {
|
||||||
...message,
|
...message,
|
||||||
state: event.type === 'error' ? 'error' : 'complete',
|
state: event.type === 'error' ? 'error' : 'complete',
|
||||||
status: event.type === 'error' ? event.message : undefined,
|
status:
|
||||||
|
event.type === 'error' && !representedToolError
|
||||||
|
? event.message
|
||||||
|
: undefined,
|
||||||
approval: undefined,
|
approval: undefined,
|
||||||
question: undefined,
|
question: undefined,
|
||||||
tools: toolTerminalState
|
tools: toolTerminalState
|
||||||
@@ -5594,7 +5619,9 @@ function App(): React.JSX.Element {
|
|||||||
{message.status}
|
{message.status}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.state === 'error' && (
|
{message.state === 'error' &&
|
||||||
|
messageIndex ===
|
||||||
|
activeConversation.messages.length - 1 && (
|
||||||
<button
|
<button
|
||||||
className="message-retry"
|
className="message-retry"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -5609,7 +5636,7 @@ function App(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
{t('chat.retry')}
|
{t('chat.retry')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user