diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 6960ffd553f..bf71fbe30e5 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -236,6 +236,10 @@ export const RunCommand = cmd({ describe: "session id to continue", type: "string", }) + .option("fork", { + describe: "fork the session before continuing (requires --continue or --session)", + type: "boolean", + }) .option("share", { type: "boolean", describe: "share the session", @@ -324,6 +328,11 @@ export const RunCommand = cmd({ process.exit(1) } + if (args.fork && !args.continue && !args.session) { + UI.error("--fork requires --continue or --session") + process.exit(1) + } + const rules: PermissionNext.Ruleset = [ { permission: "question", @@ -349,11 +358,17 @@ export const RunCommand = cmd({ } async function session(sdk: OpencodeClient) { - if (args.continue) { - const result = await sdk.session.list() - return result.data?.find((s) => !s.parentID)?.id + const baseID = args.continue + ? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id + : args.session + + if (baseID && args.fork) { + const forked = await sdk.session.fork({ sessionID: baseID }) + return forked.data?.id } - if (args.session) return args.session + + if (baseID) return baseID + const name = title() const result = await sdk.session.create({ title: name, permission: rules }) return result.data?.id diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7442037604b..0d5aefe7bc3 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -250,7 +250,8 @@ function App() { }) local.model.set({ providerID, modelID }, { recent: true }) } - if (args.sessionID) { + // Handle --session without --fork immediately (fork is handled in createEffect below) + if (args.sessionID && !args.fork) { route.navigate({ type: "session", sessionID: args.sessionID, @@ -268,10 +269,36 @@ function App() { .find((x) => x.parentID === undefined)?.id if (match) { continued = true - route.navigate({ type: "session", sessionID: match }) + if (args.fork) { + sdk.client.session.fork({ sessionID: match }).then((result) => { + if (result.data?.id) { + route.navigate({ type: "session", sessionID: result.data.id }) + } else { + toast.show({ message: "Failed to fork session", variant: "error" }) + } + }) + } else { + route.navigate({ type: "session", sessionID: match }) + } } }) + // Handle --session with --fork: wait for sync to be fully complete before forking + // (session list loads in non-blocking phase for --session, so we must wait for "complete" + // to avoid a race where reconcile overwrites the newly forked session) + let forked = false + createEffect(() => { + if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return + forked = true + sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { + if (result.data?.id) { + route.navigate({ type: "session", sessionID: result.data.id }) + } else { + toast.show({ message: "Failed to fork session", variant: "error" }) + } + }) + }) + createEffect( on( () => sync.status === "complete" && sync.data.provider.length === 0, diff --git a/packages/opencode/src/cli/cmd/tui/context/args.tsx b/packages/opencode/src/cli/cmd/tui/context/args.tsx index ffd43009a41..8a229ffaba6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/args.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/args.tsx @@ -6,6 +6,7 @@ export interface Args { prompt?: string continue?: boolean sessionID?: string + fork?: boolean } export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({ diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 05714268545..2ea49ff6b2b 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -64,6 +64,10 @@ export const TuiThreadCommand = cmd({ type: "string", describe: "session id to continue", }) + .option("fork", { + type: "boolean", + describe: "fork the session when continuing (use with --continue or --session)", + }) .option("prompt", { type: "string", describe: "prompt to use", @@ -73,6 +77,11 @@ export const TuiThreadCommand = cmd({ describe: "agent to use", }), handler: async (args) => { + if (args.fork && !args.continue && !args.session) { + UI.error("--fork requires --continue or --session") + process.exit(1) + } + // Resolve relative paths against PWD to preserve behavior when using --cwd flag const baseCwd = process.env.PWD ?? process.cwd() const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd() @@ -150,6 +159,7 @@ export const TuiThreadCommand = cmd({ agent: args.agent, model: args.model, prompt, + fork: args.fork, }, onExit: async () => { await client.call("shutdown", undefined)