From 32fdd21c80fa8b69fe987adcc56d19c0d8aae792 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 22 Mar 2026 19:48:04 -0700 Subject: [PATCH] fix(acp): preserve hidden thought replay on session load --- src/acp/translator.session-rate-limit.test.ts | 17 +++- src/acp/translator.ts | 88 ++++++++++++------- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/acp/translator.session-rate-limit.test.ts b/src/acp/translator.session-rate-limit.test.ts index 566b61a5027..cbb19b6548c 100644 --- a/src/acp/translator.session-rate-limit.test.ts +++ b/src/acp/translator.session-rate-limit.test.ts @@ -242,7 +242,7 @@ describe("acp session UX bridge behavior", () => { sessionStore.clearAllSessionsForTest(); }); - it("replays user and assistant text history on loadSession and returns initial controls", async () => { + it("replays user text, assistant text, and hidden assistant thinking on loadSession", async () => { const sessionStore = createInMemorySessionStore(); const connection = createAcpConnection(); const sessionUpdate = connection.__sessionUpdateMock; @@ -283,7 +283,13 @@ describe("acp session UX bridge behavior", () => { return { messages: [ { role: "user", content: [{ type: "text", text: "Question" }] }, - { role: "assistant", content: [{ type: "text", text: "Answer" }] }, + { + role: "assistant", + content: [ + { type: "thinking", thinking: "Internal loop about NO_REPLY" }, + { type: "text", text: "Answer" }, + ], + }, { role: "system", content: [{ type: "text", text: "ignore me" }] }, { role: "assistant", content: [{ type: "image", image: "skip" }] }, ], @@ -332,6 +338,13 @@ describe("acp session UX bridge behavior", () => { content: { type: "text", text: "Question" }, }, }); + expect(sessionUpdate).toHaveBeenCalledWith({ + sessionId: "agent:main:work", + update: { + sessionUpdate: "agent_thought_chunk", + content: { type: "text", text: "Internal loop about NO_REPLY" }, + }, + }); expect(sessionUpdate).toHaveBeenCalledWith({ sessionId: "agent:main:work", update: { diff --git a/src/acp/translator.ts b/src/acp/translator.ts index d275913800e..cc20ac38fc6 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -134,6 +134,11 @@ type GatewayChatContentBlock = { thinking?: string; }; +type ReplayChunk = { + sessionUpdate: "user_message_chunk" | "agent_message_chunk" | "agent_thought_chunk"; + text: string; +}; + const SESSION_CREATE_RATE_LIMIT_DEFAULT_MAX_REQUESTS = 120; const SESSION_CREATE_RATE_LIMIT_DEFAULT_WINDOW_MS = 10_000; @@ -261,25 +266,51 @@ function buildSessionPresentation(params: { return { configOptions, modes }; } -function extractReplayText(content: unknown): string | undefined { - if (typeof content === "string") { - return content.length > 0 ? content : undefined; +function extractReplayChunks(message: GatewayTranscriptMessage): ReplayChunk[] { + const role = typeof message.role === "string" ? message.role : ""; + if (role !== "user" && role !== "assistant") { + return []; } - if (!Array.isArray(content)) { - return undefined; + if (typeof message.content === "string") { + return message.content.length > 0 + ? [ + { + sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk", + text: message.content, + }, + ] + : []; } - const text = content - .map((block) => { - if (!block || typeof block !== "object" || Array.isArray(block)) { - return ""; - } - const typedBlock = block as { type?: unknown; text?: unknown }; - return typedBlock.type === "text" && typeof typedBlock.text === "string" - ? typedBlock.text - : ""; - }) - .join(""); - return text.length > 0 ? text : undefined; + if (!Array.isArray(message.content)) { + return []; + } + + const replayChunks: ReplayChunk[] = []; + for (const block of message.content) { + if (!block || typeof block !== "object" || Array.isArray(block)) { + continue; + } + const typedBlock = block as GatewayChatContentBlock; + if (typedBlock.type === "text" && typeof typedBlock.text === "string" && typedBlock.text) { + replayChunks.push({ + sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk", + text: typedBlock.text, + }); + continue; + } + if ( + role === "assistant" && + typedBlock.type === "thinking" && + typeof typedBlock.thinking === "string" && + typedBlock.thinking + ) { + replayChunks.push({ + sessionUpdate: "agent_thought_chunk", + text: typedBlock.thinking, + }); + } + } + return replayChunks; } function buildSessionMetadata(params: { @@ -1045,21 +1076,16 @@ export class AcpGatewayAgent implements Agent { transcript: ReadonlyArray, ): Promise { for (const message of transcript) { - const role = typeof message.role === "string" ? message.role : ""; - if (role !== "user" && role !== "assistant") { - continue; + const replayChunks = extractReplayChunks(message); + for (const chunk of replayChunks) { + await this.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: chunk.sessionUpdate, + content: { type: "text", text: chunk.text }, + }, + }); } - const text = extractReplayText(message.content); - if (!text) { - continue; - } - await this.connection.sessionUpdate({ - sessionId, - update: { - sessionUpdate: role === "user" ? "user_message_chunk" : "agent_message_chunk", - content: { type: "text", text }, - }, - }); } }