microscope web serves the React application in web/ and connects every tab
to one debugger session over WebSocket. The server is a DebuggerHandle client,
like the CLI and DAP server.
| Path | What lives there |
|---|---|
src/web/mod.rs, src/web/session.rs, src/web/connection.rs |
HTTP/WebSocket server and debugger-session ownership |
src/web/protocol.rs |
Rust protocol types and TypeScript generation source |
src/web/assets.rs, src/web/auth.rs |
Built-page serving and access control |
src/web/inspect.rs, src/web/values.rs, src/web/draw.rs, src/web/profiles.rs |
Server adapters for major page features |
src/web/syscalls.rs, web/src/profile/syscalls.ts, web/src/ui/profile/SyscallsView.tsx |
A profile's Syscalls view; see system call tracing |
web/src/store.ts, web/src/connection.ts, web/src/actions.ts |
Browser state, transport, and user actions |
web/src/router.tsx, web/src/tab.ts, web/src/ui/ |
URL state, per-tab view, and React interface |
web/src/protocol.gen.ts |
Generated TypeScript protocol; do not edit by hand |
web/src/visualize/, sdk/web/ |
Sandboxed visualizers and their public type contract |
web/test/ |
Fast TypeScript, React, replay, and sandbox tests |
web/e2e/, tests/web/ |
Browser end-to-end tests and Rust server tests |
web/vite.config.ts, web/playwright.config.ts |
Browser build and end-to-end configuration |
The server sends tab state and notices. A tab sends requests that include the
StopId of the state it read. Cache read answers by stop and request. Forget
them after writes, setting changes, and session changes.
The address bar contains the tab's complete view: stop, thread or task, frame, source, panel, and memory location. A shared link must open the same view.
Formatting shared with other clients belongs in src/present.
web/src/protocol.gen.ts is generated from src/web/protocol.rs with ts-rs.
A test detects stale output. Regenerate it with:
MICROSCOPE_UPDATE_PROTOCOL=1 just test protocolDebug builds read the page from build/web at runtime. Release builds embed
it.
just web
just web-test
just web-e2e
just web-e2e --project=chromium -g NAMEjust web-test runs TypeScript, Biome, and Vitest. End-to-end tests run real
servers in Chromium and Firefox.
Replay tests use recorded server transcripts. Update them with just web-transcripts when the protocol change is intentional.
To inspect the page, run:
just web-probe PROGRAM STEPS...The probe saves a screenshot after each step and prints browser-console
messages. web/e2e/probe.ts lists the available steps. just web-dev starts
the live-reload server.
Use binaries in web/node_modules/.bin or the Nix shell's biome. Do not use
npx, which can download a different package when a command is missing.
src/web adapts DebuggerHandle to WebSocket messages. It owns authentication
tokens, session joining, per-connection state, protocol conversion, source and
profile responses, and static assets. It does not own debugger semantics.
A connection receives session events and sends requests tagged with the state the tab observed. The server validates inputs before calling the handle and returns notices or typed answers the page can render.
Keep blocking file work and profile loading away from the controller thread. Backpressure every stream that can be driven by a browser. A disconnected tab must release its subscriptions, pending replies, drawing work, and held child decisions.
Rust records in src/web/protocol.rs are the protocol source. ts-rs generates
web/src/protocol.gen.ts. Handwritten TypeScript wrappers in
web/src/protocol.ts may add client helpers but must not duplicate wire
records.
After a Rust protocol change, run the generator through the test path and inspect both sides:
MICROSCOPE_UPDATE_PROTOCOL=1 just test protocol
git diff -- web/src/protocol.gen.tsThen run the ordinary test without the update variable. A stale generated file must fail CI.
Protocol changes need an explicit compatibility decision. Development server and page builds normally move together, but replay transcripts and an old open tab can expose ordering and missing-field problems. Prefer additive fields with clear defaults when that costs little.
web/src/store.ts owns shared application state. connection.ts owns the
transport lifecycle. model.ts and data.ts normalize wire data. UI
components select state and issue actions; they should not maintain private
copies of debugger truth.
State has several lifetimes:
- session: target, modules, settings, loaded views;
- execution: running operation and pending control state;
- stop: threads, tasks, frames, variables, memory, and disassembly answers;
- tab: selected context, panel, paths, expanded values, and pinned drawings;
- component: transient focus, hover, and layout state.
Put data in the narrowest lifetime that owns it. On a new stop, forget every
cached answer tied to the previous StopId. On session replacement, also
forget module, view, and target state. A settings or memory write can require
more selective invalidation within one stop.
router.tsx, tab.ts, and UI path helpers encode a tab's complete navigable
view. A copied URL should restore the same stop where retained, execution
context, frame, source, panel, memory address, and pinned values.
URL parsing is an untrusted boundary. Reject or normalize invalid IDs, paths, indexes, and duplicate fields. Do not issue debugger writes merely by opening a URL.
When adding durable view state, update parsing, serialization, back/forward behavior, sharing tests, and unknown-value fallback. Ephemeral state such as a hover or open menu should stay out of the URL.
actions.ts, keys.ts, palette.ts, and focus.ts form one command system.
Buttons, keyboard shortcuts, and the command palette should invoke the same
action rather than duplicate control logic.
An action declares when it is available from current state. Disabled actions should explain the missing condition where the UI has room. Keyboard handling must respect text inputs, dialogs, renderer canvases, and platform modifier conventions.
Focus is part of accessibility and debugger speed. After a stop or panel change, move focus only when the user's current focus is no longer meaningful. Do not steal it from the console or an expression editor on every event.
Value trees request children by page and retain stable expansion paths. Never load an entire large sequence to render its first screen. Virtualize long tables and lists, and keep exact integer text separate from rounded chart labels.
Every request uses the value reference and stop that produced the row. A stale reply must not replace data from a newer stop. Cancellation saves browser work but does not replace server-side stop validation.
Memory, source, and disassembly views follow the same rule: cache by complete request key and stop, then discard on invalidation.
Renderer JavaScript is program-provided input. It runs in a worker sandbox
with a narrow microscope drawing API. It receives only inputs evaluated by the
debugger and cannot fetch arbitrary memory or access page state.
The host enforces time, concurrency, shape, byte, image, and message limits. It validates every returned picture before drawing. A renderer that throws, times out, loses its WebGL context, or returns invalid data fails its card and does not break the workspace.
Live renderers receive bounded pointer and key events. The debugger's function keys remain reserved, and Escape returns focus. Stop worker and graphics resources when a card leaves its lifetime.
sdk/web/microscope-visualizer.d.ts is the public renderer API. Type-check
built-in renderers against it and update public visualizer docs with any API
change.
Profile pages use typed server queries over validated captures. Flame trees,
functions, timelines, zones, source costs, and diffs should consume
src/analysis results. Do not reimplement aggregation in TypeScript.
Large profile views use virtual rows, bounded plot bins, and workers where decoding or drawing would block input. Keep query state in the URL when it is part of a shareable investigation.
Replay tests feed recorded server traffic to the real client. They are useful for stop sequences, multiple tabs, values, profiles, and low-level state without a native process.
Transcripts are generated artifacts from Rust web tests. Do not edit JSONL by
hand. Rerecord with just web-transcripts, inspect the semantic changes, then
run just web-test.
A transcript should be small enough to explain its scenario. Add a new one only when existing traffic cannot represent the behavior.
Playwright tests own user-visible browser/server workflows. Use semantic locators and observable state, not sleeps. Each server and inferior is owned by the test harness and must end after failure as well as success.
just web-probe is for human inspection. It drives named steps, saves
screenshots under the target output directory, and prints console messages.
Use it after layout, theme, panel, focus, or responsive changes even when DOM
tests pass.
just web-dev serves Vite on its development port and microscope on a separate
port with the allowed origin configured. It drives a host browser and is one
of the recipes that deliberately leaves the pure build boundary where needed.
For a web change, identify which inputs are untrusted:
- WebSocket requests and URL fields;
- source text and program strings;
- renderer JavaScript and pictures;
- profile and capture contents;
- file paths and reverse-proxy headers.
Escape text at rendering boundaries, validate sizes before allocation, and do not turn program strings into HTML. The listen address and reverse proxy own network access; a session token is not a replacement for authenticating a public deployment.
- Is debugger behavior still implemented below the web adapter?
- Does every stopped request carry the observed
StopId? - Are caches invalidated at the right lifetime boundary?
- Does URL state round-trip and handle old or malformed values?
- Are keyboard and focus behavior accessible?
- Are large results paged or virtualized?
- Does untrusted renderer or profile data stay bounded?
- Are generated protocol files and transcripts current?
- Do unit, browser component, Chromium, and Firefox tests cover the change at the appropriate level?