Skip to content

feat(agent-session): fork an agent session into a child workspace from the sidebar - #24684

Draft
totigm wants to merge 61 commits into
stablyai:mainfrom
totigm:feat/fork-agent-session-child-workspace
Draft

totigm wants to merge 61 commits into
stablyai:mainfrom
totigm:feat/fork-agent-session-child-workspace

Conversation

@totigm

@totigm totigm commented Oct 2, 2026 •

Copy link
Copy Markdown

ELI5

You are chatting with Claude or Codex in a workspace and want to try something else without losing that line of work. Right-click the workspace, pick Fork Agent Session..., give the new branch a name, and Orca makes a new workspace nested under the current one. It starts from the exact same code, optionally including your uncommitted changes, and the same agent continues the same conversation there. The original workspace and its conversation are left untouched.

What Changed

Before: branching agent work took three manual steps:

  1. Create a workspace. It started from the project's default branch, not from the work you were on.
  2. Re-parent it with Set Parent Worktree....
  3. Start a new agent with no memory of the conversation.

The existing terminal Fork Agent Session... only pasted a capped copy of the terminal scrollback into a fresh agent. It created a top-level workspace from the branch tip and left uncommitted changes behind.

After:

  • Where it lives:
    • The workspace context menu has Fork Agent Session....
    • The terminal and native-chat menu item opens the same dialog, with that pane's session preselected.
    • The item is hidden for folder workspaces. It is disabled, with the reason shown inline, for detached, archived or bare worktrees.
  • The dialog: a session picker (only sessions that can be forked natively), the name, a Create as a child of … switch (on by default), a Bring uncommitted changes switch (shown only when there are changes, with counts), and Advanced → Start from to pick another base.
  • The fork:
    • Native conversation fork. Claude runs claude --resume <id> --fork-session. Codex runs codex fork -c tui.resume_cwd=current <id>. Both get a brand-new session id, so the parent's session keeps its own writer.
      • Verified with Claude Code 2.1.282: SessionStart (source fork), UserPromptSubmit and Stop all report the new id.
      • Other agents opened from a terminal still get the transcript as a draft, as before.
    • Exact base. The child starts at the parent's HEAD commit, re-read at submit time, rather than at a remote-tracking ref. Source Control in the child compares against the parent's branch.
    • Uncommitted changes, all or nothing, on the execution host (local, SSH relay, or remote orca serve behind a capability):
      • Tracked edits move with git stash create / git stash apply (binary-safe, Git ≥ 2.25). Untracked files are copied with the filesystem.
      • The parent is never modified.
      • Any collision, symlinked parent folder, or concurrent commit in the parent is refused before anything is written. A failure after the first write is rolled back.
      • A lost connection or a timeout is reported as "may be partially applied", never as "nothing happened" (per ssh-execution-boundary.md).
    • Setup and trust. The repo's setup script and default tabs run in the child. Before that, the user goes through the same orca.yaml trust confirmation as every other create path. Folder trust for the agent comes from the existing launch-time trust (feat(agents): pre-trust the folder wherever Orca starts an agent #23744).
    • Remote workspaces. On a remote Orca runtime, the agent tab is created on the host, as other launches already do.
  • Removed: the old terminal-only fork dialog and its dead code. The native session ownership guard now lets claude --resume X --fork-session through, but only as a single command it can fully parse. Chained, substituted or otherwise unparseable commands keep the guard.

Why

This is the stacked-work flow #10849 asks for, plus the part the terminal fork never had: the agent actually remembers the conversation.

Alternatives considered:

  • feat(worktrees): create a child workspace from the worktree context menu #9859 (child workspace from the menu). It solves the lineage and base, but the agent starts from zero. This PR covers the same entry point and adds native conversation forking and uncommitted changes.
  • Add first-class agent session forking #5609 (first-class fork system, CLI/RPC). It is much larger (about 11k lines) and adds a new runtime surface. This PR keeps every change in the renderer, except the carry operation, which runs on the host because it has to.
  • Patch-based carry (git diff + git apply). Rejected: stdin is text-only in both the local runner and the relay, and GIT_INDEX_FILE doesn't cross into WSL. stash create/apply moves the changes as git objects and is binary-safe.

How to review

98 files, but about 58% of the lines are tests and many production files are 1–10 line registrations. Suggested order, smallest to largest:

  1. Native fork (about 100 lines):
    • src/shared/agent-session-fork-argv.ts: the fork command per agent.
    • The mode: 'fork' option in src/shared/tui-agent-resume-startup.ts.
    • The --fork-session exemption in src/main/ai-vault/structured-session-ownership.ts.
  2. Fork flow (renderer):
    • src/renderer/src/lib/agent-session-fork-flow.ts runs the steps: trust/setup decision → create the child at the parent commit → carry → launch → activate. It's the best single file to read first.
    • Its helpers, each small: agent-session-fork-launch.ts (+ -launch-target, agent-fork-launch-platform), worktree-agent-fork-sessions.ts (which sessions can be forked), agent-session-fork-setup-decision.ts and agent-session-fork-source-repo.ts.
  3. UI and entry points:
    • src/renderer/src/components/agent-session-fork/ holds the dialog, its state hook, and the status probe.
    • sidebar/WorktreeForkAgentSessionMenuItem.tsx is the sidebar item (one line in the menu view).
    • terminal-pane/* now opens the shared dialog; the old dialog is deleted.
    • The modal is mounted in AppRootSurfaces.tsx.
    • CreateFromPicker.tsx and SettingsFormControls.tsx only gain optional a11y props, so existing callers are unchanged.
  4. Carry uncommitted changes (host-side, read top-down):
    • src/shared/working-tree-change-carry.ts is the whole algorithm; it is pure, with injected I/O and tested against real repos.
    • working-tree-change-carry-node-fs.ts holds the filesystem side.
    • Then one thin adapter per execution path:
      • local: main/git/source-control/carry-working-tree-changes.ts and the IPC handler in filesystem-git-index-handlers.ts
      • SSH: ssh-git-working-tree-provider.ts, plus relay/git-handler-working-tree-carry.ts
      • remote runtime: runtime-git-working-tree-carry-commands.ts and the git.carryWorkingTreeChanges RPC, behind a capability in protocol-version.ts
      • renderer client and web client: runtime-git-working-tree-carry-client.ts and web-git-carry-api.ts
    • The rest (preload, RPC params and catalog, method registration) is wiring.

The commits are split by topic in the same order, so the PR can also be reviewed commit by commit.

Linked Issue

Fixes #10849

Related:

Visual Proof

Screenshots use the Spanish locale; a video of the full flow follows.

Before After
Terminal-only fork dialog: top-level workspace, transcript as a draft Workspace context menu: Fork Agent Session...
(screenshot) (screenshot)
Dialog: name, child switch, uncommitted changes with counts, Advanced base
(screenshot)

Testing

  • I manually tested these changes locally. On macOS, pnpm dev, real Claude Code 2.1.282 and Codex 0.159.3:
    • Forked with and without uncommitted changes.
    • The child agent recalled the parent conversation.
    • The parent stayed intact.
    • Opened the dialog from the sidebar and from the terminal.
    • Opened it on a workspace with no session (branch-only).
    • Picked another base in Advanced (carry disabled with the reason).
  • Automated tests added/updated: 32 test files.
    • Carry core against real git repos: binary files, symlinks, nested repos, rename rollback, collisions, a concurrent commit in the parent, git add -N, size caps, and the source left untouched.
    • The relay handler and the IPC path checks.
    • The runtime RPC, the capability, and the web shim.
    • The native fork argv and quoting for POSIX, PowerShell and cmd.
    • The ownership guard against bypass shapes.
    • Session selection and identity checks.
    • The flow at every failure stage.
    • The dialog (happy-dom).
    • The context menu and the terminal entry points.
  • Gates on this branch:
    • pnpm typecheck, pnpm lint and check:code-quality:changed pass.
    • pnpm build:desktop builds the relay, the CLI, electron-vite and the web client. The mobile web bundle step was skipped locally because the mobile/ workspace dependencies aren't installed in my checkout; this PR doesn't touch mobile/, and CI covers it.
    • The full pnpm test passes for everything this PR touches. The only failures are in files it doesn't touch and fail the same on main in my environment: SQLite FTS5 under Node 22, real-binary integration tests, and cross-version tags missing in a fork.
  • Linux, Windows and SSH were not tested on real machines. They are covered by unit tests, all git commands are Git 2.25-safe, and all host work goes through the existing local, relay and runtime providers.

AI Disclosure

Built with Claude Code (Claude Opus 5.5, plus Claude Sonnet 5 for some implementation steps).

  • Process: a written design and plan, then per-task implementation with an independent AI review of each task. That was followed by a whole-branch review and a second three-area branch review; every finding was fixed and re-reviewed.
  • Accountability: I directed the design decisions, tested the feature by hand, and I am responsible for the change.

Review

The AI code review explicitly covered:

  • Cross-platform: paths via path.join, and quoting per shell (POSIX, PowerShell, cmd). The symlink copy is guarded and fails honestly on Windows without symlink privilege.
  • SSH/remote: the carry runs on the execution host via the relay and runtime RPC. The SSH timeout or lost contact maps to "uncertain". The runtime method is capability-gated, and the web client checks the capability first.
  • Agents: only Claude and Codex fork natively. Other agents keep the transcript fallback. Launch config reuse is identity-checked, and no resume claim is made on the source session.
  • Performance: selectors are scoped to the open dialog, and nothing runs on hot paths.
  • UI: it follows STYLEGUIDE.md and existing primitives (Dialog, Switch, Collapsible, CreateFromPicker). Strings are in all six locales.
  • Security: both carry roots are resolved as registered worktrees. No writes go through symlinked folders. The orca.yaml trust gate is kept, and the session ownership guard was not weakened.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Author

X: @totigm8

Checklist

  • This PR is small and focused. It is one feature end to end; commits are topic-separated for review.
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred). Local exceptions are listed under Testing; CI covers them.

🤖 Generated with Claude Code

totigm and others added 30 commits October 2, 2026 06:19
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… capability

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…space

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Give SshGitWorkingTreeProvider.carryWorkingTreeChanges a 120s timeout
(a carry can move up to 2,000 files / 200MB, past the mux's ~30s
default), and when the request times out or the connection drops,
report partially_applied with the error detail instead of apply_failed
- the host may have written some or all of the changes, so "nothing
was carried" would be a lie (docs/reference/ssh-execution-boundary.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…us before forking

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a "Fork Agent Session…" item to the single-worktree context menu,
above "Set Parent Worktree…", that opens the global fork dialog
pre-scoped to the right-clicked workspace. Hidden for folder
workspaces; disabled while deleting or when the worktree has no
branch, is archived, or is bare.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Fork Agent Session…" used the U+2026 ellipsis character; every sibling
label in this menu ("Set Parent Worktree...") and the existing terminal
item ("Fork Agent Session...") use three ASCII periods instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tive chat

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rtain

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
totigm and others added 30 commits October 2, 2026 06:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ollback inside the target

- Verify the stash's parent (or the source HEAD when nothing is tracked)
  still equals the target HEAD after `stash create`, so a commit made
  between the base check and the stash returns base_mismatch instead of
  silently dropping that commit's edits.
- A failing `stash create` (e.g. an intent-to-add entry) now returns
  apply_failed with git's message instead of throwing.
- Create the temporary stash with a fixed -c identity for older Git
  hosts without user.email.
- Roll back by removing carried paths before `reset --hard`, record an
  untracked copy only once it is written (or partially written), and
  never remove a path whose copy failed with EEXIST.
- Refuse to copy, remove or count as free any path behind a symlinked
  target directory: stash create does not record a tracked symlink
  replaced by a directory, so the copy used to write through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uncertain

The carry rejection classifier listed the lost-contact codes it knew and
called everything else "nothing written", so a manual disconnect, an
unreadable response or a plain transport error reported a clean failure
even though the host may have run the carry. Flip it to an allowlist of
refusals known to happen before dispatch (queue overload, unknown
method, invalid arguments, selector/auth refusals, a request released
before it was sent, an older SSH relay without the handler); anything
else is reported as possibly partially applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…us probe fails

The web client told the user the paired server needed an update whenever
its capability probe failed, including when the probe never reached the
host at all. Report an unreachable host separately; the update message is
kept for a host that answered without the carry capability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own command

Split a resume command line at shell separators and parse each invocation, so
a --fork-session flag only exempts the claude invocation it belongs to and a
chained plain resume of an owned session is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a pane

Only hand a fork the sleeping record's or launch registry's config when it
belongs to the same agent and provider session; a pane that ran Claude then
Codex otherwise forked Claude with Codex's command and env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aunch

Catch a thrown agent launch after the child workspace exists, record it as
agent-not-started and reseed the child, so the dialog reports the fork instead
of a failure that a retry would turn into a second workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the child's repo by host instead of by bare repo id when choosing the
fork command's platform and shell, so a local repo that shares an SSH repo's id
no longer makes an SSH fork get Windows quoting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… workspaces

When a remote Orca runtime owns the child workspace, create the fork tab
through the host like every other launch, with the fork command quoted for the
host's verified platform rather than the client's. The fork still makes no
resume claim, so the source session keeps its owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an applied stash type-changes a folder to a symlink, the copy guard
refused the next carried path but threw an untagged error, so the carry
recorded it as written; rollback then hit the same guard before
`reset --hard`, left the stash applied and reported partially_applied.
Tag the guard error with CARRY_SYMLINKED_ANCESTOR so, like EEXIST, it is
never recorded or removed and rollback restores the target. The
pre-write check now uses a dedicated findSymlinkedAncestor io hook and
names the blocked path in the target_dirty detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The disabled Fork Agent Session item put its reason in a title attribute,
which never shows because disabled menu items ignore the pointer. Wrap it
in a tooltip like the primary Delete Worktree item, and disable it with a
reason when the workspace's project record is missing, matching the
terminal entry's gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The not-carried toast named only a generic reason, so failures like a
carried path behind a symlinked folder or a failing git add -N gave the
user nothing to act on. The fork outcome now keeps the host's detail and
the toast shows it, truncated, on a second line under the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forking from a terminal pane without a recognised agent dropped its
captured context, so the dialog lost Copy context, and an empty
scrollback refused the fork outright. The modal data now keeps the
context with a null agent: the dialog offers Copy context for it but no
transcript launch. An empty pane opens the dialog for a branch-only fork
instead of the 'No terminal context to fork' error; the source checks
(floating terminal, folder, detached, archived, bare, unknown) still
refuse. The dialog tests share their helpers through a test fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ion appears

When a terminal pane forked by transcript reported its native session
while the dialog was open, the transcript option disappeared and the
selection fell to 'No agent'. A vanished transcript selection now moves
to that pane's native session, and the session picker ignores the empty
value Radix reports while the selected item is swapped out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames

The dialog kept saying 'Start a new branch from <workspace>' after
Advanced picked the project default or another ref; it now drops the
parent's name for any other base. Workspace names interpolated into the
description, the child switch label and the carry note now sit in
wrapping spans, so one long unbroken name no longer overflows the
dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dialog re-reads it

If the parent's status read at submit failed, the fork went out from the
branch with no changes carried even though the switch was on. It now
falls back to the dialog's last successful read; the carry still refuses
if HEAD moved since. A slow status read from when the dialog opened can
no longer overwrite the fresher one taken at submit. The reads move into
their own hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s replaced

The orca.yaml trust prompt can replace the fork dialog mid-flow, which
hid the remaining progress and let the user start a second fork of the
same workspace. Once the dialog is gone, the fork's stages now show in a
loading toast that is dismissed on success or turned into the error on
failure. A fork claims its source workspace until it finishes, and
another fork of that workspace shows an inline 'already being created'
error instead of starting; a dialog closed before the fork starts frees
the claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fork menu item now looks up its project through the store's repos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…parse

A command with command substitution or an unquoted backslash escape can run
invocations the separator split does not see, so such a command no longer gets
the --fork-session exemption and is also read whole, as before forks existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Send viewMode 'terminal' with a runtime-owned fork, as the host-published new
tab launch does, so a host that defaults Claude to chat view still opens the
fork's terminal command in a terminal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch config

Check a fork's registry launch config with the store's own stale-proof rules,
so a config tied to a launch token or terminal handle the session cannot prove
falls back to the user's defaults instead of another launch's account env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ession

Forking from an empty terminal pane, or one whose agent is unknown, left
nothing for that pane in the session list, so the dialog preselected the
first other pane's session. With exactly one such session the picker was
hidden too, and submitting silently forked a different conversation. A
pane with nothing to fork now preselects 'No agent' and always shows the
picker, so choosing another pane's session is deliberate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arrow keys skip disabled menu items, so the tooltip added for the
disabled Fork Agent Session item never reached keyboard or screen-reader
users. The reason is now a muted second line inside the item, as in the
review-notes send menu, and is part of its accessible text; the tooltip
is gone. The item also decides 'folder workspace, hide it' from the same
host-aware project lookup as the terminal entry, instead of the card's
bare-id repo, so the now-unused repo prop is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p exemption

Treat a backslash or cmd caret as unmodelable only before a quote or
separator, so a plain Windows override path keeps the fork exemption. Also
withhold the exemption for ANSI-C quoting, process substitution, a backslash
quote inside double quotes, an unterminated quote, and any chained command,
while substitution inside single quotes stays literal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut a cwd prompt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lder trust

Upstream stablyai#23744 moved agent folder trust out of the renderer: the
agentTrust:markTrusted IPC and preflightAgentTrust are gone, and the PTY spawn
builders (and the relay on SSH hosts) now pre-trust the workspace for any fresh
launch that declares its launchAgent, behind the agent-wide setting.

The fork launcher still called the deleted preflight. Drop it for both the
native and transcript forks; both already declare launchAgent on the tab, the
pending startup, the runtime-host terminal and launchAgentInNewTab, so the
child worktree is trusted on the host that runs the agent, exactly like every
other Orca agent launch. The worktreePath and connectionId the native launcher
took only for the preflight go with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mable agents

Upstream added codebuddy, qoder and dsh to RESUMABLE_TUI_AGENTS, so the
exhaustive switch no longer typechecked. None has a native fork command yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Create a child workspace from the worktree context menu, based on the parent's branch

1 participant