From 58591c37a48ee4439fc8c7c2d138b003c2df2dae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 13 May 2026 16:27:38 +0100 Subject: [PATCH] fix(tui): emit v4 embedded chat deltas (cherry picked from commit a6d878376b15a4d6f6f63cb4093816b4c20c0b4b) --- src/tui/embedded-backend.test.ts | 61 ++++++++++++++++++++++++++++++++ src/tui/embedded-backend.ts | 22 ++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/tui/embedded-backend.test.ts b/src/tui/embedded-backend.test.ts index 49e1a2ce4db..f4155bae133 100644 --- a/src/tui/embedded-backend.test.ts +++ b/src/tui/embedded-backend.test.ts @@ -207,6 +207,7 @@ describe("EmbeddedTuiBackend", () => { runId: "run-local-1", sessionKey: "agent:main:main", state: "delta", + deltaText: "hello", message: { role: "assistant", content: [{ type: "text", text: "hello" }], @@ -303,6 +304,65 @@ describe("EmbeddedTuiBackend", () => { }); }); + it("marks local embedded replacement deltas", async () => { + const { EmbeddedTuiBackend } = await import("./embedded-backend.js"); + const pending = deferred<{ + payloads: Array<{ text: string }>; + meta: Record; + }>(); + agentCommandFromIngressMock.mockReturnValueOnce(pending.promise); + + const backend = new EmbeddedTuiBackend(); + const events: Array<{ event: string; payload: unknown }> = []; + backend.onEvent = (evt) => { + events.push({ event: evt.event, payload: evt.payload }); + }; + + backend.start(); + await backend.sendChat({ + sessionKey: "agent:main:main", + message: "replace", + runId: "run-local-replace", + }); + + registeredListener?.({ + runId: "run-local-replace", + stream: "assistant", + data: { text: "Hello world" }, + }); + registeredListener?.({ + runId: "run-local-replace", + stream: "assistant", + data: { text: "Goodbye world" }, + }); + + pending.resolve({ payloads: [{ text: "Goodbye world" }], meta: {} }); + await flushMicrotasks(); + + const chatPayloads = events + .filter((entry) => entry.event === "chat") + .map( + (entry) => + entry.payload as { + state?: string; + deltaText?: string; + replace?: boolean; + }, + ); + expect( + chatPayloads + .filter((payload) => payload.state === "delta") + .map((payload) => ({ + state: payload.state, + deltaText: payload.deltaText, + replace: payload.replace, + })), + ).toEqual([ + { state: "delta", deltaText: "Hello world", replace: undefined }, + { state: "delta", deltaText: "Goodbye world", replace: true }, + ]); + }); + it("keeps a fallback response deliverable after a retryable lifecycle error", async () => { const { EmbeddedTuiBackend } = await import("./embedded-backend.js"); const pending = deferred<{ @@ -496,6 +556,7 @@ describe("EmbeddedTuiBackend", () => { runId: "run-tool-first", sessionKey: "agent:main:main", state: "delta", + deltaText: "", message: { role: "assistant", content: [{ type: "text", text: "" }], diff --git a/src/tui/embedded-backend.ts b/src/tui/embedded-backend.ts index 55b6bf86a4a..bab0c2c11dc 100644 --- a/src/tui/embedded-backend.ts +++ b/src/tui/embedded-backend.ts @@ -60,6 +60,7 @@ type LocalRunState = { sessionKey: string; controller: AbortController; buffer: string; + lastBroadcastText?: string; isBtw: boolean; question?: string; finalSent: boolean; @@ -106,6 +107,16 @@ function timeoutSecondsFromMs(timeoutMs?: number): string | undefined { return String(Math.max(0, Math.ceil(timeoutMs / 1000))); } +function resolveDeltaPayload(text: string, previousText: string | undefined) { + if (previousText === undefined) { + return { deltaText: text }; + } + if (!text.startsWith(previousText)) { + return { deltaText: text, replace: true as const }; + } + return { deltaText: text.slice(previousText.length) }; +} + export class EmbeddedTuiBackend implements TuiBackend { readonly connection = { url: "local embedded" }; @@ -396,11 +407,17 @@ export class EmbeddedTuiBackend implements TuiBackend { if (!text || projected.suppress) { return; } + const deltaPayload = resolveDeltaPayload(text, run.lastBroadcastText); + if (!deltaPayload.deltaText && !deltaPayload.replace) { + return; + } run.registered = true; + run.lastBroadcastText = text; this.emit("chat", { runId, sessionKey: run.sessionKey, state: "delta", + ...deltaPayload, message: { role: "assistant", content: [{ type: "text", text }], @@ -416,6 +433,7 @@ export class EmbeddedTuiBackend implements TuiBackend { } run.finalSent = true; run.registered = true; + run.lastBroadcastText = undefined; const projected = projectLiveAssistantBufferedText(run.buffer.trim(), { suppressLeadFragments: false, }); @@ -445,6 +463,7 @@ export class EmbeddedTuiBackend implements TuiBackend { } run.finalSent = true; run.registered = true; + run.lastBroadcastText = undefined; this.emit("chat", { runId, sessionKey: run.sessionKey, @@ -459,6 +478,7 @@ export class EmbeddedTuiBackend implements TuiBackend { } run.finalSent = true; run.registered = true; + run.lastBroadcastText = undefined; this.emit("chat", { runId, sessionKey: run.sessionKey, @@ -472,10 +492,12 @@ export class EmbeddedTuiBackend implements TuiBackend { return; } run.registered = true; + run.lastBroadcastText = ""; this.emit("chat", { runId, sessionKey: run.sessionKey, state: "delta", + deltaText: "", message: { role: "assistant", content: [{ type: "text", text: "" }],