Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Branchpoint

A local web app that wraps your installed Claude Code CLI. Conversations render as a tree of branches, and highlighting any span of an assistant message forks the session at that message into a new branch anchored to the span.

The problem it solves: a single Claude response often raises three separate technical threads at once, and a linear transcript gives you no way to pull them apart. Branchpoint gives each thread its own branch with its own clean context, which helps you keep track and keeps each branch's context window on one topic.

The spec is docs/PRD.md.


Prerequisites

  • Node 20.19+ (built and verified on 22.22.0).
  • Claude Code installed and logged in. Check with claude --version and claude /status. Verified against CLI 2.1.220. Branchpoint rides your existing CLI auth and never touches an Anthropic API key.

Install

npm install

Run

npm run dev

That one command starts both processes: the API on http://localhost:8787 and the UI on http://localhost:5273. Open http://localhost:5273.

Two environment variables, both optional:

Variable Default What it does
BRANCHPOINT_PORT 8787 API port. The Vite dev server proxies /api to it.
BRANCHPOINT_DATA ./data/branchpoint.json Where the branch tree metadata lives.

60-second walkthrough: making a fork

  1. Create a project. On first run you are asked for a name and a working directory, for example ~/code/my-repo. This directory is pinned and used for every call. It matters: Claude Code stores its sessions per directory, and a project pointed at the wrong one would silently open the wrong history.

  2. Start a conversation. Click + New conversation, type a question, press Enter. The reply streams in. Your CLAUDE.md, skills, MCP servers and permission settings all apply, because this is your own Claude Code doing the work.

  3. Ask a question about one topic. Something that produces two threads at once works best, for example: "In one short paragraph each, explain what backpressure means in stream processing and what a bloom filter is."

  4. Highlight a span of the answer. Select the sentence about bloom filters. A floating Ask about this ⑂ button appears above the selection.

  5. Click it and ask your follow-up. For example: "Why can it never produce a false negative?" Press Branch (or Cmd+Enter).

  6. You are now in a new branch. It appears nested under its parent in the sidebar, labelled from: <parent title>. It contains the conversation up to the message you forked from, then your quoted question, then an answer scoped to that span alone. The parent branch is untouched and now shows a marker on the message you forked from, linking to the child.


TUI (experimental)

A vim-like terminal front-end for the same conversations.

npm run tui

It is a plain HTTP client of the API, so it shows the same projects, branches and transcripts as the browser, backed by the same data/branchpoint.json. Either one can be open, or both at once. If the API is not already running the TUI starts it and stops it again on exit; BRANCHPOINT_PORT and BRANCHPOINT_DATA work the same as they do for the web app.

The left pane is the branch tree, the right pane is the transcript of the selected branch, and the bottom line is the mode and status bar. Forking works the way it does in the browser, except the span is selected with the keyboard: v starts a word-wise selection inside an assistant message, and the exact text you cover becomes the quote anchor of the new branch.

Install it as a command

cd tui && npm link

That puts a branchpoint command on your PATH, pointing back at this repo. Run it from wherever you are working:

cd ~/code/some-project
branchpoint

The directory you are standing in is the conversation's directory. On the first run in a new one, Branchpoint creates a project named after the folder and pins it to the resolved path. Every run after that finds the same tree, because it matches on the path the server already resolved rather than on the text you typed. npm run tui from the repo root does exactly the same thing for this repo.

To undo it, run npm unlink -g @branchpoint/tui.

Sessions

Conversations are managed the way claude manages them, because that is the muscle memory the command sits in.

Command What it does
branchpoint Start a new conversation in this directory
branchpoint -c, --continue Reopen the most recently active conversation here
branchpoint -r, --resume Choose one from a list
branchpoint -r <id> Reopen one by id; a unique prefix is enough
branchpoint -l, --list Print the conversations here and exit
branchpoint -h, --help Usage
branchpoint --version Version

A conversation's id is a Claude Code session id, so the same id works everywhere:

$ branchpoint --list
Conversations in /Users/you/code/some-project

  5e0ebcc6    2m ago      6 msgs  why does the fork copy history
  f694ab5b  ⑂ 1h ago      4 msgs  backpressure is flow control

Reopen one with `branchpoint --resume <id>`, or the newest with `branchpoint -c`.

$ branchpoint -r 5e0e          # reopens it here
$ claude --resume 5e0ebcc6-…   # the same conversation, in the bare CLI

The id of the open conversation sits in the status bar, and s brings the resume list up from inside the TUI. --list shows only conversations that have been spoken to, and marks a forked one with . --help, --version and a bad flag answer without starting the API.

Abandoned conversations do not pile up. Claude Code writes nothing to disk until the first turn, so quitting a fresh claude leaves nothing behind. Branchpoint has to mint the session id before the first turn, so instead it marks the row and drops it: a conversation nobody spoke in is discarded when you quit, and any left by a crash are swept on the next run. The sweep is refused by the server for anything with a transcript or a fork hanging off it, and it never touches a conversation opened in the browser.

An alias is safe:

alias bp=branchpoint

Even alias claude=branchpoint is safe, if you like living that way. The SDK spawns the Claude Code executable by path and without a shell, so an alias defined in your interactive shell is never consulted and Branchpoint cannot end up calling itself.

Mode Key What it does
Normal Tab Move focus between the branch tree and the transcript
Normal j / k Next / previous branch or message
Normal g / G First / last branch or message
Normal Ctrl+d / Ctrl+u Half a screen of messages down / up
Normal Enter Open the selected branch (tree focus)
Normal i Write a message to the open branch
Normal n Start a new conversation in this project
Normal s Resume list: every conversation here, most recent first
Normal v Select a span of the message under the cursor
Normal r Reload branches and transcript
Normal 1 - 9 Switch project
Normal Esc Clear the status message
Normal q Quit
Any Ctrl+c Stop the turn that is streaming, or offer to quit when nothing is running
Visual h / l, b / w Previous / next word
Visual j / k Down / up one rendered line
Visual 0 / $ Start / end of the message
Visual v or Space Pin the anchor, then motions extend the selection
Visual Enter or f Fork on the selection
Visual Esc Back to normal
Resume j / k, g / G Move through the list
Resume Enter Open the conversation under the cursor
Resume n Start a new one instead
Resume Esc Back

Browsing, sending, forking and starting conversations all work. Renaming a branch and the glossary are browser-only for now.

The composer

i opens the composer, and forking from a selection opens the same one with the quoted span above it. It is a real modal editor rather than a line that only grows at the end: a long message can be navigated, corrected in the middle, and read from the top.

It grows with what is written until it would take half the pane, then scrolls. Once it scrolls, the divider carries the caret's row (14/52) and a marks the edge the text continues past. The window is allowed to run past the last line, so writing at the bottom of a long message leaves a few blank rows underneath instead of pinning the caret to the floor of the pane.

Both submodes are shown in the mode badge: INSERT and FORK are typing, EDIT and FORK EDIT are the composer's own normal mode, where the caret is a block sitting on a character.

Submode Key What it does
Insert Enter Send, or fork
Insert Ctrl+j, Alt+Enter, \ then Enter Newline, without sending
Insert Esc Into the composer's normal mode
Insert Arrows One character or one rendered row
Insert Alt+← / Alt+→, Alt+b / Alt+f One word
Insert Ctrl+a / Ctrl+e Start / end of the row
Insert Ctrl+w Delete the word behind the caret
Insert Ctrl+u / Ctrl+k Delete to the start / end of the line
Insert Ctrl+h, Ctrl+d Delete behind / under the caret
Edit h / l, j / k One character, one rendered row
Edit w / b / e Word forward, back, end
Edit 0 / ^ / $ Start, first non-blank, end of the row
Edit gg / G Start / end of the draft
Edit i a I A o O Back to typing, in the usual places
Edit x X D C s S The usual small edits
Edit dd cc, dw cw, db de d$ d0 dG Operator plus motion
Edit p / P Put back what was cut, after or before
Edit u, Ctrl+r Undo, redo
Edit Ctrl+e / Ctrl+y Scroll one row
Edit Ctrl+d / Ctrl+u Scroll half the pane
Edit zz / zt / zb Put the caret's row in the middle, at the top, at the bottom
Edit Enter Send, or fork
Edit Esc Leave the composer, keeping the draft

Leaving the composer keeps what was in it, and i picks it back up. Writing while an answer is streaming is allowed; Enter then queues the message and sends it by itself when the turn ends.


How it works

React + Vite (:5273)  ->  Express API (:8787)
                              |
                              v
              @anthropic-ai/claude-agent-sdk 0.3.220
                              |
                              v
                 your local Claude Code CLI + auth
                              |
                              v
        ~/.claude/projects/<encoded-cwd>/*.jsonl   (read-only)

Branchpoint stores almost nothing. A branch is a Claude Code session id plus a title and a fork point. The messages themselves are read from Claude Code's own session files every time, so there is exactly one source of truth for what was said and it is not ours.

Forking

Forking is one SDK call:

query({ prompt, options: { cwd, sessionId, resume: parentSessionId, resumeSessionAt: messageUuid, forkSession: true } })

Branchpoint generates the new session id itself and passes it in, so a branch has a stable identity from the moment it is created rather than only after the model answers.

Pass-through

The only options ever set are the ones identifying which session to talk to: cwd, sessionId, resume, resumeSessionAt, forkSession, and includePartialMessages for streaming. No system prompt, no model override, no tool configuration, no API key. Slash commands you type go through verbatim as prompt text.

Failures are shown, not swallowed

  • Working directory mismatch is the number one silent failure mode, so it is checked twice: once against the cwd Claude Code reports at session start, and once against the cwd recorded in the transcript itself. Either mismatch raises a banner.
  • A resume that quietly starts a new session (the symptom of a wrong cwd) raises its own banner naming both session ids.
  • Unreadable or unfamiliar lines in a session file are counted and reported rather than hidden, because that is how you find out the format drifted.
  • Permission denials surface as a notice. Branchpoint is a non-interactive host, so it inherits your permission settings and cannot prompt; a tool that would need approval is auto-denied by the CLI and says so.

Decisions

The PRD left several choices open. These are the calls made, and why.

Express, not Fastify. The API is eleven endpoints, two of which are server-sent-event streams. Express's plain res.write is the shortest path to SSE with no plugin and no framework opinion in the way.

A JSON file, not SQLite. The stored data is tree metadata only: a few dozen rows of ids, titles and fork points, with no queries beyond "list" and "find by id". A JSON file with atomic writes and a serialised write queue has zero native dependencies and can be read with cat when something looks wrong. It sits behind a Store interface in server/src/store.ts, so moving to SQLite means implementing that interface and changing nothing else.

The SDK's fork path, not the CLI fallback. The PRD sanctions shelling out to claude --resume ... --resume-session-at ... --fork-session if the SDK's fork is broken in the installed version. It is not: resume, resumeSessionAt, forkSession and a caller-supplied sessionId were all verified working against 0.3.220 before any code was written, including that the fork copies history up to the fork point, preserves message UUIDs, and leaves the parent file untouched. Branchpoint ships the SDK path. The SDK version is pinned exactly in server/package.json for this reason; if a future version regresses, the CLI fallback belongs in server/src/claude.ts and nothing else changes.

Every launch is a fresh conversation, as in claude itself. The command is used the way claude is used, so it behaves the way claude does: a bare run starts something new, and reopening an old conversation is asked for by name (-r <id>) or picked from a list (-r, -c). Reopening the last tree by default was the earlier behaviour and it was wrong: the first thing you did was hunt for a clean conversation to type into.

An abandoned conversation is discarded, not kept. Claude Code writes no session file until the first turn, so an abandoned claude leaves no trace. Branchpoint has to supply the session id up front, so the row is marked ephemeral and swept once it is clear nobody spoke in it: on quit, and again on the next launch for anything a crash left behind. The rule for what is safe to remove lives in the server (DELETE /api/branches/:id refuses anything with a transcript or a fork hanging off it) while the policy of when to sweep stays in the front-end, which is why the sweep cannot delete a conversation someone has open in the browser.

Branch id equals session id. The data model keeps both fields as the PRD specifies, but they hold the same UUID. Since Branchpoint supplies the session id to the SDK, there is never a window where a branch exists without a session, and no id mapping to get wrong.

A floating button, not a right-click menu. This is the PRD's own deviation from the original sketch and it is kept: a context menu would fight the browser's own.

Real paths are resolved once, at project creation. The CLI records the resolved path, so a project created as /tmp/x is stored as /private/tmp/x on macOS and its transcripts live under -private-tmp-x. Storing the unresolved path would have made every lookup miss.

Escape is a mode key and nothing else; Ctrl-C is the only stop. The TUI lets you write while an answer is streaming, which puts Escape in an impossible position: in the composer it has to mean "leave this mode", and outside it used to mean "kill the turn". One key that does two irreversible-feeling things depending on invisible state is not a key anyone can trust, so the two were split. Escape now only ever steps out of the innermost thing that owns it, and it keeps the draft on the way out. Ctrl-C stops the running turn from anywhere, and with nothing to stop it takes the terminal's other contract and offers to quit, but only on the second press, so a habitual Ctrl-C never costs a draft. This is why the TUI is rendered with Ink's exitOnCtrlC turned off.

The composer is a modal editor, not an input line. The rest of the TUI is vim-shaped, so the thing you spend the most time in should be too. INSERT and EDIT are two submodes of one buffer, shared by the message composer and the fork composer, with the caret drawn as a bar in one and a block in the other. The text model lives in tui/src/editor.ts and is pure, so motions, operators, wrapping and the scroll window are all unit-tested without a terminal; the keys that drive it are glue in App.tsx. It wraps its own text rather than reusing words.ts, because the transcript wrapper is free to collapse whitespace into columns and a composer is not: every offset has to be addressable and a run of spaces has to survive being typed.

A message written during someone else's turn is queued, not refused. Refusing it and keeping the draft was the simpler option, but it makes you press Enter twice and watch for the moment in between. The message waits and sends itself when the turn ends, to the branch it was written in rather than whatever is on screen by then. Ctrl-C drops the queue along with the turn.

Locating a transcript tolerates the encoding changing. The <encoded-cwd> directory name is a first guess; if the file is not there, every project directory is scanned for the session id. The encoding is undocumented and could change.

Deviations from the spec

Tool-call rendering is collapsed, and tool results are collapsed separately. The PRD asks for ran 3 tool calls ▸. Claude Code records the call and its result as two separate transcript entries, and the result arrives dressed as a user turn, so rendering it as a message bubble would put the model's own tool output in the user's voice. Both are collapsed chips instead, expandable in place.

The quote-anchor prompt renders as what you meant, not as what was sent. The first message of a forked branch is machine-written and contains two lines of boilerplate. It renders as the quoted span plus your question, with a show sent prompt toggle for the literal text. The text sent to the model is exactly the PRD's template, unchanged.

The glossary panel is built. The PRD lists it as a stretch goal. It asks the branch to define its own jargon, in a throwaway fork so the question never enters the branch's history.

Known limits

  • Forking copies history up to the fork point, so context cost grows with fork depth. Fine for v1, per the PRD, but a deep chain of forks is not free.
  • One turn at a time per branch. In the browser, sending again while a response is streaming cancels the first; in the TUI the second message is queued instead. Forking while a turn is streaming is refused in the TUI rather than cancelling it silently.
  • No merging, no comment nodes, no canvas, no auth, no deployment. Those are the v2 parking lot below.

v2 parking lot

Documented, deliberately not built.

  • Connecting two branches into a new node whose context is both paths with the shared ancestor prefix deduplicated. The hard parts are ordering and token budget, and it is the reason the data model stores a fork point rather than a flattened path.
  • Comment nodes on connected paths, where the model responds to the combined path.
  • Canvas view with draggable nodes, replacing the indented tree.

Development

npm run dev        # server and web
npm run tui        # terminal front-end, which needs the terminal to itself
npm test           # server and tui unit tests
npm run typecheck  # all three workspaces
npm run build      # production build of server and web

Layout

server/src/
  index.ts       Express app and all routes
  claude.ts      the only module that calls the SDK
  transcript.ts  the only module that parses .jsonl
  store.ts       Store interface + JSON implementation
  prompt.ts      fork prompt template and title heuristics
web/src/
  App.tsx        state and streaming orchestration
  components/    sidebar, conversation, message, composer, glossary, setup
  markdown.ts    markdown + syntax highlighting, raw HTML escaped
tui/src/
  cli.ts         argv to a launch intent: fresh, continue, resume, list
  sessions.ts    ordering, short ids and id matching for resume
  App.tsx        modes, key routing and streaming orchestration
  words.ts       word segmentation and wrapping that keeps source offsets
  layout.ts      transcript to drawable lines, scrolling window
  server.ts      starts the API if it is not already up, and stops it again

~/.claude/projects/*/*.jsonl is treated as read-only and as an internal format. All parsing lives in server/src/transcript.ts and nowhere else, so format drift is a one-file fix. Its tests deliberately feed it malformed JSON, unknown event types, unknown content blocks, duplicate UUIDs and subagent traffic.

About

Highlight-to-fork tree branching UI over the local Claude Code CLI - web app and vim-style TUI

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages