Split Play.jsx into a directory of twelve files

`frontend/src/pages/Play.jsx` was 2280 lines holding 27 components. It is now
`frontend/src/pages/Play/`: `index.jsx` with the page component, five panels,
two drawers, the reports, the take pager, the refresh dialog, and the shared
formatting helpers.

Every line moved verbatim. A coverage check confirms every non-blank line of
the original appears exactly once, in order, across the twelve files, and a
name-resolution check confirms every identifier each file references is defined
or imported there, with no unused imports.

The line split stranded a comment at six of the boundaries. A leading comment
sits above the section it describes, so each cut left one at the end of the file
before it. All six moved to the section they describe, rewritten in the house
style.

`usePlaySession.js` is not here. The page component still owns all of the
session state. Moving eighteen `useState` calls and seven `useEffect` calls is a
rewrite rather than a move, and no frontend test would catch a mistake in it
today, so it waits for Stage 5.

Four stand-in providers under `backend/tools/` now define `last_usage = None`.
The turn engine reads that attribute after every call, and the fixtures never
defined it, so `tools.tree_fixture` crashed. That break predates this branch.

Verified by 549 passing tests, a clean `npm run lint` and `npm run build`, and
by driving the Play screen: the story view, all five panels, both drawers
including the world-state edit form, the branch map, the refresh dialog, and the
take pager stepping onto a take that lives on another branch. No console errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
This commit is contained in:
parththakkar106
2026-08-29 01:33:14 +05:30
co-authored by Claude Opus 5
parent 3a9090c20a
commit 0623dd8b78
19 changed files with 2445 additions and 2282 deletions
+88
View File
@@ -0,0 +1,88 @@
// The pager that steps between the attempts at one coordinate: ‹ 2/4 ›, and
// nothing else.
//
// SP7 shipped chips instead, because a chip could also offer "take this path"
// where a pager can only step. Driving it by hand said otherwise. The chip
// meant two things depending on where you were standing: a real switch at the
// tip, or a preview that needed a second button above it. Two meanings in one
// control is what made the tree unusable.
//
// So the pager is back, and stepping is all it does. Stepping tells the server
// nothing, because reading a take is not a decision. You decide by writing
// below a take, and that is where the branch is created (SP9, `after_id`).
//
// One step does reach the server, and it is not a fork either. A take that has
// a story of its own lives on its own branch, so going there is a branch
// switch. The story below it has to change, and only the server can say to
// what. A take on this branch is a leaf by construction. Whatever was played
// after this turn was played after the take that is live, so a take that is
// not live has nothing under it and the transcript ends there.
import { useEffect, useState } from 'react'
import { api } from '../../api'
function TakePager({
advId, action, busy, preview, takesKey, onPreview, onSwitchedBranch, onError,
}) {
const [takes, setTakes] = useState(null)
const [loading, setLoading] = useState(false)
// The cached list is only as good as the text in it. Editing a take
// rewrites one of those rows, so the page says so and the list is fetched
// again on the next step.
useEffect(() => { setTakes(null) }, [takesKey])
const count = action.take_count
const live = action.take_index
const current = preview ? preview.index : live
async function step(delta) {
const next = current + delta
if (next < 0 || next >= count || loading || busy) return
setLoading(true)
try {
// Fetched once per message, then cached — walking back and forth through
// the takes should not re-hit the server for a list that has not changed.
const list = takes || await api.listTakes(advId, action.id)
if (!takes) setTakes(list)
const target = list[next]
if (target.branch_id !== action.branch_id) {
// It has a story of its own. Only the server knows what is under it.
onPreview(null)
onSwitchedBranch(await api.switchBranch(advId, target.branch_id))
} else if (next === live) {
onPreview(null)
} else {
onPreview({
actionId: action.id,
index: next,
// The take's own node id, never its ordinal: the group renumbers
// whenever a take is added, and an ordinal held across that points
// at a different one. This is what `after_id` is given if the reader
// writes from here.
takeId: target.id,
text: target.text,
reasoning: target.reasoning,
})
}
} catch (err) {
onError(err.message)
} finally {
setLoading(false)
}
}
if (count < 2) return null
return (
<div className="take-pager">
<button type="button" disabled={busy || loading || current === 0}
onClick={() => step(-1)} title="The take before this one" aria-label="Previous take">‹</button>
<span className="take-count" aria-live="polite">{current + 1}/{count}</span>
<button type="button" disabled={busy || loading || current === count - 1}
onClick={() => step(1)} title="The take after this one" aria-label="Next take">›</button>
{preview && !preview.written && (
<span className="take-note">write below to keep this one</span>
)}
</div>
)
}
export { TakePager }