Skip to content

Commit 095b9b4

Browse files
fix(ui): clear chat stream before terminal commits
1 parent 388dc56 commit 095b9b4

2 files changed

Lines changed: 68 additions & 33 deletions

File tree

ui/src/ui/controllers/chat.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,28 @@ function createActiveStreamingState() {
8282
});
8383
}
8484

85+
function trackChatMessagesAssignments(state: ChatState) {
86+
let chatMessages = state.chatMessages;
87+
const assignments: Array<{
88+
chatRunId: string | null;
89+
chatStream: string | null;
90+
messages: unknown[];
91+
}> = [];
92+
Object.defineProperty(state, "chatMessages", {
93+
configurable: true,
94+
get: () => chatMessages,
95+
set: (messages: unknown[]) => {
96+
assignments.push({
97+
chatRunId: state.chatRunId,
98+
chatStream: state.chatStream,
99+
messages,
100+
});
101+
chatMessages = messages;
102+
},
103+
});
104+
return assignments;
105+
}
106+
85107
function createOtherRunSilentFinalPayload(text: string): ChatEventPayload {
86108
return {
87109
runId: "run-announce",
@@ -733,7 +755,10 @@ describe("handleChatEvent", () => {
733755
sessionKey: "main",
734756
state: "final",
735757
};
758+
const assignments = trackChatMessagesAssignments(state);
759+
736760
expect(handleChatEvent(state, payload)).toBe("final");
761+
expect(assignments).toMatchObject([{ chatRunId: null, chatStream: null }]);
737762
expect(state.chatRunId).toBe(null);
738763
expect(state.chatStream).toBe(null);
739764
expect(state.chatStreamStartedAt).toBe(null);
@@ -799,7 +824,7 @@ describe("handleChatEvent", () => {
799824
expect(state.chatStream).toBe(null);
800825
});
801826

802-
it("appends final payload message from own run before clearing stream state", () => {
827+
it("appends final payload message from own run after clearing stream state", () => {
803828
const state = createState({
804829
sessionKey: "main",
805830
chatRunId: "run-1",
@@ -816,7 +841,10 @@ describe("handleChatEvent", () => {
816841
timestamp: 101,
817842
},
818843
};
844+
const assignments = trackChatMessagesAssignments(state);
845+
819846
expect(handleChatEvent(state, payload)).toBe("final");
847+
expect(assignments).toMatchObject([{ chatRunId: null, chatStream: null }]);
820848
expect(state.chatMessages).toEqual([payload.message]);
821849
expect(state.chatRunId).toBe(null);
822850
expect(state.chatStream).toBe(null);
@@ -847,8 +875,10 @@ describe("handleChatEvent", () => {
847875
state: "aborted",
848876
message: partialMessage,
849877
};
878+
const assignments = trackChatMessagesAssignments(state);
850879

851880
expect(handleChatEvent(state, payload)).toBe("aborted");
881+
expect(assignments).toMatchObject([{ chatRunId: null, chatStream: null }]);
852882
expect(state.chatRunId).toBe(null);
853883
expect(state.chatStream).toBe(null);
854884
expect(state.chatStreamStartedAt).toBe(null);

ui/src/ui/controllers/chat.ts

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,45 +1129,50 @@ export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
11291129
}
11301130
} else if (payload.state === "final") {
11311131
const finalMessage = normalizeFinalAssistantMessage(payload.message);
1132-
if (finalMessage && !shouldHideAssistantChatMessage(finalMessage)) {
1133-
state.chatMessages = [...state.chatMessages, finalMessage];
1134-
} else if (
1135-
state.chatStream?.trim() &&
1136-
!isSilentReplyStream(state.chatStream) &&
1137-
!isHeartbeatAckStream(state.chatStream)
1138-
) {
1139-
state.chatMessages = [
1140-
...state.chatMessages,
1141-
{
1142-
role: "assistant",
1143-
content: [{ type: "text", text: state.chatStream }],
1144-
timestamp: Date.now(),
1145-
},
1146-
];
1147-
}
1132+
const visibleFinalMessage =
1133+
finalMessage && !shouldHideAssistantChatMessage(finalMessage) ? finalMessage : null;
1134+
const streamedText = state.chatStream ?? "";
1135+
const fallbackMessage =
1136+
!visibleFinalMessage &&
1137+
streamedText.trim() &&
1138+
!isSilentReplyStream(streamedText) &&
1139+
!isHeartbeatAckStream(streamedText)
1140+
? {
1141+
role: "assistant",
1142+
content: [{ type: "text", text: streamedText }],
1143+
timestamp: Date.now(),
1144+
}
1145+
: null;
11481146
reconcileTerminalRun("done", "done");
1147+
if (visibleFinalMessage) {
1148+
state.chatMessages = [...state.chatMessages, visibleFinalMessage];
1149+
} else if (fallbackMessage) {
1150+
state.chatMessages = [...state.chatMessages, fallbackMessage];
1151+
}
11491152
} else if (payload.state === "aborted") {
11501153
const normalizedMessage = normalizeAbortedAssistantMessage(payload.message);
1151-
if (normalizedMessage && !shouldHideAssistantChatMessage(normalizedMessage)) {
1152-
state.chatMessages = [...state.chatMessages, normalizedMessage];
1153-
} else {
1154-
const streamedText = state.chatStream ?? "";
1155-
if (
1156-
streamedText.trim() &&
1157-
!isSilentReplyStream(streamedText) &&
1158-
!isHeartbeatAckStream(streamedText)
1159-
) {
1160-
state.chatMessages = [
1161-
...state.chatMessages,
1162-
{
1154+
const visibleAbortedMessage =
1155+
normalizedMessage && !shouldHideAssistantChatMessage(normalizedMessage)
1156+
? normalizedMessage
1157+
: null;
1158+
const streamedText = state.chatStream ?? "";
1159+
const fallbackMessage =
1160+
!visibleAbortedMessage &&
1161+
streamedText.trim() &&
1162+
!isSilentReplyStream(streamedText) &&
1163+
!isHeartbeatAckStream(streamedText)
1164+
? {
11631165
role: "assistant",
11641166
content: [{ type: "text", text: streamedText }],
11651167
timestamp: Date.now(),
1166-
},
1167-
];
1168-
}
1169-
}
1168+
}
1169+
: null;
11701170
reconcileTerminalRun("interrupted", "killed");
1171+
if (visibleAbortedMessage) {
1172+
state.chatMessages = [...state.chatMessages, visibleAbortedMessage];
1173+
} else if (fallbackMessage) {
1174+
state.chatMessages = [...state.chatMessages, fallbackMessage];
1175+
}
11711176
} else if (payload.state === "error") {
11721177
const errorMessage = hadActiveRunBeforeEvent ? buildErrorAssistantMessage(payload) : null;
11731178
if (errorMessage) {

0 commit comments

Comments
 (0)