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
+4 -1
View File
@@ -46,8 +46,10 @@ ACTION_LIST_COLUMNS = (
models.Action.variant_index,
# SP9: the pager's key. Deferred, it would be a lazy load per row — a query
# behind every message on the page, which is the whole thing `load_only`
# is here to stop.
# is here to stop. `branch_id` rides along for the same reason: the pager
# reads it to tell a local step from a branch switch.
models.Action.parent_id,
models.Action.branch_id,
models.Action.created_at,
)
@@ -1014,6 +1016,7 @@ def list_variants(
index=i,
text=row.text,
reasoning=row.reasoning,
branch_id=row.branch_id,
created_at=row.created_at.isoformat() if row.created_at else None,
active=row.live,
)
+8
View File
@@ -200,6 +200,11 @@ class ActionOut(ORMModel):
# pre-SP9 pair and SP8 drops them.
take_count: int = 1
take_index: int = 0
# Which line this node is on, so the pager can tell the two kinds of step
# apart without asking the server first: a take on this branch is a leaf
# with nothing under it, and showing it is a local matter; a take on another
# branch has a story of its own, and going there is a branch switch.
branch_id: int | None = None
created_at: datetime
@@ -211,6 +216,9 @@ class VariantOut(BaseModel):
index: int
text: str
reasoning: str | None = None
# See ActionOut.branch_id: it decides whether choosing this take is a local
# step or a branch switch.
branch_id: int | None = None
created_at: str | None = None
active: bool = False