From 69cfc44dba32b37aeadd13c5f4cf243967d9f346 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:35:57 +0530 Subject: [PATCH] fix(acp): replay loaded session transcript (#30645) Co-authored-by: opencode-agent[bot] Co-authored-by: Shoubhit Dash --- packages/opencode/src/acp/event.ts | 25 ++++++++++++ .../opencode/test/acp/service-session.test.ts | 40 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/opencode/src/acp/event.ts b/packages/opencode/src/acp/event.ts index df105d6ac..05b8b77b3 100644 --- a/packages/opencode/src/acp/event.ts +++ b/packages/opencode/src/acp/event.ts @@ -12,6 +12,7 @@ import type { import { Effect } from "effect" import { ACPSession } from "./session" import { ACPPermission } from "./permission" +import { partsToContentChunks, type ReplayPart } from "./content" import { duplicateRunningToolUpdate, errorToolUpdate, @@ -87,7 +88,31 @@ export class Subscription { await this.recordFetchedPart(message.info.sessionID, message, part) if (part.type === "tool") { await this.handleToolPart(message.info.sessionID, part) + continue } + await this.replayContentPart(message, part) + } + } + + private async replayContentPart(message: SessionMessageResponse, part: Part) { + if (part.type !== "text" && part.type !== "file" && part.type !== "reasoning") return + + const sessionUpdate = + part.type === "reasoning" + ? "agent_thought_chunk" + : message.info.role === "user" + ? "user_message_chunk" + : "agent_message_chunk" + + for (const chunk of partsToContentChunks([part as ReplayPart])) { + await this.input.connection.sessionUpdate({ + sessionId: message.info.sessionID, + update: { + sessionUpdate, + messageId: message.info.id, + ...chunk, + }, + }) } } diff --git a/packages/opencode/test/acp/service-session.test.ts b/packages/opencode/test/acp/service-session.test.ts index 2a189293e..890719072 100644 --- a/packages/opencode/test/acp/service-session.test.ts +++ b/packages/opencode/test/acp/service-session.test.ts @@ -316,6 +316,46 @@ describe("ACP service sessions", () => { expect(result.configOptions?.find((option) => option.id === "mode")?.currentValue).toBe("plan") }) + it("replays loaded session transcript chunks", async () => { + const { service, updates } = makeService([ + { + info: { id: "msg_user", sessionID: "ses_loaded", role: "user" }, + parts: [{ id: "part_user", sessionID: "ses_loaded", messageID: "msg_user", type: "text", text: "hello" }], + }, + { + info: { id: "msg_assistant", sessionID: "ses_loaded", role: "assistant" }, + parts: [ + { + id: "part_assistant", + sessionID: "ses_loaded", + messageID: "msg_assistant", + type: "text", + text: "hi there", + }, + ], + }, + ]) + + await Effect.runPromise(service.loadSession({ cwd: "/workspace", sessionId: "ses_loaded", mcpServers: [] })) + + expect( + updates + .map((item) => item.update) + .filter((item) => item.sessionUpdate === "user_message_chunk" || item.sessionUpdate === "agent_message_chunk"), + ).toEqual([ + { + sessionUpdate: "user_message_chunk", + messageId: "msg_user", + content: { type: "text", text: "hello" }, + }, + { + sessionUpdate: "agent_message_chunk", + messageId: "msg_assistant", + content: { type: "text", text: "hi there" }, + }, + ]) + }) + it("lists sessions sorted by updated time with cursor support", async () => { const { service } = makeService() const first = await Effect.runPromise(service.listSessions({ cwd: "/workspace" }))