Skip to content

fix(viewer): make the DM-narrating wait watchable + advertise nav-during-compose (G3 latency UX) - #571

Merged
100yenadmin merged 1 commit into
mainfrom
fix/g3-streaming-nav
Jun 2, 2026
Merged

fix(viewer): make the DM-narrating wait watchable + advertise nav-during-compose (G3 latency UX)#571
100yenadmin merged 1 commit into
mainfrom
fix/g3-streaming-nav

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 2, 2026

Copy link
Copy Markdown
Member

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 /events narration 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 /events prose lands for the in-flight turn — stamps pending.streaming = true (folded into the existing stuck-clear updater; no extra state churn; falsy by default so today's behavior is preserved).
  • ScreenTable passes streaming={Boolean(pending.streaming)} into DmNarratingBeat (viewer/openworlds/screen-table.jsx:961).
  • DmNarratingBeat (viewer/openworlds/screen-table.jsx:1327) flips its later-beat copy once streaming is 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 /chat resolves the turn (the existing Option-B semantics). pending.streaming resets per turn because armPending starts 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 composeapp.jsx's onNavigate (navigate, line ~785) has no pendingActive gate; only move/write controls gate. The player just didn't know. Added a visible affordance near DmNarratingBeat (viewer/openworlds/screen-table.jsx:1437) with real onNavigate calls (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 stays aria-hidden.

Tests

  • viewer/tests/test_live_narration_stream.py — 2 new jsdom behavioral tests: test_streamed_beat_marks_pending_streaming (an /events beat sets pending.streaming while keeping the turn gated) and test_new_turn_resets_streaming_flag (a fresh turn does not inherit it).
  • viewer/tests/test_openworlds_static.pytest_openworlds_narrating_beat_reflects_live_stream_and_offers_nav: static assert that the spinner block renders the streaming-aware copy (wired to the /events tail) and the narrating-nav-affordance with real onNavigate calls.
  • Full viewer/tests single-process locally: 296 passed, 2 skipped; 1 failure is an unrelated PortraitGenRealSubprocessTests venv-isolation artifact (subprocess couldn't import pydantic in 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

    • Added live streaming indicator displaying "The scene is unfolding…" when narration is actively being streamed during gameplay
    • Added navigation affordances guiding players to character sheet, map, and journal during scene composition
  • Accessibility

    • Improved screen reader behavior by scoping live region announcements to status updates only, preventing redundant control re-announcements

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

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

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: 80d9a8cf-460f-4ef4-a213-d2fe55cbf3f8

📥 Commits

Reviewing files that changed from the base of the PR and between 12207a9 and a568ca9.

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

📝 Walkthrough

Walkthrough

This PR extends the live-session pending narrating indicator with a streaming flag that tracks mid-turn narration arrivals via /events. DmNarratingBeat is updated to display streaming-aware messaging ("The scene is unfolding…") and conditionally render navigation affordances to character sheet, map, and journal during non-cold-open pending waits. Tests verify streaming state management and UI wiring.

Changes

Narration Streaming Indicator and UI

Layer / File(s) Summary
Streaming flag state model
viewer/openworlds/app.jsx, viewer/tests/test_live_narration_stream.py
pending state now includes a streaming boolean derived from /events arrivals per turn. notePendingProgress sets streaming: true and clears stuck when streamed prose arrives mid-turn. Tests verify that streaming is set without resolving the turn and that new turns start un-streamed.
ScreenTable wiring to DmNarratingBeat
viewer/openworlds/screen-table.jsx
ScreenTable extracts pending.streaming and passes it as the streaming flag alongside onNavigate when rendering the pending narration beat.
DmNarratingBeat streaming props and logic
viewer/openworlds/screen-table.jsx
DmNarratingBeat signature expanded to accept streaming and onNavigate props. Computed label and waitHint switch to present-tense "The scene is unfolding…" when streaming is true (non-cold-open only). showNavAffordance flag controls conditional rendering of nav buttons.
Streaming UI rendering and accessibility
viewer/openworlds/screen-table.jsx, viewer/tests/test_openworlds_static.py
Pending beat rendering scopes aria-live="polite" to announced status only; navigation affordance for character/map/journal renders outside the live region to prevent re-announcement. Nav buttons are hidden on cold-open and shown when onNavigate is available. Static test verifies wiring, copy switching, and nav affordance behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • electricsheephq/WorldOS#394: Modifies the same useLiveSession live-session pending/progress logic to react to mid-turn /events narration (clearing stuck/adjusting pending behavior) as part of the incremental DM narration streaming flow.
  • electricsheephq/WorldOS#328: Modifies DmNarratingBeat component and narration-pending state wiring in screen-table.jsx; the current PR's pending.streaming and updated props build directly on that narration affordance implementation.
  • electricsheephq/WorldOS#351: Introduces firstBeat and adaptive stuck timing in the same pending-beat flow; the current PR extends that shared pending-beat state to track /events-streaming via pending.streaming and adjust UI accordingly.

Poem

🐰 Streams flow through the pending beat,
Unfolding scenes now feel complete.
When prose arrives mid-turn so fast,
The flag marks streaming at long last.
Scene unfolding—now that's neat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: making the DM-narrating wait watchable and advertising navigation during compose for G3 latency UX improvements.
Description check ✅ Passed The description comprehensively covers objectives, implementation details, testing, and invariants, though it deviates from the template structure by not explicitly checking the CLA and licensing checkboxes.
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.

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