Skip to content

Commit 034a8a4

Browse files
ariane-emoryBryceRyan
authored andcommitted
feat(tui): add Claude Code-style --fork flag to duplicate sessions before continuing (resolves anomalyco#11137) (anomalyco#11340)
1 parent ba6e930 commit 034a8a4

4 files changed

Lines changed: 59 additions & 6 deletions

File tree

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ export const RunCommand = cmd({
236236
describe: "session id to continue",
237237
type: "string",
238238
})
239+
.option("fork", {
240+
describe: "fork the session before continuing (requires --continue or --session)",
241+
type: "boolean",
242+
})
239243
.option("share", {
240244
type: "boolean",
241245
describe: "share the session",
@@ -324,6 +328,11 @@ export const RunCommand = cmd({
324328
process.exit(1)
325329
}
326330

331+
if (args.fork && !args.continue && !args.session) {
332+
UI.error("--fork requires --continue or --session")
333+
process.exit(1)
334+
}
335+
327336
const rules: PermissionNext.Ruleset = [
328337
{
329338
permission: "question",
@@ -349,11 +358,17 @@ export const RunCommand = cmd({
349358
}
350359

351360
async function session(sdk: OpencodeClient) {
352-
if (args.continue) {
353-
const result = await sdk.session.list()
354-
return result.data?.find((s) => !s.parentID)?.id
361+
const baseID = args.continue
362+
? (await sdk.session.list()).data?.find((s) => !s.parentID)?.id
363+
: args.session
364+
365+
if (baseID && args.fork) {
366+
const forked = await sdk.session.fork({ sessionID: baseID })
367+
return forked.data?.id
355368
}
356-
if (args.session) return args.session
369+
370+
if (baseID) return baseID
371+
357372
const name = title()
358373
const result = await sdk.session.create({ title: name, permission: rules })
359374
return result.data?.id

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ function App() {
250250
})
251251
local.model.set({ providerID, modelID }, { recent: true })
252252
}
253-
if (args.sessionID) {
253+
// Handle --session without --fork immediately (fork is handled in createEffect below)
254+
if (args.sessionID && !args.fork) {
254255
route.navigate({
255256
type: "session",
256257
sessionID: args.sessionID,
@@ -268,10 +269,36 @@ function App() {
268269
.find((x) => x.parentID === undefined)?.id
269270
if (match) {
270271
continued = true
271-
route.navigate({ type: "session", sessionID: match })
272+
if (args.fork) {
273+
sdk.client.session.fork({ sessionID: match }).then((result) => {
274+
if (result.data?.id) {
275+
route.navigate({ type: "session", sessionID: result.data.id })
276+
} else {
277+
toast.show({ message: "Failed to fork session", variant: "error" })
278+
}
279+
})
280+
} else {
281+
route.navigate({ type: "session", sessionID: match })
282+
}
272283
}
273284
})
274285

286+
// Handle --session with --fork: wait for sync to be fully complete before forking
287+
// (session list loads in non-blocking phase for --session, so we must wait for "complete"
288+
// to avoid a race where reconcile overwrites the newly forked session)
289+
let forked = false
290+
createEffect(() => {
291+
if (forked || sync.status !== "complete" || !args.sessionID || !args.fork) return
292+
forked = true
293+
sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => {
294+
if (result.data?.id) {
295+
route.navigate({ type: "session", sessionID: result.data.id })
296+
} else {
297+
toast.show({ message: "Failed to fork session", variant: "error" })
298+
}
299+
})
300+
})
301+
275302
createEffect(
276303
on(
277304
() => 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export const TuiThreadCommand = cmd({
6464
type: "string",
6565
describe: "session id to continue",
6666
})
67+
.option("fork", {
68+
type: "boolean",
69+
describe: "fork the session when continuing (use with --continue or --session)",
70+
})
6771
.option("prompt", {
6872
type: "string",
6973
describe: "prompt to use",
@@ -73,6 +77,11 @@ export const TuiThreadCommand = cmd({
7377
describe: "agent to use",
7478
}),
7579
handler: async (args) => {
80+
if (args.fork && !args.continue && !args.session) {
81+
UI.error("--fork requires --continue or --session")
82+
process.exit(1)
83+
}
84+
7685
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
7786
const baseCwd = process.env.PWD ?? process.cwd()
7887
const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
@@ -150,6 +159,7 @@ export const TuiThreadCommand = cmd({
150159
agent: args.agent,
151160
model: args.model,
152161
prompt,
162+
fork: args.fork,
153163
},
154164
onExit: async () => {
155165
await client.call("shutdown", undefined)

0 commit comments

Comments
 (0)