From 11f83a76c5d96c2876119570919a879a65590b21 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: Mon, 18 May 2026 23:37:28 +0800 Subject: [PATCH] Prevent stale worktree startup labels in chat The desktop status store retained the last non-default server verb until idle, so a one-time worktree startup label could leak into later thinking or execution bubbles. The fix treats default Thinking and verb-less status updates as a reset for transient status text while preserving explicit non-default labels. Constraint: Server-side duplicate worktree startup guards are still valid and should remain the source of launch truth. Rejected: Suppress the label in StreamingIndicator | stale state would still leak to any future consumer of statusVerb. Confidence: high Scope-risk: narrow Directive: Keep statusVerb transient; default Thinking or missing verbs must not preserve prior special labels. Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts --run Tested: cd desktop && bun run lint Tested: bun test src/server/__tests__/conversations.test.ts -t worktree --timeout 30000 Tested: git diff --check --- desktop/src/stores/chatStore.test.ts | 35 ++++++++++++++++++++++++++++ desktop/src/stores/chatStore.ts | 8 +++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index cbddbe71..b9c401bf 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -2230,6 +2230,41 @@ describe('chatStore history mapping', () => { vi.useRealTimers() }) + it('clears transient worktree startup text when normal thinking resumes', () => { + useChatStore.setState({ + sessions: { + [TEST_SESSION_ID]: makeSession({ + chatState: 'idle', + }), + }, + }) + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'status', + state: 'thinking', + verb: 'Creating worktree', + }) + expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.statusVerb).toBe('Creating worktree') + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'status', + state: 'thinking', + verb: 'Thinking', + }) + expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.statusVerb).toBe('') + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'status', + state: 'thinking', + verb: 'Creating worktree', + }) + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'status', + state: 'thinking', + }) + expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.statusVerb).toBe('') + }) + it('sends a desktop notification when the agent finishes a markdown reply', () => { vi.useFakeTimers() diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index a5a30765..600f8fed 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -799,9 +799,13 @@ export const useChatStore = create((set, get) => ({ const shouldFlush = hasPendingStreamText && msg.state === 'idle' return { chatState: preserveStreamingTurn ? 'streaming' : msg.state, - ...(msg.verb && msg.verb !== 'Thinking' ? { statusVerb: msg.verb } : {}), + statusVerb: msg.state === 'idle' + ? '' + : msg.verb && msg.verb !== 'Thinking' + ? msg.verb + : '', ...(msg.tokens ? { tokenUsage: { ...session.tokenUsage, output_tokens: msg.tokens } } : {}), - ...(msg.state === 'idle' ? { activeThinkingId: null, statusVerb: '' } : {}), + ...(msg.state === 'idle' ? { activeThinkingId: null } : {}), ...(shouldFlush ? { messages: appendAssistantTextMessage(session.messages, pendingText, Date.now()), streamingText: '',