diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index b80a2389ef24..6e4c4db8df35 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -789,12 +789,18 @@ export const RunCommand = effectCmd({ } const model = pick(args.model) + const parts = [...files, { type: "text" as const, text: message }] + // Mirror the prompt to --format json consumers. The streamed + // message.part events only cover the assistant reply, so without this + // the user turn never appears in the stream (it lives only in + // `opencode export`). + emit("user", { parts }) const result = await client.session.prompt({ sessionID, agent, model, variant: args.variant, - parts: [...files, { type: "text", text: message }], + parts, }) if (result.error) { if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error)) diff --git a/packages/opencode/test/cli/run/run-process.test.ts b/packages/opencode/test/cli/run/run-process.test.ts index 00d2e64b377e..8c526c4cdc61 100644 --- a/packages/opencode/test/cli/run/run-process.test.ts +++ b/packages/opencode/test/cli/run/run-process.test.ts @@ -81,4 +81,28 @@ describe("opencode run (non-interactive subprocess)", () => { }), 60_000, ) + + // Regression for #29997: the user's prompt must surface in --format json as a + // `user` event. Previously the stream began at step_start and the prompt was + // only retrievable via `opencode export`, so anything rebuilding a transcript + // from the stream lost the user turn entirely. + cliIt.concurrent( + "--format json emits a user event carrying the prompt (regression for #29997)", + ({ llm, opencode }) => + Effect.gen(function* () { + yield* llm.text("ok") + const result = yield* opencode.run("marco", { format: "json" }) + opencode.expectExit(result, 0) + + const events = opencode.parseJsonEvents(result.stdout) + const user = events.find((e) => e.type === "user") + expect(user).toBeDefined() + expect(JSON.stringify(user!.parts)).toContain("marco") + + // The user turn must precede the assistant's reply in the stream. + const types = events.map((e) => e.type) + expect(types.indexOf("user")).toBeLessThan(types.indexOf("text")) + }), + 60_000, + ) })