Merge pull request #3970 from jeremymcs/fix/ask-user-question-stream-order

fix(web): preserve only final ask_user_questions text
This commit is contained in:
Jeremy McSpadden 2026-04-11 07:36:30 -05:00 committed by GitHub
commit 679587bee2
2 changed files with 48 additions and 13 deletions

View file

@ -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");

View file

@ -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<string, unknown>).args as Record<string, unknown> | 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: "",
})
}