diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 235280a0d9..a22e6c4622 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -453,6 +453,12 @@ export namespace ACP { return } } + + // ACP clients already know the prompt they just submitted, so replaying + // live user parts duplicates the message. We still replay user history in + // loadSession() and forkSession() via processMessage(). + if (part.type !== "text" && part.type !== "file") return + return } diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts index a3ae01c5c1..a0944e33b7 100644 --- a/packages/opencode/test/acp/event-subscription.test.ts +++ b/packages/opencode/test/acp/event-subscription.test.ts @@ -295,6 +295,46 @@ describe("acp.agent event subscription", () => { }) }) + test("does not emit user_message_chunk for live prompt parts", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const { agent, controller, sessionUpdates, stop } = createFakeAgent() + const cwd = "/tmp/opencode-acp-test" + const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId) + + controller.push({ + directory: cwd, + payload: { + type: "message.part.updated", + properties: { + sessionID: sessionId, + time: Date.now(), + part: { + id: "part_1", + sessionID: sessionId, + messageID: "msg_user", + type: "text", + text: "hello", + }, + }, + }, + } as any) + + await new Promise((r) => setTimeout(r, 20)) + + expect( + sessionUpdates + .filter((u) => u.sessionId === sessionId) + .some((u) => u.update.sessionUpdate === "user_message_chunk"), + ).toBe(false) + + stop() + }, + }) + }) + test("keeps concurrent sessions isolated when message.part.delta events are interleaved", async () => { await using tmp = await tmpdir() await Instance.provide({