Răsfoiți Sursa

feat(tui): add Claude Code-style --fork flag to duplicate sessions before continuing (resolves #11137) (#11340)

Ariane Emory 2 luni în urmă
părinte
comite
84c5df19c7

+ 19 - 4
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

+ 29 - 2
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,

+ 1 - 0
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({

+ 10 - 0
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)