From 8703165e0104956a6dace85baae9859ff0938ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 13 Aug 2026 23:12:08 +0800 Subject: [PATCH] fix(h5): show AskUserQuestion before tool stream completes #1220 Materialize the question card directly from a permission request when the streamed tool block has not arrived yet. Upsert by tool-use id so either event order produces one visible, answerable card without requiring a refresh. --- desktop/src/stores/chatStore.test.ts | 86 ++++++++++++++++++++++++++++ desktop/src/stores/chatStore.ts | 18 +++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index fcd83cf6..4f700377 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -4054,6 +4054,92 @@ describe('chatStore history mapping', () => { expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.runtimeConfigReadyCount).toBe(1) }) + it('shows AskUserQuestion when permission arrives before the streamed tool block', () => { + const input = { + questions: [ + { + question: 'Should we persist data?', + options: [{ label: 'No' }, { label: 'Yes' }], + }, + ], + } + useChatStore.setState({ + sessions: { [TEST_SESSION_ID]: makeSession() }, + }) + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'permission_request', + requestId: 'perm-ask-permission-first', + toolName: 'AskUserQuestion', + toolUseId: 'tool-ask-permission-first', + input, + }) + + expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toContainEqual( + expect.objectContaining({ + type: 'tool_use', + toolName: 'AskUserQuestion', + toolUseId: 'tool-ask-permission-first', + input, + }), + ) + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'tool_use_complete', + toolName: 'AskUserQuestion', + toolUseId: 'tool-ask-permission-first', + input, + }) + + expect( + useChatStore.getState().sessions[TEST_SESSION_ID]?.messages.filter( + (message) => message.type === 'tool_use' && + message.toolUseId === 'tool-ask-permission-first', + ), + ).toHaveLength(1) + }) + + it('keeps one AskUserQuestion when the streamed tool block arrives first', () => { + const input = { + questions: [ + { + question: 'Which scope?', + options: [{ label: 'Current file' }, { label: 'Repository' }], + }, + ], + } + useChatStore.setState({ + sessions: { [TEST_SESSION_ID]: makeSession() }, + }) + const store = useChatStore.getState() + + store.handleServerMessage(TEST_SESSION_ID, { + type: 'tool_use_complete', + toolName: 'AskUserQuestion', + toolUseId: 'tool-ask-stream-first', + input, + }) + store.handleServerMessage(TEST_SESSION_ID, { + type: 'permission_request', + requestId: 'perm-ask-stream-first', + toolName: 'AskUserQuestion', + toolUseId: 'tool-ask-stream-first', + input, + }) + + const session = useChatStore.getState().sessions[TEST_SESSION_ID] + expect(session?.pendingPermission).toMatchObject({ + requestId: 'perm-ask-stream-first', + toolUseId: 'tool-ask-stream-first', + }) + expect( + session?.messages.filter( + (message) => message.type === 'tool_use' && + message.toolUseId === 'tool-ask-stream-first', + ), + ).toHaveLength(1) + }) + it('keeps AskUserQuestion permission requests out of the message list while tracking the pending request', () => { useChatStore.setState({ sessions: { diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 27167649..4f025285 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -3340,6 +3340,7 @@ export const useChatStore = create((set, get) => ({ } const hasPermissionMessage = s.messages.some((message) => message.type === 'permission_request' && message.requestId === msg.requestId) + const isAskUserQuestion = msg.toolName === 'AskUserQuestion' return { pendingPermission, @@ -3349,8 +3350,21 @@ export const useChatStore = create((set, get) => ({ apiRetry: null, streamingFallback: null, messages: - msg.toolName === 'AskUserQuestion' || hasPermissionMessage - ? s.messages + isAskUserQuestion && msg.toolUseId + ? upsertToolUseMessage(s.messages, msg.toolUseId, (existing) => ({ + id: existing?.id ?? nextId(), + type: 'tool_use', + toolName: msg.toolName, + toolUseId: msg.toolUseId!, + originalToolUseId: existing?.originalToolUseId, + input: msg.input, + timestamp: existing?.timestamp ?? Date.now(), + parentToolUseId: existing?.parentToolUseId, + isPending: false, + partialInput: existing?.partialInput, + })) + : isAskUserQuestion || hasPermissionMessage + ? s.messages : [...s.messages, { id: nextId(), type: 'permission_request',