fix(viewer): make the DM-narrating wait watchable + advertise nav-during-compose (G3 latency UX) - #571
Conversation
…ing-compose (G3 latency UX) The ~120-200s/beat DM latency is structural, but the play-screen spinner read as a DEAD freeze: a static 'Weaving the next beat…' line with no connection to the live /events narration #393 already streams into the chronicle above it, and nothing told the player that read-only screens stay open while the DM composes. A veteran praised the story but rage-quit on the wait. Two LOW-RISK, additive UI fixes: FIX 1 — wire the spinner to the in-flight /events tail. notePendingProgress (the fn that already fires when live prose streams) now stamps pending.streaming; ScreenTable passes it into DmNarratingBeat, which flips its later-beat copy from the anticipatory 'Weaving the next beat' to a present-tense 'The scene is unfolding above … appearing above as it's composed.' So the player WATCHES the beat being written. Cold-open path unchanged; pre-streaming copy unchanged. FIX 2 — advertise nav-during-compose. Read-only nav (character sheet / map / journal) is already un-gated during compose (onNavigate has no pendingActive gate), but the player didn't know. Added a visible affordance near the narrating beat with real onNavigate calls, so the long wait no longer reads as 'frozen, can't do anything'. a11y: the non-first narrating beat's aria-live region is scoped to the status text so the nav buttons aren't re-announced on every status change. Tests: 2 jsdom behavioral tests (pending.streaming set on /events stream; reset per turn) + 1 static assert (streaming-aware copy + nav affordance render). Full viewer suite single-process locally: 296 passed (1 unrelated portrait-subprocess env failure that CI provisions for).
|
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 (4)
📝 WalkthroughWalkthroughThis PR extends the live-session pending narrating indicator with a ChangesNarration Streaming Indicator and UI
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
Why
A veteran played the full arc (story praised) but rage-quit: the DM takes ~120–200s+/beat AND the play screen showed only a static "Weaving the next beat — this can take a minute or two" spinner with no progress → the wait felt dead. The latency is structural (lean+effort already engage); this makes the wait watchable and tells the player they can do things while waiting.
Two LOW-RISK, additive UI changes (no quality A/B needed).
FIX 1 — stream the in-flight narration into the spinner
The live
/eventsnarration tail already flows into the chronicle above the spinner (#393/#394), but the spinner copy was disconnected from it — a static line while prose was visibly arriving right above. Now:useLiveSession.notePendingProgress(app.jsx) — the function that already fires the moment live/eventsprose lands for the in-flight turn — stampspending.streaming = true(folded into the existing stuck-clear updater; no extra state churn; falsy by default so today's behavior is preserved).ScreenTablepassesstreaming={Boolean(pending.streaming)}intoDmNarratingBeat(viewer/openworlds/screen-table.jsx:961).DmNarratingBeat(viewer/openworlds/screen-table.jsx:1327) flips its later-beat copy oncestreamingis true: label → "The scene is unfolding above", hint → "The Dungeon Master is writing this beat — it's appearing above as it's composed." So the player watches the beat being written. The cold-open (firstBeat) path and the pre-streaming "Weaving the next beat" copy are unchanged.Turn-gating is unchanged: the bar stays gated until
/chatresolves the turn (the existing Option-B semantics).pending.streamingresets per turn becausearmPendingstarts a fresh pending object.FIX 2 — nav-during-compose affordance
Read-only nav (character sheet / map-Travel / Quest Journal / Quick Stash) is already un-gated during compose —
app.jsx'sonNavigate(navigate, line ~785) has nopendingActivegate; only move/write controls gate. The player just didn't know. Added a visible affordance nearDmNarratingBeat(viewer/openworlds/screen-table.jsx:1437) with realonNavigatecalls (character/map/journal) so the long wait no longer reads as "frozen, can't do anything". It is a working invitation, not just copy.a11y
The non-first narrating beat's
aria-live="polite"region is now scoped to the status text only (label + hint), so the nav buttons below it are not re-announced on every status change; the single meaningful "the scene is arriving" flip is still announced once. The per-second elapsed counter staysaria-hidden.Tests
viewer/tests/test_live_narration_stream.py— 2 new jsdom behavioral tests:test_streamed_beat_marks_pending_streaming(an/eventsbeat setspending.streamingwhile keeping the turn gated) andtest_new_turn_resets_streaming_flag(a fresh turn does not inherit it).viewer/tests/test_openworlds_static.py—test_openworlds_narrating_beat_reflects_live_stream_and_offers_nav: static assert that the spinner block renders the streaming-aware copy (wired to the/eventstail) and thenarrating-nav-affordancewith realonNavigatecalls.viewer/testssingle-process locally: 296 passed, 2 skipped; 1 failure is an unrelatedPortraitGenRealSubprocessTestsvenv-isolation artifact (subprocess couldn't importpydanticin the ad-hoc local venv) that CI provisions for. Relying on CI for the authoritative run.Invariants
Pure viewer/UI; no engine, no state writes, no schema change. Additive (empty/falsy
streaming== today). Does not relax turn-gating. Does not touch the macOS/OpenWorlds sibling lane.NOT for merge yet
A live re-run must confirm the narration renders early enough to feel live (not just that the data flows) and that G3 satisfaction recovers. Holding for that.
Summary by CodeRabbit
New Features
Accessibility