Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
24 changes: 24 additions & 0 deletions packages/opencode/test/cli/run/run-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
})
Loading