--- name: steam-cdp description: Drive and inspect the running Steam client UI over the Chrome DevTools Protocol (CEF remote debugging, port 8080) — list targets, evaluate JS probes, navigate routes, click elements, dispatch keys, capture screenshots, stream console errors. Use for hands-free UI automation and read-only diagnosis of Steam's gamepad UI on a Steam Deck (or any Steam with CEF debugging enabled). --- # Steam CDP Automation Steam's embedded Chromium (CEF) exposes remote debugging on **localhost:8080** on the device (enabled by default in Decky environments; desktop Steam needs `-cef-enable-debugging`). Everything here is **read-only diagnosis and test driving** — never ship CDP tricks inside a plugin. The bundled scripts live in `scripts/` next to this file and are dependency-free (Node ≥ 21 for global `fetch`/`WebSocket`). ## Setup (once per machine session) ```bash ssh -N -L 8080:localhost:8080 deck@ & # tunnel; survives plugin_loader restarts node scripts/cdp.mjs targets # list targets, sanity-check the tunnel ``` ## Targets | Alias | Target | Use for | |---|---|---| | `bp` | gamepad-UI main window ("Steam Big Picture Mode" / localized) | the visible screen: screenshots, DOM clicks, key input | | `qam` | QuickAccess | Quick Access Menu overlay | | `mainmenu` | MainMenu | left Steam main menu overlay | | `shared` / `sjc` | SharedJSContext | main JS context (no pixels) — webpack/module probing | ## Script inventory ```bash node scripts/cdp.mjs targets # list targets with aliases node scripts/cdp.mjs # Runtime.evaluate a JS file, print its return value node scripts/cdp-nav.mjs /your-route # navigate the gamepad UI to a route node scripts/cdp-shot.mjs bp /tmp/ui.png # screenshot a target to PNG node scripts/cdp-key.mjs bp ArrowDown Enter # dispatch keyDown/keyUp pairs node scripts/cdp-console.mjs shared 30 # stream console warnings/errors for N seconds node scripts/cdp.mjs mainmenu scripts/probe-mainmenu.js # example probe: menu patch anchors ``` `cdp-lib.mjs` is the shared helper (target resolution, sessions, evaluate, screenshot, keys) — import it to build new drivers. ## Writing probes A probe is an IIFE evaluated in the target page. Return a JSON-serializable value instead of printing from inside the page. Reuse the bundled probes before writing another React-fiber walk. For read-only webpack inspection, capture the runtime require and search factory sources: ```js window[Object.keys(window).find((k) => k.startsWith("webpackChunk"))].push([ [Symbol()], {}, (r) => (req = r), ]); // Object.entries(req.m).filter(([id, f]) => String(f).includes("SomeConstant")) // req(id) ``` ## Calling a plugin backend Use the loader's WS route in `shared`: ```js await DeckyBackend.call("loader/call_plugin_method", "", "get_playback"); await DeckyBackend.call("loader/call_plugin_method", "", "volume", 0.6); ``` Do not call `window.__DECKY_SECRET_INTERNALS_..._deckyLoaderAPIInit.connect(...)` for ordinary probes. Each call replaces that plugin's event-listener map, so its frontend silently stops receiving `decky.emit` events. If intentionally simulating a frontend reload, restore ownership with `await DeckyPluginLoader.importPlugin("")`; this reruns the frontend bundle while keeping the backend alive. To prove the UI is live, sample changing text such as playback progress twice. A single DOM read only proves the current tree exists. ## Driving the UI (hard-won rules) - **DOM clicks work for gamepad activation**: `el.dispatchEvent(new MouseEvent("click", {bubbles: true}))` fires both `onClick` and Focusable `onActivate`. No need for synthetic gamepad input in most flows. - **Match by text prefix, not equality** — labels often carry count badges (`textContent.trim().startsWith("Liked")`). - **Navigation**: `cdp-nav.mjs` uses the raw Router on purpose; `@decky/ui`'s `Navigation` targets the focused window, which doesn't exist under CDP and lands elsewhere. Re-navigating to the current route does **not** remount the page — click a tab/home element to reset state. - **Screenshot assertions**: screenshot `bp`, then Read the PNG. For playback/progress checks, take two screenshots a measured interval apart and compare position deltas against wall-clock (rate ≈ 1.0×). For lists, scroll through and assert row count / dedup keys / layout. - After `cdp-nav.mjs` the page needs ~1s to settle before a screenshot is meaningful (the script already sleeps 900 ms). ## Pitfalls - Shell quoting eats probe code — write probes to a file and pass the path; don't inline JS in `-e`-style arguments. - `pkill -f "8080:localhost"` style cleanup can match your own shell's command line and kill it (zsh exit 144) — kill by PID instead. - The visible screen is `bp`, not `SharedJSContext` — screenshots of `shared` are blank. - After a Steam restart (desktop-mode round trip) the `bp` target gets a **new id** — re-resolve targets instead of reusing an id, or you read a dead window. - Overlay targets (`qam`, `mainmenu`) only render while the overlay is open on-device.