fix(viewer): keep latest chronicle beat + action box visible; camp-rest feedback when DM busy - #402
Conversation
…mp-rest feedback when DM busy (#402) Two viewer bugs found in a full-arc playtest. MAJOR — the Chronicle grew unbounded so after ~3 beats the latest DM narration AND the action box were pushed out of view (and an a11y reader truncated before the newest content), stalling the run: - useLiveSession: bound the live tail — chatBeats (60) + player echoes (40) no longer accumulate for the whole session. - screen-table: render only the most-recent 50 rows (CHRONICLE_RENDER_CAP) so the DOM + accessibility tree stay bounded; an affordance points to the Quest Journal for older beats. Anchor the action bar (flex 0 0 auto) so it is never pushed out; label the scroll region role="log". - Smarter auto-follow: scroll to the newest beat when the player is at/near the bottom OR just submitted a move (one-shot snap), but RESPECT a reader who scrolled up (no yank mid-read). Follows streamed paragraphs + the pending/narrating indicator into view. MINOR — Camp "Begin Resting" silently no-op'd while the DM was mid-turn (can_act stays true, so the click POSTed a move that just queued): - Thread the DM-busy (pending) state from the app-level live session into ScreenMap -> CampSidebar; disable the rest CTA + explain ("Resolve the current beat first"), mirroring ScreenTable's "one move at a time" gate. Viewer-only; no wire-contract / --resume / persist_beat changes. Verified with focused single-process viewer tests + a live headless-chromium run of the real OpenWorlds app over a seeded 70-beat session (viewer tests are not in CI). Adds 2 behavioral (bounded-tail) + 3 static tests.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR bounds the memory footprint of long-running playtests by capping in-flight session arrays, prevents camp rests during active DM narration, and windowing the chronicle DOM with smart scroll-following. The changes span app-level routing, component state derivation, and rendered UI controls across multiple screens. ChangesLive Session Bounding and DM-Narration UI
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
…nsition meta-text (#752) (#890) * fix(viewer): bound chronicle a11y footprint + suppress inter-beat transition meta-text (#752) The 2026-06-15 confirm sweep flagged #752 MAJOR by 3 of 5 personas (newbie, adversarial, narrative). Verbatim: "Chronicle log grows into one massive block — later beats invisible in a11y tree" / "Oversized chronicle log pushes Actions section out of a11y tree entirely" / "buries action buttons — player can't tell if DM is done" / "hiding all action controls from screen reader". Plus adversarial: "DUPLICATE DM narration for the opening beat" and "Engine META-TEXT transition phrases LEAK into the chronicle between player beats". FELT MECHANISM: the screen-reader / QA blind-player reads the page via a LINEAR, length-capped accessibility snapshot (qa/playwright/palette_server.js does ariaSnapshot().slice(0,9000)) rendered in DOM order. The Chronicle (role="log") renders BEFORE the action palette + composer, so a long run of multi-paragraph DM beats fills the whole snapshot budget and the action controls are sliced off the snapshot ENTIRELY. #402 anchored the bar VISUALLY (a sticky DOM sibling) but a linear capped snapshot can't be fixed by visual positioning. FIX (viewer-only, READ-ONLY — engine stays sole writer): 1. a11y bound: cap the chronicle's ACCESSIBILITY footprint independently of its visual row count — only the most-recent CHRONICLE_A11Y_TAIL (8) rendered rows stay in the a11y tree; older rendered rows are aria-hidden (still fully VISIBLE for sighted scroll-back, full history in the Quest Journal). The latest beat is always exposed; the bound only engages once the rendered list exceeds the tail. Plus a named role="region" aria-label="Actions" so AT can target the controls. New pure exported chronicleRowAriaHidden(i,total) + CHRONICLE_A11Y_TAIL. 2. opening-beat / player-echo DEDUP: pinned the exactly-once contract (the existing #727/#740/#405 dedup already collapses the opening + the Continue/free-text echo; tests lock it so it can't regress). 3. META-TEXT leak: new high-confidence _BEAT_TRANSITION pattern in sanitizeNarration suppresses inter-beat transition stage-directions ("Moving on to the next beat", "Transitioning to the next scene", "Scene transition", "End of beat", "Beginning the next beat", "between the beats", "move to the next part of the story") while preserving real fiction ("heart skips a beat", "tavern scene", "smooth transition from the parapet to the rope", "between the beats of the drum"). Tests (JSX harness, real .jsx transpiled under Node): - viewer/tests/test_chronicle_a11y_bound.py (3): tail constant tight; action controls stay in the a11y tree after 30 rows (only the tail exposed); short chronicle exposes every row. - viewer/tests/test_chronicle_dedup_opening.py (13): opening once across bands; Continue/free-text no double-echo; wrapper + inter-beat transition meta-text suppressed; surrounding real prose preserved; in-world beat/scene/transition prose survives. fast_gate.sh PASS (215 engine). Full viewer suite 596 passed / 1 skipped. * fix(viewer): tighten _BEAT_TRANSITION to stop over-stripping real fiction (review fix) Adversarial review found the meta-text regex silently deleted real fiction sentences ('the end of the act left them breathless', 'by the close of the scene, three lay dead', 'beginning the act of contrition'). Root: the end-of/beginning-the arms used act/chapter/part (real-fiction words) + an optional forward qualifier, so they matched descriptive prose. FIX: those two arms now use only the engine's own struct terms (beat|scene) AND require the phrase to be TERMINAL (the meta note IS the short sentence, ending on the struct) — descriptive fiction embeds the phrase mid-sentence with trailing prose, so it's spared. The leak cases ('End of beat.', 'Beginning the next beat.') still suppress. +5 survive-case tests (the exact over-strip examples). Story quality is the north star — no silent fiction deletion. --------- Co-authored-by: Eva <arncalso@gmail.com>
Summary
Fixes two viewer bugs found in a full-arc playtest. Viewer-only — no changes to wire contracts, the DM
--resumemodel, orpersist_beat. Stays inviewer/openworlds/*.jsx(+ viewer tests).Do NOT close on merge — verify on the next full-arc playtest.
Bug 1 (MAJOR, blocked progress) — Chronicle grew until new DM narration was invisible
After 2–3 beats the chronicle text was so long that the latest DM narration and the action textbox / Declare button were pushed out of view, and the accessibility tree truncated before reaching the newest content — so the player couldn't see new responses and the run stalled at 5 turns.
Root cause (combination):
useLiveSessionaccumulatedchatBeats(every streamed/turn-end DM beat) andlog(every player echo) for the whole session with no cap.screen-tablerendered every merged row, so the DOM + a11y tree grew without limit; the newest beat sat below an ever-taller scroll region.Fix:
useLiveSession—MAX_LIVE_BEATS = 60,MAX_LIVE_ECHOES = 40(generous — well above one multi-paragraph DM turn, so a beat is never clipped as it streams). Older prose stays in the server'srecentEventshistory band.CHRONICLE_RENDER_CAP = 50rows. DOM + a11y tree stay bounded; a one-line affordance points to the Quest Journal for older beats. (recentEventsis already capped server-side at 12.)flex: 0 0 autoso it is never pushed out; the scroll region is the sole grower and is labelledrole="log".pendingtoo, so the narrating indicator + freshly-streamed paragraphs are followed into view.Bug 2 (MINOR) — "Begin Resting" gave no feedback while the DM was mid-turn
On the Camp screen, clicking "✺ Begin Resting" during a DM turn silently no-op'd —
can_actstaystruewhile the DM narrates and/moveis an append-sink that always returns{ok:true}, so the move just queued behind the in-flight turn (nothing advanced; the success toast was misleading).Fix: thread the DM-busy (
pending) state from the app-level live session intoScreenMap → CampSidebar. While the DM is narrating, the rest CTA is disabled with a clear label ("✺ DM is narrating…"), a tooltip, and a hint ("Resolve the current beat first") — mirroringScreenTable's existing "one move at a time" gate. (The click handler also toasts on the keyboard/edge path.) When the DM is idle the button is unchanged ("✺ Begin Resting", enabled).How I verified
Viewer tests are NOT in CI (CI runs engine/rules/voice pytest +
license_checkonly), so I validated viewer behavior locally, single-process, and with a live headless-chromium run of the real OpenWorlds app.python3 -m unittest, single-process): 200/201 pass. The one failure istest_portrait_gen(ModuleNotFoundError: pydantic— an engine dep absent in the bare env; pre-existing, in a file this PR doesn't touch).license_checkpasses.test_live_narration_stream.py— 2 behavioral tests that run the realuseLiveSessionhook through the cap (stream 60+25 beats → tail trims to 60; newest survives, oldest dropped).test_openworlds_static.py— 3 static tests asserting the windowing/anchor/auto-follow wiring (CHRONICLE_RENDER_CAP,renderedLog,role="log",stickToBottomRef/snapNextRef, anchored action bar), the app-level tail cap (MAX_LIVE_BEATS/boundTail), and the camp DM-busy gate (dmBusythreaded map→camp, button disabled, hint shown).viewer/server.pyagainst a seeded 70-beat campaign and drove chromium against the real/openworlds/UI:distanceFromBottom = 0(auto-followed to latest).Caveats
Summary by CodeRabbit
New Features
Performance