Skip to content

Implicit fork + resume for chat and agent sessions - #155

Draft
big-guy wants to merge 2 commits into
ness-dev:mainfrom
big-guy:session-resume-and-fork
Draft

Implicit fork + resume for chat and agent sessions#155
big-guy wants to merge 2 commits into
ness-dev:mainfrom
big-guy:session-resume-and-fork

Conversation

@big-guy

@big-guy big-guy commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Opening a new agent surface — a Chat (json-claude) tab or an xterm Claude/Codex tab — now branches off or resumes a prior session implicitly, driven by what's already open in the worktree:

  • A same-kind agent tab already open → fork from the active one (tip).
  • No agent tabs of any kind open → resume the worktree's last known session.
  • Otherwise → blank.

Grouping is by agent kind, not tab surface: Chat and xterm Claude share ~/.claude/projects, so they fork from / resume each other; Codex tabs form their own group. Fork uses each agent's native mechanism (Claude --fork-session, Codex fork <id>); the agent mints the new session id and we bind it to the tab via the existing hook/init discovery path.

Design

The crux is decoupling the tab id from the agent session id (they were forced equal). The tab id stays the stable slice/instance/React key; the on-disk session id lives in TerminalTab.sessionId (persisted, late-bindable for forks) and JsonClaudeInstance.claudeSessionId. This lets a forked session own its own transcript without breaking tab identity, and survives reload.

  • session-birth.ts — pure, dependency-injected decision logic (resolveTabBirth / deriveSpawnSpec / agentKindOfTab) + SessionSpawnSpec.
  • agentsAgentSpawnOpts.forkFromSessionId + AgentModule.listSessions; Claude/Codex buildSpawnArgs emit fork flags.
  • json-claude-manager — spawn-spec drives --session-id/--resume/--fork-session; the forked session_id is captured from the init event onto the tab.
  • index.ts — per-kind birthDeps via getAgent(kind); createChatTab/createAgentTab + jsonClaude:addChatTab / agent:addTab IPCs.
  • panes-fsm — first-tab resume-last for chat and xterm (bypassed for prompted/teleport launches).
  • rendereraddChatTab/addAgentTab backend methods; transient forkFromSessionId threaded through XTerminal.

Behavior note

Because grouping is cross-surface, a worktree with only an xterm Claude tab open → a new Chat now forks it (previously: blank). Intentional, and matches "fork when another Claude tab is open."

Testing

tsc -b, electron-vite build, and the full vitest suite (1464 tests) pass. New coverage: session-birth (chat+codex birth, cross-surface fork, resume excludes open ids), json-claude-manager (spawn-spec flags + fork id discovery), Claude/Codex buildSpawnArgs fork/resume/blank.

Caveat: Claude --fork-session was verified end-to-end (new session id minted, fresh self-consistent transcript, source untouched). The Codex fork path is covered by unit tests against the documented codex fork <id> syntax but not run live — worth a manual smoke before relying on Codex forking.

🤖 Generated with Claude Code

Opening a new agent surface (a json-claude chat tab or an xterm
Claude/Codex tab) now branches off or resumes a prior session implicitly,
driven by what's already open in the worktree:

  - A same-kind agent tab already open → fork from the active one (tip).
  - No agent tabs of any kind open      → resume the worktree's last known
                                          session.
  - Otherwise                           → blank.

Grouping is by agent KIND, not tab surface: chat and xterm Claude share the
~/.claude/projects store, so they fork from / resume each other; Codex tabs
form their own group. Fork uses each agent's native mechanism (Claude
--fork-session, Codex `fork <id>`); the agent mints the new session id and we
bind it to the tab via the existing hook/init discovery path.

Key design: the tab id and the agent session id are decoupled (they were
forced equal). The tab id stays the stable slice/instance/React key; the
on-disk session id lives in TerminalTab.sessionId (persisted, late-bindable
for forks) and JsonClaudeInstance.claudeSessionId. This lets a forked session
own its own transcript without breaking tab identity, and survives reload.

- session-birth.ts: pure, dependency-injected decision logic
  (resolveTabBirth / deriveSpawnSpec / agentKindOfTab) + SessionSpawnSpec.
- agents: AgentSpawnOpts.forkFromSessionId + AgentModule.listSessions;
  claude/codex buildSpawnArgs emit fork flags.
- json-claude-manager: spawn-spec drives --session-id/--resume/--fork-session;
  fork session_id captured from the init event onto the tab.
- index.ts: per-kind birthDeps via getAgent(kind); createChatTab/createAgentTab
  + jsonClaude:addChatTab / agent:addTab IPCs.
- panes-fsm: first-tab resume-last for chat and xterm (bypassed for
  prompted/teleport launches).
- renderer: addChatTab/addAgentTab backend methods; transient
  forkFromSessionId threaded through XTerminal.

Tests: session-birth (chat+codex birth, cross-surface fork, resume excludes
open ids), json-claude-manager (spawn-spec flags + fork id discovery),
claude/codex buildSpawnArgs fork/resume/blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@big-guy
big-guy force-pushed the session-resume-and-fork branch from b32559e to 964c574 Compare June 5, 2026 02:25
@big-guy big-guy self-assigned this Jun 5, 2026
When an agent tab exits, the restart overlay's button now auto-focuses
on the visible pane, so Enter or Space (native button activation)
starts a new session without a mouse. Gated on `visible` so hidden
tabs' overlays don't steal focus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 5786b28)
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.

1 participant