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
+20 -8
View File
@@ -105,10 +105,15 @@ export const api = {
// attempts themselves are fetched when the reader actually pages through.
listVariants: (advId, actionId) =>
request(`/adventures/${advId}/actions/${actionId}/variants`),
selectVariant: (advId, actionId, index) =>
request(`/adventures/${advId}/actions/${actionId}/variant`, {
method: 'POST', body: JSON.stringify({ index }),
}),
// The same endpoint under the name the pager uses. "Take" is what the UI
// calls one of these now, and the vocabulary is worth keeping straight —
// `variant` belongs to the pre-tree pair of columns SP8 drops.
listTakes: (advId, actionId) =>
request(`/adventures/${advId}/actions/${actionId}/variants`),
// No `selectVariant` / `forkFromAttempt` here any more. Both endpoints still
// exist and are tested, but the pager needs neither: stepping between takes
// tells the server nothing, and what used to be "take this path" is now
// whatever the reader writes next, carried by `after_id` on the turn itself.
// The story tree (Phase 14). One request draws the whole shape however many
// forks there are. The three that change it answer with the story as it now
@@ -122,11 +127,18 @@ export const api = {
}),
deleteBranch: (advId, branchId) =>
request(`/adventures/${advId}/branches/${branchId}`, { method: 'DELETE' }),
// Take the story down one attempt. A fork only when it has to be: while the
// attempts are still at the tip they are leaves, and the server switches.
forkFromAttempt: (advId, actionId) =>
request(`/adventures/${advId}/actions/${actionId}/fork`, { method: 'POST' }),
// Play a turn again, differently (SP9). An AI turn regenerates; a player's
// own takes the text given. Streams, because it is a turn like any other.
//
// Reaches any turn, not only the newest — which is the whole difference from
// `retry`, and the reason the pager can offer this on every message.
addTake: (advId, actionId, text, handlers, signal) =>
streamSSE(`/adventures/${advId}/actions/${actionId}/takes`, { text }, handlers, signal),
// `afterId` names the take the turn is played after. Omitted it means the
// tip, which is every ordinary turn. Naming a take the story moved past is
// what forks a branch — stepping between takes to read them does not, and
// the server is never told about it.
sendAction: (advId, payload, handlers, signal) =>
streamSSE(`/adventures/${advId}/actions`, payload, handlers, signal),
retry: (advId, handlers, signal) => streamSSE(`/adventures/${advId}/retry`, {}, handlers, signal),