diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 54248f96f3..0a7f0b716a 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -50,6 +50,11 @@ export const RunCommand = cmd({ describe: "session id to continue", type: "string", }) + .option("fork-session", { + alias: ["fork"], + describe: "fork the session before continuing (requires --continue or --session)", + type: "boolean", + }) .option("share", { type: "boolean", describe: "share the session", @@ -133,6 +138,11 @@ export const RunCommand = cmd({ process.exit(1) } + if (args.forkSession && !args.continue && !args.session) { + UI.error("--fork-session requires --continue or --session") + process.exit(1) + } + const execute = async (sdk: OpencodeClient, sessionID: string) => { const printEvent = (color: string, type: string, title: string) => { UI.println( @@ -279,11 +289,16 @@ export const RunCommand = cmd({ const sdk = createOpencodeClient({ baseUrl: args.attach }) const sessionID = await (async () => { - 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.forkSession) { + const forked = await sdk.session.fork({ sessionID: baseID }) + return forked.data?.id } - if (args.session) return args.session + + if (baseID) return baseID const title = args.title !== undefined @@ -354,11 +369,16 @@ export const RunCommand = cmd({ } const sessionID = await (async () => { - 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.forkSession) { + const forked = await sdk.session.fork({ sessionID: baseID }) + return forked.data?.id } - if (args.session) return args.session + + if (baseID) return baseID const title = args.title !== undefined diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 713def2e5a..11334bed83 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -249,7 +249,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, @@ -267,10 +268,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 ffd43009a4..8a229ffaba 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 0571426854..8d7c0bd679 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -64,6 +64,11 @@ export const TuiThreadCommand = cmd({ type: "string", describe: "session id to continue", }) + .option("fork-session", { + alias: ["fork"], + type: "boolean", + describe: "fork the session when continuing (use with --continue or --session)", + }) .option("prompt", { type: "string", describe: "prompt to use", @@ -73,6 +78,11 @@ export const TuiThreadCommand = cmd({ describe: "agent to use", }), handler: async (args) => { + if (args.forkSession && !args.continue && !args.session) { + UI.error("--fork-session 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 +160,7 @@ export const TuiThreadCommand = cmd({ agent: args.agent, model: args.model, prompt, + fork: args.forkSession, }, onExit: async () => { await client.call("shutdown", undefined)