Repository navigation
build(desktop): add a dev build that adapts Steam's game page in the desktop client - #2389
Conversation
333b4e9 to
8f64cb3
Compare
danielcopper
left a comment
There was a problem hiding this comment.
This is a sound place to start: no shipped bundle carries any of it, the ledger, the read-only Fiber and the realm handling are the right shape, and #2107 D5 now settles how the two cross-file rules get checked. Most of the comments below come down to the failure paths: a pass that throws, a mount that fails or an attach that throws leaves Steam's page changed or the watcher's timers running, and says nothing. They are latent today because nothing is handed in, and live with the first view, so I would fix them here. Measured on a German desktop client along the way: @decky/ui's class maps are the only rung that finds the play bar there.
8f64cb3 to
ee677ab
Compare
|
Thank you, and for the German measurement in particular: it is what decided the ladder. Every failure path you found was real and is fixed, and the tests for the fixes fail without them, including under the two mutations you named for the ledger. Setting the window only after the attach returns is the exception: since a window that failed is never tried again, undoing it changes nothing a test can see. Three catches that only log have no test:
The architecture page is rewritten along your page-wide notes, and the realm point now lives in the widened |
danielcopper
left a comment
There was a problem hiding this comment.
Most of round one is done and holds: 19 of the 20 threads are addressed, the two ledger mutations from round one now fail the snapshot test, the class-hash check fails on a string and on a JSX className, and the suite, typecheck and lint pass at this head. What is left is mostly the same kind as before, failure paths that go live with the first view: a switch between two RomM games and a controller whose first pinning throws, both inline with probes. One more from thread 7: a RomM page the finders cannot recognise still logs nothing, and after a Steam update that is the line one would look for.
ee677ab to
256f923
Compare
|
Thank you. All seven points held. Each fix comes with a test that fails without it: a switch between two RomM games; a synchronous mount failure, which now shares one handler with |
256f923 to
bdde0d0
Compare
danielcopper
left a comment
There was a problem hiding this comment.
All seven points hold, and each fix is pinned: with the switch-as-leave rule, the dispose on a throwing first pinning, D8's rendered check, the failure handler's schedulePass() or the regex-literal selector taken out, a test goes red each time, and both probes from the last round now pass. One small thing inline. And D6 and D8 on #2388 now describe less than the code does: a switch to another RomM game also gives the page back, and every failed mount, the synchronous one included, gives Steam's part back; a dated revision of each would keep the issue and the body in step.
onRomMAppIdsChanged(listener) calls the listener once after a synchronous run of changes to the set, on the microtask after it, and not at all for a register or unregister that changes nothing. registerRomMAppId now returns early for an appId the set already holds; adding it again was already a no-op on the set, so the early return only keeps it from notifying. Nothing in a shipped bundle listens yet.
…desktop client pnpm -C frontend build:desktop builds the three usual bundles, with src/index.tsx rewritten in the two builds of the panel (scripts/start-desktop-surface.mjs) to call startDesktopSurface() after disownStrandedPruneLeases();. The build fails when that statement is there other than once, or when a build of the panel never reaches src/index.tsx. No other build carries frontend/src/desktop/. startDesktopSurface() starts the navigation watcher, which finds the desktop client window, follows its route to a RomM shortcut's game page and adapts the page — sticky play bar, the hero's overflow, Steam's duplicate sticky header, the right-hand controls — through a restoration ledger that gives every change back when the page leaves it, a pass that threw included. It runs at most one pass an animation frame and on no timer of its own, logs what it found and every failure, and hides a part of Steam's page only once the view replacing it has rendered. It is handed no view to mount, so it draws nothing of its own. ESLint fails a string under frontend/src/desktop/ shaped like a minified class hash, and a test holds that leaving a page gives back the style attribute of every element on it (danielcopper#2107 D5). docs/architecture/desktop-dom-architecture.md describes the watcher, and .claude/rules/desktop-dom.md holds the rules for changing Steam's page.
bdde0d0 to
e859684
Compare
|
Thank you. D6 and D8 on #2388 are struck with dated revisions. D11 restores the page on leaving it for another RomM game's page as well, and D12 has every failed mount give Steam's page back, one that throws as it is mounted included. The body carries D11 to D13 in their place. The inline point is answered there. |
danielcopper
left a comment
There was a problem hiding this comment.
All points from the last round hold. Logging how long an unrecognised page was open tells a page left early apart from one the finders cannot read, without a threshold nobody has measured; checked by measuring from the last missed pass instead of the first, which fails both new cases. On the desktop client the watcher adapted a RomM game page and found the panel, the play bar and the Play button by module class.
|



Adds a dev-only build that starts the desktop surface beside the panel, and the navigation watcher it starts: it finds the desktop client's window, follows its route to a RomM shortcut's game page, and adapts that page through a ledger that gives every change back. It draws no view of its own yet, and no shipped bundle behaves differently.
The desktop dev build
pnpm -C frontend build:desktop(frontend/rollup.desktop.config.js) builds the same three bundles asbuild:dev, and gives the two builds of the panel one more transform (insertDesktopSurfaceStart,frontend/scripts/start-desktop-surface.mjs) that rewritessrc/index.tsxas it is bundled to callstartDesktopSurface()right afterdisownStrandedPruneLeases();. The build fails rather than ship without the surface: when that statement is there other than once, and when a build of the panel never reachessrc/index.tsx. Nothing else importsfrontend/src/desktop/, sopnpm build,build:dev, CI and the release tarball carry none of it. Nothing in the build callsstopDesktopSurface(): a JS-context rebuild, not a teardown call, is what ends the panel.The navigation watcher
desktop/navigationWatcher.tslooks for the desktop client's window (findDesktopWindow,desktop/desktopWindow.ts) when it starts, on a poll and on Steam's popup callbacks. On a RomM shortcut's game page it makes the play bar sticky, keeps the hero wrapper atoverflow: visibleand bounds the artwork's overflow on the parallax layer below it (boundHeroOverflow), hides Steam's duplicate sticky header, and pushes the right-hand controls to the right edge. Every change goes throughwatcher/restorationLedger.ts, and leaving the page gives back whatever the ledger holds, a pass that threw partway included; moving to another RomM game's page counts as leaving. Steam's elements are found without naming a hashed class, through the selector ladder inwatcher/elementSelectors.ts, and no rung reads text or an ARIA label; Fiber (watcher/fiberInspector.ts) is only read.A pass runs when something asks for one — an element added or removed, a change to the set of RomM shortcuts, a view's first render or failure — at most one an animation frame; no timer runs a pass on a page. The sticky play bar controller's
ResizeObserverfollows the sizes the clip margin is computed from. The watcher logs each transition, every failure it catches, and a RomM page it left without ever recognising it, with how long that page was open, on lines startingdesktop:.The watcher imports no view: it draws the
gameViewandplayButtonit is handed, andstartDesktopSurface(desktop/index.ts) hands it neither yet, so Steam's own content and Play button stay. When a view is handed, Steam's part is hidden only once the view has rendered, and a mount that fails is not tried again for that appId and gives back any Steam part an earlier mount had hidden. Those paths are tested with stand-in components.The watcher hears about new RomM shortcuts through
onRomMAppIdsChanged, added toutils/rommAppIds.tsin its own commit.registerRomMAppIdnow returns early for an appId the set already holds; adding it again was already a no-op on the set, so this only keeps the listeners from being told of a change that did not happen. Nothing in a shipped bundle listens.How it was checked
frontend/scripts/start-desktop-surface.test.mjspins the rewrite, the modules it leaves alone, and the three ways the build fails.navigationWatcher.test.tsdrives a stand-in desktop window whose frames and observer run only when the test says, with real React roots. It covers adapting, re-adapting what Steam adds later, a switch between RomM games, the restore on leaving, a page handed one part or none, a view that never renders, the real registry, a pass that throws, mounts that fail, a page never recognised, and the window supervisor. Its "every change it makes is given back" case compares thestyleattribute of every element before and after; it fails when the ledger records a second original or when a style is written past it.stickyPlayBarController.test.tscovers the pinning, the hero, the clip margin, and a first pinning that throws.elementSelectors.moduleClasses.test.tsholds the first rung against made-up hashed class maps.eslintNoClassHashes.test.tskeeps the class-hash check reporting, in strings, template parts and regex literals.ResizeObservernow follows.pnpm buildleavesstartDesktopSurfaceout of the standalone panel bundle,check:bundleandcheck:packagespass on it, andbuild:desktop's two panel bundles call it.pnpm typecheck,pnpm lint, and the full Vitest suite pass (227 test files, 6018 tests).Docs:
docs/architecture/desktop-dom-architecture.md(new, How the desktop surface adapts Steam's game page),docs/architecture/frontend-bundles.md(The desktop dev build),docs/architecture/invariants.md,docs/contributing/frontend-dev-loop.md(Desktop surface dev loop),docs/contributing/development.md,GLOSSARY.md,frontend/src/desktop/README.md,.claude/rules/desktop-dom.md(new),CLAUDE.md.Decisions
From #2388; D1 was replaced by D10, D3 by D6 and D6 in turn by D11, and D8 by D12.
DesktopGamePage, an optionalgameViewandplayButton), andstartDesktopSurfaceindesktop/index.tsis the one place that hands it anything. Today it hands it nothing, so the watcher adapts Steam's page and mounts nothing.overflow: visible, so the artwork's 3D parallax is not flattened; the page's scroll gap is closed lower in the chain, on the parallax layer, byboundHeroOverflow.onRomMAppIdsChanged(utils/rommAppIds.ts), which hears a synchronous run of changes as one call and a change that changes nothing not at all;registerRomMAppIdreturns early for an appId the set already holds.ResizeObserver's, which watches the play bar, the scroller, the content container and the parallax layer. Measured on a Steam Deck's desktop client, 2026-10-14: the watcher's interval and settle timeouts changed nothing the observer's passes had not, and the sticky play bar controller's settle timeouts caught only the parallax layer's clip margin.styleattribute, and an ESLint check that no code underfrontend/src/desktop/names a minified class hash, each with its entry in the invariant register.pnpm -C frontend build:desktop, which rewritessrc/index.tsxin the two builds of the panel to callstartDesktopSurface()right afterdisownStrandedPruneLeases();, and fails when that statement is there other than once, or when a build of the panel never reachessrc/index.tsx.pnpm build,build:dev, CI,mise run devand the release tarball carry none of it. Nothing in the build calls a stop: a JS-context rebuild, not a teardown call, ends the panel.DomRestorationLedger, restored whenever the ledger holds anything once the page is left — for a page that is no RomM shortcut's or for another RomM game's — and when the watcher detaches. Fiber is read and never written. A selector never names a hashed class: each finder inwatcher/elementSelectors.tsresolves through a ladder of@decky/ui's resolved classes, the readable part of a class name, Fiber names, and structure; no rung reads text or an ARIA label, which read differently in every language — on a German desktop client only the first rung finds the play button and the play bar. The hero's parallax layer and mirrored canvas are found by their computed transform. The rules are.claude/rules/desktop-dom.md.onUncaughtError— is logged and not tried again for that appId, and has the next pass give Steam's page back before it adapts the page again. Every failure the watcher catches is logged with what it was doing.Closes #2388. Part of #2107