feat(sessions): fork a session — the what-if branch - #67
Conversation
The largest designed-but-unbuilt piece of the triad. §313: "resume a copy from
any point, leaving the original intact — the what-if-I'd-told-it-to-do-X-instead
branch." Fork-from-END, as §411 settles the scoping: per-turn fork needs a
transcript picker, which is separate work.
WHICH EVENTS TRAVEL is the entire design, and the answer is not "all of them":
user / agent / assistant YES — this is the conversation being forked
title YES, then overridden, so the fork is not a second
row with an identical name
end NO — that is the ORIGINAL's terminal state. Copying
it renders a live fork as `done` while you type
label NO — lifecycle and pinning are the original's. A
fork of an archived session must arrive visible, and
must not silently take a second pin
The copy records `forkedFrom` in its meta — §4 provenance, and what a later
branch-graph draws from. The key is written ONLY on a fork, so every existing
session file stays byte-identical and an older build reading a forked session
simply does not know it was forked rather than failing to read it.
A fork IS a resume, into a copy, so the handler calls resumeSession() on the new
id rather than duplicating the replay, the budget planning, or the honest
"resumed from a summary" note.
The sixth card button: labels now collapse below 420px (chat) / 400px (panel),
up from 360/340 — six labelled buttons need ~370px against five at ~300px, and
the row is nowrap so overshoot overflows rather than wraps. The guard added with
export already covered this; only the threshold moved.
Tests: 8 in sessions.test.js against a fixture that is sealed AND archived AND
pinned — every state a fork must not inherit. Verified non-vacuous:
copy the end event 20/25 copy label events 20/25
drop the (fork) suffix 21/25 drop provenance 22/25
do not go live 23/25
All 32 suites green.
One test bug worth recording: `m.list().find(e => e.id === m.fork(id))` calls
fork() once per row against a list fetched before any fork existed. Hoisted.
There was a problem hiding this comment.
Pull request overview
Adds “fork session” support to the LevelCode AI sessions subsystem: creating a new live session that copies the source conversation while intentionally not inheriting terminal/lifecycle metadata, enabling “what-if” branching without mutating the original.
Changes:
- Implement
sessions.fork()to create a new session seeded from an existing session’s transcript, with provenance recorded asmeta.forkedFromand withend/labelevents excluded. - Add a new Fork action to session cards across both webviews and route the action through
extension.jsto reuse the existing resume path. - Update experience docs and expand/adjust tests to cover event-selection rules, provenance, and UI action presence.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| extensions/levelcode-ai/test/webviewCss.test.js | Updates the session-card narrow-pane guard test for the action row; currently needs tightening to assert the intended 420/400 collapse thresholds. |
| extensions/levelcode-ai/test/sessionsUi.test.js | Updates UI unit test to expect the new fork action in the session-card action row. |
| extensions/levelcode-ai/test/sessions.test.js | Adds fork-focused lifecycle tests (state/lifecycle/pin isolation, provenance, “fork becomes live”, etc.). |
| extensions/levelcode-ai/sessionStore.js | Extends session meta line creation to optionally include forkedFrom only when forking. |
| extensions/levelcode-ai/sessions.js | Adds fork(id) implementation and exports it from the sessions manager. |
| extensions/levelcode-ai/media/sessionsView.html | Adds Fork action button and updates label-collapse container-query threshold (now 400px). |
| extensions/levelcode-ai/media/chat.html | Adds Fork action button and updates label-collapse container-query threshold (now 420px). |
| extensions/levelcode-ai/extension.js | Handles sessionAction: fork by forking then resuming the new session id. |
| docs/levelcode-sessions-experience.md | Marks fork-from-end as shipped and documents the event-selection/provenance rules. |
Suppressed comments (1)
extensions/levelcode-ai/test/webviewCss.test.js:221
- This CSS guard test is now too permissive and the inline rationale is outdated. The comment still talks about “five labelled buttons”, and the regex allows any 3xx/4xx collapse width, which wouldn’t catch an accidental regression away from the intended 420px (chat.html) / 400px (sessionsView.html) thresholds described in this PR.
test('SESSION CARD: six nowrap action buttons cannot overflow a narrow pane', () => {
// The card is two fixed-height lines and the action row is `flex-wrap: nowrap`, so buttons that
// do not fit do not wrap — they overflow. Five LABELLED buttons need roughly 300px; a sidebar is
// routinely narrower. The labels therefore have to disappear before that happens.
//
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…is-commented Review flagged the comments: they still said "a fifth button" and "five labelled buttons ... ~300px" next to a row that now renders six. Correct — and going to fix the wording turned up a real bug behind it. MEASURED the shipped styling in headless Chrome, six labels rendered: chat.html 434px padding 0 9px, 11.5px text, gap 6, 1px border sessionsView.html 363px padding 0 8px, 11px text, gap 4, no border The thresholds were 420 and 400. So chat.html had a 14px band — content widths between 420 and 434 — where the labels were still painted and the nowrap row spilled out of the card. Exactly the failure the guard was added to prevent, reintroduced by raising the button count without re-measuring. Now 460 and 390, both above the need with room for a wider system font. The comments are rewritten with the real numbers and a note to re-measure when adding a button. The test is rewritten to assert the RELATIONSHIP rather than a regex shape. It now parses the threshold and requires it to be >= the measured need, so a seventh button fails until someone re-measures, and the message says why: chat.html: labels collapse at 420px but six labelled buttons need 434px — between those widths the labels paint and the nowrap row overflows the card Boundary verified: 433 rejected, 434 accepted, and the 420 that shipped rejected. All 32 suites green.
|
Both correct, fixed in 5cf3f13 — and going to fix the wording turned up a real bug behind it. I measured the shipped styling in headless Chrome with all six labels rendered:
So The comments are rewritten with the real numbers and a note to re-measure when adding a button. The more useful partThe test was asserting a regex shape ( So a seventh button fails until someone re-measures, and the message says why rather than pointing at a magic number. Boundary verified: 433 rejected, 434 accepted, and the 420 that shipped in this PR rejected. All 32 suites green. |
The largest designed-but-unbuilt piece of the triad. §313:
Fork-from-end, as §411 settles the scoping: per-turn fork needs a transcript picker, which is separate work.
Which events travel is the entire design
And the answer is not "all of them":
user/agent/assistanttitleenddonewhile you type into itlabelVerified against a fixture that is sealed and archived and pinned:
Provenance
The copy records
forkedFromin its meta — §4 provenance, and what a later branch-graph would draw from. The key is written only on a fork, so every existing session file stays byte-identical and an older build reading a forked session simply doesn't know it was forked rather than failing to read it. There's a test asserting an ordinary session never gains the key.A fork is a resume, into a copy
So the handler calls
resumeSession()on the new id rather than duplicating the transcript replay, the budget planning, or the honest "resumed from a summary" note.fork()has already made the copy live.The sixth card button
Labels now collapse below 420px (chat) / 400px (panel), up from 360/340. Six labelled buttons need ~370px against five at ~300px, and the row is
nowrapso overshoot overflows rather than wraps. The guard added with #65 already covered this — only the threshold moved.Tests
8 new in
sessions.test.js. Verified non-vacuous:endeventlabelevents(fork)suffixAll 32 suites green.
Still open in the triad
Per-turn fork (needs the transcript picker), the branch-graph visualisation, memory-set export, and the four docs still marked "Status: proposed".