Skip to content

Commit 4ee83d4

Browse files
Apply PR #11340: feat(tui): add Claude Code-style --fork-session flag to duplicate sessions before continuing (resolves #11137)
2 parents 02edf9f + f535193 commit 4ee83d4

File tree

4 files changed

+69
-10
lines changed

4 files changed

+69
-10
lines changed

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: ["fork"],
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
@@ -249,7 +249,8 @@ function App() {
249249
})
250250
local.model.set({ providerID, modelID }, { recent: true })
251251
}
252-
if (args.sessionID) {
252+
// Handle --session without --fork immediately (fork is handled in createEffect below)
253+
if (args.sessionID && !args.fork) {
253254
route.navigate({
254255
type: "session",
255256
sessionID: args.sessionID,
@@ -267,10 +268,36 @@ function App() {
267268
.find((x) => x.parentID === undefined)?.id
268269
if (match) {
269270
continued = true
270-
route.navigate({ type: "session", sessionID: match })
271+
if (args.fork) {
272+
sdk.client.session.fork({ sessionID: match }).then((result) => {
273+
if (result.data?.id) {
274+
route.navigate({ type: "session", sessionID: result.data.id })
275+
} else {
276+
toast.show({ message: "Failed to fork session", variant: "error" })
277+
}
278+
})
279+
} else {
280+
route.navigate({ type: "session", sessionID: match })
281+
}
271282
}
272283
})
273284

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