From bd91186e2f1d84faac9d3e95798d792853da0204 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Sat, 11 Apr 2026 07:20:18 -0500 Subject: [PATCH] fix(web): drop provisional pre-tool question text --- .../web-live-interaction-contract.test.ts | 42 +++++++++++++++++++ web/lib/gsd-workspace-store.tsx | 19 +++------ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/tests/integration/web-live-interaction-contract.test.ts b/src/tests/integration/web-live-interaction-contract.test.ts index 5e288b69f..ce473ff40 100644 --- a/src/tests/integration/web-live-interaction-contract.test.ts +++ b/src/tests/integration/web-live-interaction-contract.test.ts @@ -358,6 +358,7 @@ function routeEvent(state: MinimalLiveState, event: any): MinimalLiveState { } case "tool_execution_start": { s.activeToolExecution = { id: event.toolCallId, name: event.toolName }; + s.streamingAssistantText = ""; break; } case "tool_execution_end": { @@ -802,6 +803,7 @@ test("(g-2) tool_execution_start/end update activeToolExecution", async () => { assert.ok(state.activeToolExecution); assert.equal(state.activeToolExecution.id, "tc-1"); assert.equal(state.activeToolExecution.name, "bash"); + assert.equal(state.streamingAssistantText, ""); state = routeEvent(state, { type: "tool_execution_end", @@ -813,6 +815,46 @@ test("(g-2) tool_execution_start/end update activeToolExecution", async () => { assert.equal(state.activeToolExecution, null); }); +test("(g-3) tool_execution_start clears provisional streaming text so only post-tool final text survives", async () => { + let state = createMinimalLiveState(); + + state = routeEvent(state, { + type: "message_update", + assistantMessageEvent: { + type: "text_delta", + delta: "It seems the questions were presented to the user. Let me wait for them to answer.", + }, + }); + assert.equal(state.streamingAssistantText, "It seems the questions were presented to the user. Let me wait for them to answer."); + + state = routeEvent(state, { + type: "tool_execution_start", + toolCallId: "tc-ask-1", + toolName: "ask_user_questions", + }); + assert.equal(state.streamingAssistantText, ""); + + state = routeEvent(state, { + type: "tool_execution_end", + toolCallId: "tc-ask-1", + toolName: "ask_user_questions", + result: {}, + isError: false, + }); + state = routeEvent(state, { + type: "message_update", + assistantMessageEvent: { + type: "text_delta", + delta: "What are you working on? Once you answer I'll tailor my approach accordingly.", + }, + }); + state = routeEvent(state, { type: "turn_end" }); + + assert.deepEqual(state.liveTranscript, [ + "What are you working on? Once you answer I'll tailor my approach accordingly.", + ]); +}); + test("(h) steer and abort commands post the correct RPC command type", async (t) => { const fixture = makeWorkspaceFixture(); const sessionPath = createSessionFile(fixture.projectCwd, fixture.sessionsDir, "sess-steer", "Steer Session"); diff --git a/web/lib/gsd-workspace-store.tsx b/web/lib/gsd-workspace-store.tsx index de80f47bd..adee496d6 100644 --- a/web/lib/gsd-workspace-store.tsx +++ b/web/lib/gsd-workspace-store.tsx @@ -5134,25 +5134,18 @@ export class GSDWorkspaceStore { } private handleToolExecutionStart(event: ToolExecutionStartEvent): void { - // Finalize any in-flight streaming content into segments before the tool runs - const pendingSegments: TurnSegment[] = [] - if (this.state.streamingThinkingText.length > 0) { - pendingSegments.push({ kind: "thinking", content: this.state.streamingThinkingText }) - } - if (this.state.streamingAssistantText.length > 0) { - pendingSegments.push({ kind: "text", content: this.state.streamingAssistantText }) - } this.patchState({ activeToolExecution: { id: event.toolCallId, name: event.toolName, args: (event as Record).args as Record | undefined, }, - ...(pendingSegments.length > 0 ? { - currentTurnSegments: [...this.state.currentTurnSegments, ...pendingSegments], - streamingAssistantText: "", - streamingThinkingText: "", - } : {}), + // Treat pre-tool streaming text as ephemeral. Claude Code can emit + // provisional assistant text before a tool call, then replace it with + // the real final text after the tool completes. If we finalize that + // interim text here, the chat timeline shows stale text above the tool. + streamingAssistantText: "", + streamingThinkingText: "", }) }