fix(webchat): render final assistant payloads without history wait (#14928)

Co-authored-by: BradGroux <3053586+BradGroux@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-22 21:13:00 +01:00
parent 02dc0c8752
commit 8264d4521b
3 changed files with 46 additions and 1 deletions

View File

@@ -53,7 +53,7 @@ describe("handleChatEvent", () => {
expect(state.chatStream).toBe("Hello");
});
it("returns 'final' for final from another run (e.g. sub-agent announce) without clearing state", () => {
it("returns final for final from another run without clearing active stream", () => {
const state = createState({
sessionKey: "main",
chatRunId: "run-user",
@@ -73,6 +73,7 @@ describe("handleChatEvent", () => {
expect(state.chatRunId).toBe("run-user");
expect(state.chatStream).toBe("Working...");
expect(state.chatStreamStartedAt).toBe(123);
expect(state.chatMessages).toEqual([]);
});
it("processes final from own run and clears state", () => {
@@ -93,6 +94,30 @@ describe("handleChatEvent", () => {
expect(state.chatStreamStartedAt).toBe(null);
});
it("appends final payload message from own run before clearing stream state", () => {
const state = createState({
sessionKey: "main",
chatRunId: "run-1",
chatStream: "Reply",
chatStreamStartedAt: 100,
});
const payload: ChatEventPayload = {
runId: "run-1",
sessionKey: "main",
state: "final",
message: {
role: "assistant",
content: [{ type: "text", text: "Reply" }],
timestamp: 101,
},
};
expect(handleChatEvent(state, payload)).toBe("final");
expect(state.chatMessages).toEqual([payload.message]);
expect(state.chatRunId).toBe(null);
expect(state.chatStream).toBe(null);
expect(state.chatStreamStartedAt).toBe(null);
});
it("processes aborted from own run and keeps partial assistant message", () => {
const existingMessage = {
role: "user",

View File

@@ -72,6 +72,21 @@ function normalizeAbortedAssistantMessage(message: unknown): Record<string, unkn
return candidate;
}
function normalizeFinalAssistantMessage(message: unknown): Record<string, unknown> | null {
if (!message || typeof message !== "object") {
return null;
}
const candidate = message as Record<string, unknown>;
const role = typeof candidate.role === "string" ? candidate.role.toLowerCase() : "";
if (role && role !== "assistant") {
return null;
}
if (!("content" in candidate) && !("text" in candidate)) {
return null;
}
return candidate;
}
export async function sendChatMessage(
state: ChatState,
message: string,
@@ -208,6 +223,10 @@ export function handleChatEvent(state: ChatState, payload?: ChatEventPayload) {
}
}
} else if (payload.state === "final") {
const finalMessage = normalizeFinalAssistantMessage(payload.message);
if (finalMessage) {
state.chatMessages = [...state.chatMessages, finalMessage];
}
state.chatStream = null;
state.chatRunId = null;
state.chatStreamStartedAt = null;