Skip to content

Commit d256017

Browse files
committed
agentHost: retire the optimistic turn start on the first backend action
The client holds its own chat/turnStarted in _pendingActions until the backend echoes it with the originating clientSeq, which the agent host does not send. The start stays pending for the whole turn, and _recomputeOptimistic replays it over confirmed state on every action, resetting activeTurn to a fresh empty turn. The UI reads the optimistic state, so a streaming response renders nothing until completion moves the turn into turns[] and the whole reply appears at once. Promote the pending start on the first backend action naming the turn rather than only on a terminal one.
1 parent 5836444 commit d256017

2 files changed

Lines changed: 41 additions & 8 deletions

File tree

src/vs/platform/agentHost/common/state/agentSubscription.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -482,28 +482,30 @@ export class ChatStateSubscription extends BaseAgentSubscription<ChatState> {
482482
this._confirmedApply(envelope.action);
483483
}
484484
} else if (!envelope.rejectionReason) {
485-
this._promotePendingTurnStartIfTerminal(envelope.action);
485+
this._promotePendingTurnStart(envelope.action);
486486
this._confirmedApply(envelope.action);
487487
}
488488
this._recomputeOptimistic();
489489
}
490490

491-
private _promotePendingTurnStartIfTerminal(action: StateAction): void {
492-
// A backend-originated terminal turn action may arrive without the clientSeq
493-
// that would normally confirm our optimistic turn start. Promote that start
494-
// first so the terminal action can close it instead of leaving it pending.
491+
/**
492+
* Confirms an optimistic turn start from the first backend action naming its
493+
* turn, since a backend-originated action may arrive without our clientSeq.
494+
*/
495+
private _promotePendingTurnStart(action: StateAction): void {
495496
if (!isChatAction(action)) {
496497
return;
497498
}
498-
if (action.type !== ActionType.ChatTurnComplete && action.type !== ActionType.ChatTurnCancelled && action.type !== ActionType.ChatError) {
499+
const turnId = 'turnId' in action ? action.turnId : undefined;
500+
if (turnId === undefined) {
499501
return;
500502
}
501-
const index = this._pendingActions.findIndex(p => p.action.type === ActionType.ChatTurnStarted && p.action.turnId === action.turnId);
503+
const index = this._pendingActions.findIndex(p => p.action.type === ActionType.ChatTurnStarted && p.action.turnId === turnId);
502504
if (index === -1) {
503505
return;
504506
}
505507
const [{ action: pendingAction }] = this._pendingActions.splice(index, 1);
506-
if (this._confirmedState && (!this._confirmedState.activeTurn || this._confirmedState.activeTurn.id !== action.turnId)) {
508+
if (this._confirmedState && (!this._confirmedState.activeTurn || this._confirmedState.activeTurn.id !== turnId)) {
507509
this._confirmedState = this._applyReducer(this._confirmedState, pendingAction);
508510
}
509511
}

src/vs/platform/agentHost/test/common/agentSubscription.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,37 @@ suite('ChatStateSubscription', () => {
730730
turns: [{ id: 'turn-1', state: TurnState.Complete }],
731731
});
732732
});
733+
734+
test('streamed response parts stay visible while the optimistic turn start is pending', () => {
735+
const sub = createSub();
736+
sub.handleSnapshot(makeChatState(chatUri), 0);
737+
738+
sub.applyOptimistic({
739+
type: ActionType.ChatTurnStarted,
740+
turnId: 'turn-1',
741+
startedAt: '2025-01-01T00:00:00.000Z',
742+
message: { text: 'hello', origin: { kind: MessageKind.User } },
743+
});
744+
745+
sub.receiveEnvelope(makeEnvelope(
746+
{
747+
type: ActionType.ChatResponsePart,
748+
turnId: 'turn-1',
749+
part: { kind: ResponsePartKind.Markdown, id: 'part-1', content: '' },
750+
},
751+
1,
752+
undefined,
753+
));
754+
sub.receiveEnvelope(makeEnvelope(
755+
{ type: ActionType.ChatDelta, turnId: 'turn-1', partId: 'part-1', content: 'streamed' },
756+
2,
757+
undefined,
758+
));
759+
760+
assert.deepStrictEqual((sub.value as ChatState | undefined)?.activeTurn?.responseParts, [
761+
{ kind: ResponsePartKind.Markdown, id: 'part-1', content: 'streamed' },
762+
]);
763+
});
733764
});
734765

735766
// TerminalStateSubscription

0 commit comments

Comments
 (0)