| name | instrument-ionwake-browser |
| description | Drive and read Ionwake's `__ionwake*` window instrumentation hooks (`__ionwakeScene` on /play, `__ionwakeShipyard` on /shipyard, and the dev-only `__ionwake` debug flags) from outside the page via the Chrome DevTools MCP. Use to read live sim/scene/clock state, screenshot the running app, drive the shipyard, load ship models, inspect a ship schema, log the WebTransport wire, or keep a backgrounded tab stepping. |
Ionwake installs diagnostics/test hooks on window so an agent can read scene state and drive the viewers from outside the page, without patching source.
They are presentation-adjacent tooling, never part of the game loop. Drive them with the Chrome DevTools MCP evaluate_script tool (cheap JSON, no screenshot).
Reach for take_screenshot only when you need to see the result.
| Hook | Page | Installed by |
|---|
window.__ionwakeScene | /play | frontend/src/play/scene.ts:257 (typed at :75) |
window.__ionwakeShipyard | /shipyard | frontend/src/shipyard/shipyard.tsx:404 (typed at :98) |
window.__ionwake (dev only) | /play | frontend/src/play/net-debug.ts |
The two __ionwake{Scene,Shipyard} hooks are page-scoped: each exists only while its page is mounted and is deleted on navigation away.
__ionwake is the exception: a dev-only debug namespace (stripped from production builds via import.meta.env.DEV, so it is undefined on a deployed site) that persists across navigation, so a flag you set stays set.
Prerequisites: dev servers must be running
__ionwakeShipyard needs only Vite (:3000).
__ionwakeScene's own/clock/remotes stay undefined until the Rust server (:8080) feeds the first snapshot, so /play needs both.
Check (this is the readiness probe, run it first):
curl -s -o /dev/null -w "frontend :3000 -> %{http_code}\n" http://localhost:3000
curl -s -o /dev/null -w "server :8080 -> %{http_code}\n" http://localhost:8080/health
Per AGENTS.md, assume both servers are already running and restart on source changes (Vite via
HMR, the Rust server via mise dev:server's watcher), so do not start or restart them in the
background. If a server is down, ask the user to start it (the tasks, for reference, are below).
mise dev:frontend
mise dev:server
Run (agent path): drive the hooks via the MCP
Open the page in a tab, then call evaluate_script. The MCP awaits returned Promises, so an
async function body can await __ionwakeShipyard.loadModel(...) directly.
Do not hesitate to add capabilities to these APIs and edit this file to update documentation.
__ionwakeScene — the /play scene
| Member | Kind | Returns | Notes |
|---|
own | getter | {x,z,yaw,vx,vz} | undefined | own ship's rendered pose; yaw is instant-aim heading |
remotes | getter | Array<{id,x,z,yaw}> | every tracked remote ship |
clock | getter | {renderTick,offset,lastSnapTick,window} | undefined | render-clock/window diagnostics |
bolts | getter | number | live count of bolts |
explosions | getter | number | live count of explosions |
fps | getter | number | engine's smoothed FPS |
setHeadless(on) | method | void | swap the rAF loop for a Web-Worker heartbeat so the tab keeps stepping at ~60 Hz even when backgrounded |
setHeadless(true) keeps a backgrounded tab rendering+stepping — needed when driving two /play
tabs at once, since the MCP can only foreground one.
__ionwakeShipyard — the /shipyard viewer
| Member | Kind | Returns | Notes |
|---|
getParams() | method | Controls | colorize controls; colors are #rrggbb strings, not Color3 |
setParams(patch) | method | void | merge a partial Controls |
resetParams() | method | void | restore the source-material defaults |
setCamera({alpha?,beta?,radius?}) | method | void | nudge the orbit camera; any subset |
loadModel(name) | method | Promise<void> | swap the displayed model; await it |
getSchema() | method | ShipSchema | null | sockets + authored stats; null before a model loads |
The shipyard opens with no model loaded, so getSchema() is null and params are defaults until
you loadModel. Pass a model name from SHIP_MODELS (frontend/src/ship/load-ship-model.ts) —
star-sparrow/model-1 is one. Navigate to http://localhost:3000/shipyard, then (verified: the
schema's weapon/thruster arrays populated and params returned the new hex colors):
async () => {
const api = window.__ionwakeShipyard;
await api.loadModel('star-sparrow/model-1');
api.setParams({ primaryColor: '#00e5ff', secondaryColor: '#ff007a', glow: 3, emission: 2 });
api.setCamera({ alpha: -1.2, beta: 1.1, radius: 22 });
const schema = api.getSchema();
return {
params: api.getParams(),
weapons: schema?.weapons?.length,
thrusters: schema?.thrusters?.length
};
}
__ionwake — dev-only debug flags
A persistent dev-only namespace for transport debugging, installed when /play opens. It survives
navigation (unlike the page hooks) and is stripped from production builds, so on a deployed site it
is undefined.
| Flag | Effect |
|---|
logMessages | When true, every decoded client/server message is console.logged in both directions: [net send] input … (60 Hz) and [net recv] snapshot … (20 Hz). |
() => { window.__ionwake.logMessages = true; }
The frames are opaque binary in DevTools (a custom postcard protocol) and no browser surfaces
WebTransport payloads, so this hook is the way to read the wire. The flag persists across reloads, so
to capture the one-time welcome (which arrives at connect) set it and reload.
Gotchas
__ionwakeScene.own/clock/remotes are undefined without the Rust server.
With :8080 down, /play renders the grid alone and these never populate.
They also start undefined for a moment after load until the first snapshot seeds the clock — poll, don't read once.
- The shipyard starts empty.
getSchema() returns null and getParams() returns defaults
until you await loadModel(...). Calling setParams before a model is loaded is a no-op visually.
loadModel returns a Promise. In evaluate_script use an async body and await it; the MCP resolves the returned promise.
A non-async caller reads stale schema/params.
- Hooks are page-scoped and deleted on navigation.
On
/play, __ionwakeShipyard is undefined (and vice versa).
Re-check typeof after any navigate_page.
- Getters return live/reused state.
own and remotes hand back the simulation's reused buffers; serialize what you need inside the same evaluate_script call rather than stashing the reference for later.
- Shipyard colors are hex strings.
setParams({ primaryColor: '#00e5ff' }), never a Color3.
Troubleshooting
- The user might play around automated pages while you're working.
If opened tab/pages change unexpectedly, assume this is the reason and restore as needed to continue working.
window.__ionwakeScene is undefined: you're not on /play (or it just unmounted).
Navigate to http://localhost:3000/play and re-check.
own/clock stay undefined on /play: the Rust server is down.
curl :8080; it normally runs already and restarts on changes, so if it refuses the connection ask
the user to start it.
getSchema() is null on /shipyard: no model loaded yet.
await __ionwakeShipyard.loadModel('star-sparrow/model-1').
- MCP page list is empty / nav fails: see the
chrome-devtools-mcp:troubleshooting skill.