Put the pager back, and let any turn be played again

SP7 replaced the pager with chips, on the grounds that a chip could also offer
"take this path" while a pager could only step. Driving it by hand said
otherwise, and the reason is worth keeping: the chip meant two different
things depending on where the reader was standing -- a real switch at the tip,
a preview needing a second button above it further back. Two meanings in one
control is what made the tree unusable.

So: one control that does one thing. Stepping reads a take and nothing else,
and it tells the server nothing, because reading is not a decision. The
transcript below a take that is not live simply ends -- such a take is a leaf
by construction, since whatever was played after the turn was played after the
take that *is* live. The decision is made by writing, and `after_id` carries it.

One step does reach the server and is still not a fork: a take with a story of
its own lives on its own branch, so going there is a branch switch and only the
server can say what is underneath. `branch_id` on the take is what tells the
two apart without asking first.

And a fork button on every turn but the opening. On the AI's it regenerates; on
your own it opens the text so you can say something else. What the story made
of the old take is kept, on the line it was written on.

`selectVariant` and `forkFromAttempt` leave the client. Both endpoints stay --
tested, and `stand_on` is shared with the write path -- but the pager needs
neither.

426 backend tests; lint and build clean. Not yet driven by hand: the frontend
still has no test runner, so this needs the `--keep` fixture and eyes, exactly
as SP7 did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dvvqn9ZDR4ixeFPHNbww7
This commit is contained in:
parththakkar106
2026-08-18 21:34:12 +05:30
committed by Parth
co-authored by Claude Opus 5
parent ea7336e5d3
commit f9b836fc0b
5 changed files with 219 additions and 146 deletions
+32 -33
View File
@@ -426,51 +426,50 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
}
.story .action-tools button:hover { color: var(--text); }
/* The attempts at one AI beat. Stays quiet until hovered — it's a footnote on
the message, not part of the prose. */
.attempts {
/* The takes of one turn: ‹ 2/4 ›. Stays quiet until hovered — it's a footnote
on the message, not part of the prose.
Small and unemphatic on purpose. Stepping through takes reads them and does
nothing else, so it should not carry the visual weight of a decision; the
decision is made by writing, which happens in the composer below. */
.take-pager {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
gap: 2px;
margin-top: 6px;
font-family: var(--font-ui);
opacity: 0.45;
transition: opacity 0.15s;
}
.story .action:hover .attempts,
.attempts:focus-within { opacity: 1; }
.attempt-chip {
padding: 3px 10px;
font-size: 0.73rem;
line-height: 1.5;
border-radius: 999px;
.story .action:hover .take-pager,
.take-pager:focus-within { opacity: 1; }
.take-pager button {
width: 20px;
height: 20px;
padding: 0;
font-size: 0.9rem;
line-height: 1;
color: var(--text-dim);
background: transparent;
border: 1px solid var(--border);
border: 1px solid transparent;
border-radius: 4px;
}
.attempt-chip:hover:not(:disabled) { color: var(--text); border-color: var(--border-bright); }
.attempt-chip[aria-pressed="true"] {
color: var(--bg);
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
.take-pager button:hover:not(:disabled) {
color: var(--text);
border-color: var(--border-bright);
}
.attempt-chip:disabled { opacity: 0.35; cursor: default; }
/* Dashed until hovered: taking a path the story moved past creates a branch,
so it should not look like the same weight of click as browsing one. */
.take-path {
padding: 3px 10px;
font-size: 0.73rem;
line-height: 1.5;
color: var(--accent);
background: transparent;
border: 1px dashed var(--accent-dim);
.take-pager button:disabled { opacity: 0.3; cursor: default; }
.take-count {
min-width: 30px;
text-align: center;
font-size: 0.72rem;
font-variant-numeric: tabular-nums;
color: var(--text-dim);
}
.take-path:hover:not(:disabled) { color: var(--accent-bright); border-style: solid; }
.take-path:disabled { opacity: 0.35; cursor: default; }
.attempt-note {
margin-left: 2px;
/* Only while a take that isn't the live one is on screen: it says what the
next thing typed will do, because that is the click that forks. */
.take-note {
margin-left: 6px;
font-size: 0.72rem;
font-style: italic;
color: var(--accent-dim);