diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index fd36a75cb9..1ba8b282a9 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -1944,6 +1944,75 @@ describe("OpenCode adapter startTurn error handling", () => { ]); }); + test("streamHistory hides user text that OpenCode marks synthetic", async () => { + const fakeClient = { + session: { + get: vi.fn().mockResolvedValue({ + data: { revert: undefined }, + error: undefined, + }), + messages: vi.fn().mockResolvedValue({ + data: [ + { + info: { id: "msg_continue", sessionID: "ses_unit_test", role: "user" }, + parts: [ + { + id: "prt_continue", + sessionID: "ses_unit_test", + messageID: "msg_continue", + type: "text", + text: "Summarize the task tool output above and continue with your task.", + synthetic: true, + }, + ], + }, + { + info: { id: "msg_user", sessionID: "ses_unit_test", role: "user" }, + parts: [ + { + id: "prt_user", + sessionID: "ses_unit_test", + messageID: "msg_user", + type: "text", + text: "Read the notes", + }, + { + id: "prt_resource", + sessionID: "ses_unit_test", + messageID: "msg_user", + type: "text", + text: "Reading MCP resource: notes.md", + synthetic: true, + }, + ], + }, + ], + error: undefined, + }), + }, + } as never; + + const session = new __openCodeInternals.OpenCodeAgentSession( + { provider: "opencode", cwd: "/tmp/test" }, + fakeClient, + "ses_unit_test", + createTestLogger(), + ); + + const history: AgentStreamEvent[] = []; + for await (const event of session.streamHistory()) { + history.push(event); + } + + expect(history).toEqual([ + { + type: "timeline", + provider: "opencode", + item: { type: "user_message", text: "Read the notes", messageId: "msg_user" }, + }, + ]); + }); + test("streamHistory omits replay timestamps when OpenCode omits times", async () => { const fakeClient = { session: { diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 362f2a7442..2276e85786 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -1318,7 +1318,10 @@ function buildOpenCodeReplayTimelineEvents( } if (info.role === "user") { const text = parts - .filter((part): part is Extract => part.type === "text") + .filter( + (part): part is Extract => + part.type === "text" && isUserAuthoredOpenCodeText(part), + ) .map((part) => part.text) .join(""); @@ -2854,6 +2857,11 @@ function shouldSuppressOpenCodeAssistantPart( ); } +// OpenCode marks text it adds to a user message itself with `synthetic` and hides it from its own UI. +function isUserAuthoredOpenCodeText(part: { synthetic?: boolean }): boolean { + return part.synthetic !== true; +} + function appendOpenCodeTextPart( part: Extract< Extract["properties"]["part"], @@ -2864,7 +2872,11 @@ function appendOpenCodeTextPart( events: AgentStreamEvent[], ): void { if (messageRole === "user") { - if (!part.text || state.emittedUserMessageIds?.has(part.messageID)) { + if ( + !part.text || + !isUserAuthoredOpenCodeText(part) || + state.emittedUserMessageIds?.has(part.messageID) + ) { return; } state.emittedUserMessageIds?.add(part.messageID); diff --git a/packages/server/src/server/agent/providers/opencode/event-translator.test.ts b/packages/server/src/server/agent/providers/opencode/event-translator.test.ts index 50a3a9edfe..69977f1ab9 100644 --- a/packages/server/src/server/agent/providers/opencode/event-translator.test.ts +++ b/packages/server/src/server/agent/providers/opencode/event-translator.test.ts @@ -1276,6 +1276,44 @@ describe("translateOpenCodeEvent", () => { expect(result).toEqual([]); }); + it("shows OpenCode user text parts unless OpenCode marks them synthetic", () => { + const state = createState(); + const userTextPart = (messageId: string, text: string, synthetic: boolean) => { + translateOpenCodeEvent( + { + type: "message.updated", + properties: { info: { id: messageId, sessionID: "session-1", role: "user" } }, + }, + state, + ); + return translateOpenCodeEvent( + { + type: "message.part.updated", + properties: { + part: { + id: `part-${messageId}`, + sessionID: "session-1", + messageID: messageId, + type: "text", + text, + synthetic, + }, + }, + }, + state, + ); + }; + + expect(userTextPart("msg-continue", "Continue if you have next steps", true)).toEqual([]); + expect(userTextPart("msg-user", "Plugin prompt", false)).toEqual([ + { + type: "timeline", + provider: "opencode", + item: { type: "user_message", text: "Plugin prompt", messageId: "msg-user" }, + }, + ]); + }); + it("ignores message.part.delta for user messages", () => { const state = createState();