Skip to content

Commit ea5aee2

Browse files
Apply PR #11340: feat(tui): add Claude Code-style --fork-session flag to duplicate sessions before continuing (resolves #11137)
2 parents 0b91e90 + 235ff55 commit ea5aee2

4 files changed

Lines changed: 69 additions & 10 deletions

File tree

packages/opencode/src/cli/cmd/run.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export const RunCommand = cmd({
5050
describe: "session id to continue",
5151
type: "string",
5252
})
53+
.option("fork-session", {
54+
alias: ["F"],
55+
describe: "fork the session before continuing (requires --continue or --session)",
56+
type: "boolean",
57+
})
5358
.option("share", {
5459
type: "boolean",
5560
describe: "share the session",
@@ -133,6 +138,11 @@ export const RunCommand = cmd({
133138
process.exit(1)
134139
}
135140

141+
if (args.forkSession && !args.continue && !args.session) {
142+
UI.error("--fork-session requires --continue or --session")
143+
process.exit(1)
144+
}
145+
136146
const execute = async (sdk: OpencodeClient, sessionID: string) => {
137147
const printEvent = (color: string, type: string, title: string) => {
138148
UI.println(
@@ -279,11 +289,16 @@ export const RunCommand = cmd({
279289
const sdk = createOpencodeClient({ baseUrl: args.attach })
280290

281291
const sessionID = await (async () => {
282-
if (args.continue) {
283-
const result = await sdk.session.list()
284-
return result.data?.find((s) => !s.parentID)?.id
292+
const baseID = args.continue
293+
? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id
294+
: args.session
295+
296+
if (baseID && args.forkSession) {
297+
const forked = await sdk.session.fork({ sessionID: baseID })
298+
return forked.data?.id
285299
}
286-
if (args.session) return args.session
300+
301+
if (baseID) return baseID
287302

288303
const title =
289304
args.title !== undefined
@@ -354,11 +369,16 @@ export const RunCommand = cmd({
354369
}
355370

356371
const sessionID = await (async () => {
357-
if (args.continue) {
358-
const result = await sdk.session.list()
359-
return result.data?.find((s) => !s.parentID)?.id
372+
const baseID = args.continue
373+
? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id
374+
: args.session
375+
376+
if (baseID && args.forkSession) {
377+
const forked = await sdk.session.fork({ sessionID: baseID })
378+
return forked.data?.id
360379
}
361-
if (args.session) return args.session
380+
381+
if (baseID) return baseID
362382

363383
const title =
364384
args.title !== undefined

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,8 @@ function App() {
247247
})
248248
local.model.set({ providerID, modelID }, { recent: true })
249249
}
250-
if (args.sessionID) {
250+
// Handle --session without --fork immediately (fork is handled in createEffect below)
251+
if (args.sessionID && !args.fork) {
251252
route.navigate({
252253
type: "session",
253254
sessionID: args.sessionID,
@@ -265,10 +266,36 @@ function App() {
265266
.find((x) => x.parentID === undefined)?.id
266267
if (match) {
267268
continued = true
268-
route.navigate({ type: "session", sessionID: match })
269+
if (args.fork) {
270+
sdk.client.session.fork({ sessionID: match }).then((result) => {
271+
if (result.data?.id) {
272+
route.navigate({ type: "session", sessionID: result.data.id })
273+
} else {
274+
toast.show({ message: "Failed to fork session", variant: "error" })
275+
}
276+
})
277+
} else {
278+
route.navigate({ type: "session", sessionID: match })
279+
}
269280
}
270281
})
271282

283+
// Handle --session with --fork: wait for sync to be fully complete before forking
284+
// (session list loads in non-blocking phase for --session, so we must wait for "complete"
285+
// to avoid a race where reconcile overwrites the newly forked session)
286+
let forked = false
287+
createEffect(() => {
288+
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
289+
forked = true
290+
sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
291+
if (result.data?.id) {
292+
route.navigate({ type: "session", sessionID: result.data.id })
293+
} else {
294+
toast.show({ message: "Failed to fork session", variant: "error" })
295+
}
296+
})
297+
})
298+
272299
createEffect(
273300
on(
274301
() => sync.status === "complete" && sync.data.provider.length === 0,

packages/opencode/src/cli/cmd/tui/context/args.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Args {
66
prompt?: string
77
continue?: boolean
88
sessionID?: string
9+
fork?: boolean
910
}
1011

1112
export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export const TuiThreadCommand = cmd({
6464
type: "string",
6565
describe: "session id to continue",
6666
})
67+
.option("fork-session", {
68+
alias: ["F"],
69+
type: "boolean",
70+
describe: "fork the session when continuing (use with --continue or --session)",
71+
})
6772
.option("prompt", {
6873
type: "string",
6974
describe: "prompt to use",
@@ -73,6 +78,11 @@ export const TuiThreadCommand = cmd({
7378
describe: "agent to use",
7479
}),
7580
handler: async (args) => {
81+
if (args.forkSession && !args.continue && !args.session) {
82+
UI.error("--fork-session requires --continue or --session")
83+
process.exit(1)
84+
}
85+
7686
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
7787
const baseCwd = process.env.PWD ?? process.cwd()
7888
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
@@ -150,6 +160,7 @@ export const TuiThreadCommand = cmd({
150160
agent: args.agent,
151161
model: args.model,
152162
prompt,
163+
fork: args.forkSession,
153164
},
154165
onExit: async () => {
155166
await client.call("shutdown", undefined)

0 commit comments

Comments
 (0)