Skip to content

Fork this chat — branch conversations on both backends, with full UI - #416

Open
pufit wants to merge 1 commit into
mainfrom
pufit/fork-chat
Open

Fork this chat — branch conversations on both backends, with full UI#416
pufit wants to merge 1 commit into
mainfrom
pufit/fork-chat

Conversation

@pufit

@pufit pufit commented Aug 28, 2026

Copy link
Copy Markdown
Member

What

A complete "fork this chat" feature: branch any conversation into a new independent chat — from its end or from any earlier message — on both agent backends (Claude and Codex).

The backend already had fork scaffolding (POST /api/sessions/fork, WS fork, codex thread/fork + lastTurnId), but Claude couldn't fork mid-conversation, forks started visually empty, invalid forks failed only on their first message, and nothing in the UI exposed any of it.

Backend

  • Claude mid-point forks: ClaudeClient now records each turn's last main-chain transcript-entry uuid and emits it as TurnCompleted.native_turn_id (exact codex parity — same column, same mapping helper). Message-anchored forks pass it as resume_session_at alongside fork_session (gated on spec.fork so it can never truncate the original in place).
  • Display parity: fork_session() copies the source's message rows up to the fork boundary into the fork (db.copy_messages_to_session) — the forked chat opens showing exactly the history the agent remembers. Copies keep created_at and native_turn_id (forks are re-forkable) and get external_id="forkcopy:<src id>" for provenance.
  • Fail fast, honestly: fork requests are validated at the API call (404 unknown source; 409 no native conversation yet / no turn mapping at the anchor) instead of erroring on the fork's first message. The engine-side missing-mapping refusal is now backend-agnostic — a message fork must never silently carry the full context.
  • Symmetric fork-window guard in PATCH /api/sessions/{id}: un-nesting a not-yet-started fork is refused (it would silently discard the pending fork context).

UI/UX

  • Hover toolbar on every message (Slack-style, floats top-right of the reading column, zero layout shift): copy message + "fork from here". The fork action appears only where an anchor exists (native_turn_id present in the transcript), so it never promises a branch it can't make.
  • Header "⑂ Fork" button + ⌘⇧F — fork the whole chat; shown once the session has a native conversation.
  • Sidebar kebab → Fork on any loaded session row.
  • Fork provenance banner in a forked chat — "Forked from {parent}", one click back to the source; forks also get a violet branch icon in the sidebar and nest under their source (existing parent_session_id nesting).
  • Branch pills at divergence points: messages that have forks hanging off them show ⑂ <fork title> pills linking to each branch.
  • Post-fork flow: instant fork → navigate into the new chat (push, so Back returns to the source) → copied history + banner orient you immediately.

Verified

  • pytest tests/ — 3367 passed (new coverage: copy boundaries incl. user-message anchors, unmapped-anchor refusal, no-conversation refusal, claude uuid capture incl. sidechain exclusion, per-turn anchor reset, resume_session_at gating).
  • npm run build + vitest (229 passed; the one failing suite is a pre-existing collection failure on main), eslint clean on touched files.
  • Live E2E probe of the risky seam with the real Claude CLI: seeded two turns, forked at turn 1's live-captured uuid — the fork recalled turn 1's content and had genuinely dropped turn 2's, in a new session id, source untouched.
  • Visual pass in the running UI (header button, hover toolbars, gating on sessions without anchors).

Notes

  • Sessions predating per-turn anchor recording (all existing Claude chats) can be forked whole; per-message forks light up as new turns complete. Codex rows have carried native_turn_id since v039.
  • No schema change — uses the existing native_turn_id column and v039 indexes.

Generated by Nerve

- claude: record per-turn transcript uuid as native_turn_id, pass
  resume_session_at for message-anchored forks (codex parity)
- fork_session: validate anchor + native conversation up front, copy
  source messages up to the fork boundary for display parity
- UI: header Fork button, hover fork-from-here on messages, sidebar
  kebab Fork, fork provenance banner, branch pills at divergence
  points, cmd+shift+F
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