Skip to content

fix(nav): nobody may monkey-patch window.showScreen — add screen:changing, make the shell listen (#924) - #925

Merged
byrongamatos merged 2 commits into
mainfrom
fix/screen-change-event
Jul 12, 2026
Merged

byrongamatos merged 2 commits into
mainfrom
fix/screen-change-event

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Closes #924. Stacked on #923.

window.showScreen was wrapped by three independent parties, each capturing whatever happened to be there at the time:

app.js publishes the raw function
  → static/v3/shell.js wrapped it (to call syncActive, and to map home → v3-songs)
  → the stems plugin wrapped it AGAIN (to tear down on leaving the player)

Plugins load asynchronously, so the chain linked up in whatever order the race settled. A capture taken before shell.js installed silently dropped the mapping it carried — and the library opened on the dead legacy #home screen. Testers saw that as "randomly, the library shows the old interface" (#923).

#923 fixed the symptom. This removes the cause: neither wrapper ever needed to be one.

Two events, and the distinction is the whole point

screen:changing emitted before anything happens. "I am leaving from." Teardown/cancel here.
screen:changed emitted after the DOM and data settle. "I am on id." Now also carries from.

screen:changing is new, and it exists because Codex caught me collapsing the two.

The stems plugin tore down its audio graph before showScreen did anything. screen:changed fires at the very end — after core awaits library and provider loads — so moving the plugin onto it would have delayed teardown behind a slow fetch, or skipped it entirely if that fetch threw, and stems would keep playing on a non-player screen.

A test pins the ordering: screen:changing must precede the first await.

shell.js is a plain screen:changed listener now — like app.js, audio-mixer.js and tour-engine.js already were. window.showScreen is an unwrapped function again, and tests/js/no_showscreen_monkeypatch.test.js fails CI if anything in static/ ever assigns to it again, so the hazard is structurally impossible rather than merely avoided.

And a fallback that could never fire

My retry-if-the-bus-is-late path listened for slopsmith:capabilities:ready. Core dispatches feedBack:capabilities:ready (capabilities.js:1536) — the slopsmith: name is the pre-DMCA event, and nothing has emitted it since the rename. Codex caught it.

A guard that cannot fire is worse than no guard: it reads as protection and is decoration.

The same dead-event bug turned out to be sitting in three of the stems plugin's fallbacks, where it has silently disabled its lifecycle wiring whenever the bus was late. Fixed in feedback-plugin-stems#38.

Verification

A/B against origin/main:

nav highlight + topbar title follow identically with shell.js as a listener
screen:changing → screen:changed fire in order, with the right {id, from}
window.showScreen is unwrapped
showScreen('home') still lands on v3-songs

node 1053 · pytest 2425 · ESLint 0 · Codex 0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Legacy “home” navigation now opens the Songs screen when the v3 interface is available.
    • Screen navigation events now include the previously active screen for improved transition tracking.
    • Navigation updates remain synchronized through screen-change events.
  • Tests

    • Added coverage for legacy home routing, navigation event details, event timing, and prevention of conflicting navigation hooks.

byrongamatos and others added 2 commits July 12, 2026 15:19
… inside showScreen

Testers: "randomly, when moving to the library from another menu option, the library shows the
old interface — never when a song ends."

━━━ WHAT WAS ACTUALLY HAPPENING ━━━

#home is the PRE-V3 library screen. The v3 shell replaced it with #v3-songs, and the mapping DID
exist — but only inside WRAPPERS on window.showScreen, and only for callers that go through
`window`. THREE independent parties monkey-patch it, each capturing whatever happens to be there
at the time:

    app.js publishes the raw function
      -> shell.js wraps it, adding the home -> v3-songs mapping
      -> the stems plugin wraps it AGAIN (src/main.js:1029), capturing the current value

Plugins load ASYNCHRONOUSLY. The chain links up in whatever order the race settles, and any
capture taken before shell.js installs — or any re-assignment after it — silently drops the
mapping. Hence "randomly".

AND THE INTERNAL CALLERS NEVER TOUCHED window.showScreen AT ALL. closeCurrentSong and the
Esc-from-settings shortcut call the IMPORTED showScreen, which no wrapper ever sees. Reproduced
in a browser: the unwrapped function with 'home' lands on the dead legacy screen EVERY time.

"Never when a song ends" is the tell, and it is what identified the mechanism: closeCurrentSong
resolves its target through _resolvePlayerOrigin(), which ALREADY applies this mapping. That one
path was fine — which is exactly why the bug looked random rather than total.

PRE-EXISTING, not a regression from the module carve: the onclick="showScreen('home')" links and
the wrapper-only mapping both date to 2026-06-22.

━━━ THE FIX ━━━

The guard lives inside showScreen now: ONE place, in the function every caller routes through,
instead of a chain of monkey-patches that must each remember. Wrapper order stops mattering, and
the module-internal callers are covered for the first time.

Verified in a browser: the raw, unwrapped showScreen('home') — which reproduced as #home — now
lands on #v3-songs, and cannot be undone by any wrapper order.

━━━ AND A [P1] I INTRODUCED, WHICH CODEX CAUGHT ━━━

My first cut mapped BOTH 'home' and 'v3-home', copied straight from _resolvePlayerOrigin.

That is correct THERE and wrong HERE. _resolvePlayerOrigin computes where to RETURN TO after a
song, and landing on the Songs list from the dashboard is the right behaviour. But #v3-home is
the v3 DASHBOARD — a real screen that the shell's Home nav, the onboarding tour and the dashboard
re-render listener all target. Redirecting it would have made Home unreachable.

A LEGACY ALIAS IS NOT THE SAME THING AS A RETURN TARGET. Only 'home' is mapped now, and a test
pins that: re-adding 'v3-home' to the guard fails it.

4 tests, bite-tested both ways.

node 1049, pytest 2425, ESLint 0, Codex 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ging, make the shell listen (#924)

window.showScreen was wrapped by THREE independent parties, each capturing whatever happened to be
there at the time:

    app.js publishes the raw function
      -> static/v3/shell.js wrapped it (to call syncActive, and to map home -> v3-songs)
      -> the stems plugin wrapped it AGAIN (to tear down on leaving the player)

Plugins load ASYNCHRONOUSLY, so the chain linked up in whatever order the race settled. A capture
taken before shell.js installed silently dropped the mapping it carried — and the library opened on
the dead legacy #home screen. Testers saw that as "randomly, the library shows the old interface"
(#923).

#923 fixed the symptom by moving the mapping inside showScreen. This removes the CAUSE: neither
wrapper ever needed to be one.

━━━ TWO EVENTS, AND THE DISTINCTION IS THE WHOLE POINT ━━━

    screen:changing  emitted BEFORE anything happens. "I am leaving `from`." Teardown/cancel here.
    screen:changed   emitted after the DOM and data settle. "I am on `id`." Now carries `from`.

screen:changing is new, and it exists because Codex caught me collapsing the two. The stems plugin
tore down its audio graph BEFORE showScreen did anything; screen:changed fires at the very END,
after core awaits library and provider loads — so moving the plugin onto it would have delayed
teardown behind a slow fetch, or skipped it entirely if that fetch threw, and stems would keep
playing on a non-player screen. A test pins the ordering: screen:changing must precede the first
await.

shell.js is a plain screen:changed listener now, like app.js, audio-mixer.js and tour-engine.js
already were. window.showScreen is an unwrapped function again, and tests/js/
no_showscreen_monkeypatch.test.js fails CI if anything in static/ ever assigns to it again — so the
hazard is structurally impossible rather than merely avoided.

━━━ AND A FALLBACK THAT COULD NEVER FIRE ━━━

My retry-if-the-bus-is-late path listened for `slopsmith:capabilities:ready`. Core dispatches
`feedBack:capabilities:ready` (capabilities.js:1536) — the slopsmith: name is the PRE-DMCA event
and nothing has emitted it since the rename. Codex caught it. A guard that cannot fire is worse
than no guard: it reads as protection and is decoration.

(The same dead-event bug turned out to be sitting in THREE of the stems plugin's fallbacks, where
it has silently disabled its lifecycle wiring whenever the bus was late. Fixed in
feedback-plugin-stems#38.)

VERIFIED. A/B against origin/main: the nav highlight and topbar title follow IDENTICALLY with
shell.js as a listener; screen:changing -> screen:changed fire in order with the right {id, from};
window.showScreen is unwrapped; and showScreen('home') still lands on v3-songs.

node 1053, pytest 2425, ESLint 0, Codex 0.

Closes #924

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ca560f2-9e6f-4b7d-b839-cffc6c7c7fb2

📥 Commits

Reviewing files that changed from the base of the PR and between 545e569 and 0cb8b45.

📒 Files selected for processing (4)
  • static/js/session.js
  • static/v3/shell.js
  • tests/js/no_showscreen_monkeypatch.test.js
  • tests/js/show_screen_legacy_home.test.js

📝 Walkthrough

Walkthrough

showScreen now routes legacy home navigation to v3-songs when available and emits source-screen context. The v3 shell listens for screen:changed instead of wrapping window.showScreen, with tests covering routing, event ordering, payloads, and monkey-patch prevention.

Changes

Screen navigation

Layer / File(s) Summary
Navigation routing and event payloads
static/js/session.js, tests/js/show_screen_legacy_home.test.js
Legacy home requests map to v3-songs when present, while screen:changing and screen:changed include the previous screen ID. Tests verify routing conditions and ordering.
Shell event integration
static/v3/shell.js, tests/js/no_showscreen_monkeypatch.test.js
The v3 shell synchronizes through screen:changed with capability-readiness retry behavior, and tests prevent window.showScreen monkey-patching while checking event payloads and ordering.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant showScreen
  participant feedBack
  participant v3Shell
  showScreen->>feedBack: Emit screen:changing with id and from
  showScreen->>feedBack: Emit screen:changed with id and from
  feedBack->>v3Shell: Deliver screen:changed
  v3Shell->>v3Shell: Invoke syncActive(id)
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the navigation hook removal and event-based shell update.
Linked Issues check ✅ Passed The PR implements the requested event-based navigation flow and removes the shell monkey-patch without changing unrelated scope.
Out of Scope Changes check ✅ Passed The changes stay focused on navigation behavior, event payloads, and regression tests for the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/screen-change-event

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

@byrongamatos
byrongamatos merged commit 8d3db5f into main Jul 12, 2026
5 checks passed
@byrongamatos
byrongamatos deleted the fix/screen-change-event branch July 12, 2026 14:06
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.

window.showScreen is monkey-patched by three parties — order-dependent, and it already broke the library once

1 participant