Skip to content

Tell a forked session apart from the one it came from - #53

Merged
devcodes9 merged 4 commits into
mainfrom
feat/fork-aware-sessions
Sep 4, 2026
Merged

Tell a forked session apart from the one it came from#53
devcodes9 merged 4 commits into
mainfrom
feat/fork-aware-sessions

Conversation

@devcodes9

@devcodes9 devcodes9 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Forked sessions were invisible. Claude Code forks a conversation by copying the transcript into a new file under a new session id and records nothing that says so, so the two branches sat in the list as unrelated rows with the same title, the same project and the same opening prompt. Picking the wrong one resumes a branch missing everything that happened after the split.

the two branches, one marked fork

The list now marks the fork, and the preview and read headers name the branch it came from and the message it split at.

How it detects one. Copied messages keep the uuids they had in the original, which is the only trace the format leaves. Sessions whose first message shares a uuid are one conversation branched in two. Direction is decided at the message where two branches stop agreeing: whichever carried on first is the one that was there to be copied. Length cannot be the signal, because an original that was forked from and then abandoned is the shorter of the two.

Where the mark sits. Ahead of the title, in the list and in the preview and read headers alike. The list pane is 42% of the terminal and the row's fixed prefix is 42 columns, so a mark trailing the title needed a terminal over 200 columns wide to render at all, and a fork carries the same long title as the session it came from, which is exactly the row whose title gets cut. Leading the title costs nothing on rows that are not forks, and every title still starts in the same column so the marks line up to be scanned. A word rather than a glyph: and are both unreadable at 14px.

Cost. build_index now stores each session's first-message uuid in index.json and carries it across cache hits, since it never changes. That is one extra partial read per new transcript, and only the handful of sessions sharing a fingerprint are read in full. Against 632 local sessions: 0.16s on a cold cache, nothing on a warm one, three forks found.

Scope. Claude Code only; Codex has no equivalent. Ranking is untouched, so a fork and its original still compete as two results. Whether one should suppress the other is a separate question and needs real usage first.

Eight detection cases and four display cases in tests/test_fork_detect.py, including fork-of-a-fork attribution, the abandoned-original case, and a regression test that the mark stays ahead of the title. Full suite: 112 pass.

On the screenshot. Recorded against the synthetic demo corpus at a 160 column terminal, not a real transcript, with a fork fixture added to the generator. docs/fork.png is a separate commit so it can be dropped before merge if you would rather the repo not carry it.

Claude Code forks a conversation by copying the transcript into a new file
under a new session id and records nothing that says so. The two branches then
sit in the list as unrelated rows carrying the same title, the same project and
the same opening prompt, and resuming the wrong one silently drops everything
that happened after the split.

The only trace the format leaves is that copied messages keep the uuids they
had in the original, so a shared first-message uuid is the fingerprint. Which
branch is the original is decided at the message where two branches stop
agreeing: whichever carried on first is the one that was there to be copied.
Length cannot be the signal, because an original that was forked from and then
abandoned is the shorter of the two.

Detection runs at index time over the roots build_index now carries in
index.json, and only reads the handful of files that share a fingerprint.
Against 632 local sessions that is 0.16s of root scanning on a cold cache,
nothing on a warm one, and it finds three forks.
Trailing the title, the mark needed a terminal over 200 columns wide to render:
the list pane is 42% of the terminal, the row's fixed prefix is 42 columns, and
a fork carries the same long title as the session it came from, so the title was
already being cut off well before the mark. The screenshot in this PR only
showed it because the recording was 260 columns.

It now leads the title. Every title starts in the same column so the marks still
line up to be scanned, and rows that are not forks pay nothing. A word rather
than a glyph because ⑂ and ⋔ are both unreadable at 14px.
The list leads the title with the mark; the preview carried the same fact last
on a line that opens with the project and the date. Same fact, different place
in each view, and the wrong end of the line for a flag. The clause naming the
original and the split point stays where it is, under the flag.

Also drops a comment claiming HEADER_LINES constrains the preview header to five
lines. Nothing computes from HEADER_LINES any more.
@devcodes9
devcodes9 merged commit d523742 into main Sep 4, 2026
6 checks passed
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