Skip to content

fix(viewer): keep latest chronicle beat + action box visible; camp-rest feedback when DM busy - #402

Merged
100yenadmin merged 1 commit into
mainfrom
fix/chronicle-visibility-rest-feedback
May 30, 2026
Merged

fix(viewer): keep latest chronicle beat + action box visible; camp-rest feedback when DM busy#402
100yenadmin merged 1 commit into
mainfrom
fix/chronicle-visibility-rest-feedback

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 30, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two viewer bugs found in a full-arc playtest. Viewer-only — no changes to wire contracts, the DM --resume model, or persist_beat. Stays in viewer/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):

  • Unbounded state. useLiveSession accumulated chatBeats (every streamed/turn-end DM beat) and log (every player echo) for the whole session with no cap.
  • Unbounded render. screen-table rendered every merged row, so the DOM + a11y tree grew without limit; the newest beat sat below an ever-taller scroll region.
  • Dumb auto-scroll. The old effect pinned scrollTop to the bottom on every content change (yanking a reader who'd scrolled up) and never fired when only the pending/narrating indicator toggled.

Fix:

  • Bound the live tail in useLiveSessionMAX_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's recentEvents history band.
  • Window the rendered chronicle — render only the most-recent CHRONICLE_RENDER_CAP = 50 rows. DOM + a11y tree stay bounded; a one-line affordance points to the Quest Journal for older beats. (recentEvents is already capped server-side at 12.)
  • Anchor the action barflex: 0 0 auto so it is never pushed out; the scroll region is the sole grower and is labelled role="log".
  • Smart auto-follow — scroll to the newest beat when the player is at/near the bottom or just submitted a move (one-shot snap on a new turn), but respect a reader who scrolled up (no yank mid-read). Depends on pending too, 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_act stays true while the DM narrates and /move is 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 into ScreenMap → 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") — mirroring ScreenTable'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_check only), so I validated viewer behavior locally, single-process, and with a live headless-chromium run of the real OpenWorlds app.

  • Focused viewer tests (python3 -m unittest, single-process): 200/201 pass. The one failure is test_portrait_gen (ModuleNotFoundError: pydantic — an engine dep absent in the bare env; pre-existing, in a file this PR doesn't touch). license_check passes.
  • New tests added here:
    • test_live_narration_stream.py — 2 behavioral tests that run the real useLiveSession hook 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 (dmBusy threaded map→camp, button disabled, hint shown).
  • Live browser run — booted viewer/server.py against a seeded 70-beat campaign and drove chromium against the real /openworlds/ UI:
    • 70 distinct beats streamed → 50 narration rows rendered (= the cap, not 70), latest beat present, oldest trimmed, "earlier beats" affordance shown → DOM + a11y tree bounded.
    • Action box always reachable — input + Declare visible in-viewport, distanceFromBottom = 0 (auto-followed to latest).
    • Auto-follow — a beat appended mid-session was ingested and scrolled to the bottom.
    • Scroll-up respected — scrolled to the top, appended a new beat → scroll position held (not yanked down), beat still ingested.
    • Camp rest while DM mid-turn — posted a move (pending armed; survives table→camp navigation via the app-level hook) → rest button disabled, labelled "✺ DM is narrating…", with the "Resolve the current beat first" hint. Idle case: enabled, normal label.

Caveats

  • Viewer behavior is not covered by CI; reviewers should keep this open and confirm on the next full-arc playtest (per the title note).
  • The render cap (50) windows the displayed chronicle; the full history remains available in the Quest Journal. This is intentional (a 10-beat session should be as navigable as a 2-beat one).

Summary by CodeRabbit

  • New Features

    • Rest action now provides clear feedback when the DM is mid-narration, preventing unintended conflicts
    • Chronicle log now displays a bounded window of recent beats with improved auto-scroll behavior
  • Performance

    • Enhanced stability during extended play sessions through optimized memory management of live session data

Review Change Stack

…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.
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f29b157-7a2c-40ee-ab4c-ec1cba670c89

📥 Commits

Reviewing files that changed from the base of the PR and between 23271c0 and 5143e27.

📒 Files selected for processing (6)
  • viewer/openworlds/app.jsx
  • viewer/openworlds/camp-sidebar.jsx
  • viewer/openworlds/screen-map.jsx
  • viewer/openworlds/screen-table.jsx
  • viewer/tests/test_live_narration_stream.py
  • viewer/tests/test_openworlds_static.py

📝 Walkthrough

Walkthrough

This 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.

Changes

Live Session Bounding and DM-Narration UI

Layer / File(s) Summary
Live-session tail bounding infrastructure
viewer/openworlds/app.jsx, viewer/tests/test_live_narration_stream.py, viewer/tests/test_openworlds_static.py
MAX_LIVE_BEATS and MAX_LIVE_ECHOES constants define in-memory caps; boundTail(arr, max) helper trims arrays to their most-recent entries. Bounding is applied to player-echo log during recordPlayerEcho, and to chatBeats during both /chat poll ingestion and /events streamed narration. Helper and caps are exposed on window for tests/devtools. Test harness gains caps() and beatCount() accessors; new behavior tests verify bounded tail and newest-beat preservation.
DM-busy state propagation and rest control
viewer/openworlds/app.jsx, viewer/openworlds/screen-map.jsx, viewer/openworlds/camp-sidebar.jsx, viewer/tests/test_openworlds_static.py
App routes liveSession to ScreenMap via router wiring. ScreenMap accepts liveSession, derives dmBusy as "pending and not stuck", and passes dmBusy to CampSidebar. CampSidebar uses dmBusy to gate beginRest with an early return and danger toast, disables the "Begin Resting" button, and updates messaging to differentiate DM-narrating from read-only states. Static test validates the full flow and button disabled state.
Chronicle rendering and smart scroll management
viewer/openworlds/screen-table.jsx, viewer/tests/test_openworlds_static.py
CHRONICLE_RENDER_CAP limits rendered chronicle rows; renderedLog is computed by slicing visibleLog to the last CHRONICLE_RENDER_CAP entries. stickToBottomRef and snapNextRef track scroll position and force scroll-to-bottom after moves. onLogScroll handler updates stickToBottomRef using distance-from-bottom thresholding. Scroll container gains role="log" and aria-label for accessibility; older hidden beats are counted and surfaced via a "earlier beats kept" notice linking to Quest Journal. Action bar remains anchored via flex layout.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • electricsheephq/WorldOS#393: Changes to chatBeats ingestion from /events streamed narration directly address the issue's request to surface DM mid-turn narration and avoid blank-screen waits.
  • electricsheephq/WorldOS#385: Updates to useLiveSession (liveSession hook in app.jsx) and chronicle UI (screen-table.jsx) relate to the reported cold-open/frozen-first-beat issue.

Possibly related PRs

  • electricsheephq/WorldOS#351: New dmBusy logic in ScreenMap derives "pending and not stuck" from liveSession.pending, directly building on how that PR modifies the stuck state computation in useLiveSession.
  • electricsheephq/WorldOS#343: Both PRs propagate liveSession through ScreenRouter into screen components—this PR routes into ScreenMap/CampSidebar for dmBusy, while the retrieved PR routes into ScreenTable for live state binding.
  • electricsheephq/WorldOS#400: Both PRs modify the player-echo recording path in app.jsx; this PR further bounds/trims the log array during echo append, building on that PR's idempotent echo logic.

Poem

🐰 Bounding arrays keeps the session lean,
No more infinite chat to be seen!
Smart scrolls follow, yet respect the scroll—
And when DMs narrate, rest takes a stroll.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides comprehensive details on both bugs, their root causes, and fixes applied. However, it is missing the required Licensing/CLA checklist from the repository template. Add the Licensing/CLA section with checkboxes from the template to confirm CLA agreement and absence of restricted content.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes both major fixes: bounding the chronicle to keep it visible and providing feedback when camp-rest is unavailable during DM narration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@100yenadmin
100yenadmin merged commit ce46f61 into main May 30, 2026
7 checks passed
@100yenadmin
100yenadmin deleted the fix/chronicle-visibility-rest-feedback branch May 30, 2026 16:19
100yenadmin added a commit that referenced this pull request Jun 14, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant