# DOM web backend The web backend keeps the existing Pardes core and replaces the old Emscripten/SDL/WebGL shell. Zig produces a `wasm32-freestanding` module with no imports. Vanilla JavaScript owns `requestAnimationFrame`, browser input, window sizing, and effect/IO dispatch; HTML and CSS render selectable, accessible text. There is no canvas. The default web build links Tree-sitter's C runtime and the Zig grammar inside that same import-free module. A small freestanding compatibility layer supplies the C ABI locally, so source panes retain the core's Tree-sitter highlighting without Emscripten, WASI, or browser libc imports. Other grammar tiers remain selectable with `-Dtree-sitter=minimal` or `-Dtree-sitter=full`. `src/web.zig` is the narrow WASM ABI. Events enter through exported functions, and `pardes_frame` writes the canonical `Surface` into a packed 20-byte cell array. `src/web/app.mjs` reads that array in one linear pass and patches stable DOM nodes. Core effects become browser operations (links, downloads, clipboard) or `pardes-io` custom events for a process-capable embedding host. Build a replay from a dump: ```sh zig build web \ -Dplatform=web \ -Dtarget=wasm32-freestanding \ -Dtree-sitter=zig \ -Ddump=test/web-snapshots/source-list.dump.zon ``` The output is in `zig-out/web`. Serve that directory over HTTP; browsers do not allow a useful WASM module load from `file:` URLs. The browser shell has no argv: every command-line flag `src/main.zig` parses — `--tty`, `--tty-toggle`, `-n`, `-l`, `--nested`, `-h`, and the positional file-or-directory — is native-only, because the wasm module roots at `src/web.zig` and never links `main.zig`. State comes from the embedded dump instead. What is genuinely absent is narrower than "no IO". The module has no threads and no host filesystem, so there is no startup config file, LOOK resolves against the build-generated source archive rather than disk, and no language BACKEND is compiled in (`zls_backend` is off for wasm — which also means `lsp.supports` is empty, so the core never even raises a language query there; `web.zig`'s prong for it is waiting for a host that links one). The EFFECTS themselves are all still emitted, each as a numbered code across the ABI: `spawn` 1, `write` 2, `resize_pty` 3, `open_link` 4, `save_file` 5, `write_dump` 6, `set_clipboard` 7, `lsp` 8, `watch` 9, `quit` 10, `new_file` 11, `pipe` 12, `read_clipboard` 13. Inbound, `pardes_output`, `pardes_set_cwd`, `pardes_eof`, `pardes_paste` and `pardes_lsp_response` exist to answer them. A plain page ignores most of those and gets a replay viewer; a process-capable host (the `pardes-io` events above) answers them and gets terminals. The core does not know the difference — that is the point of the seam. Two dependency-free test layers cover the boundary: ```sh # Real WASM in Node plus a small DOM double. zig build web-harness \ -Dplatform=web \ -Dtarget=wasm32-freestanding \ -Dtree-sitter=zig \ -Ddump=test/web-snapshots/source-list.dump.zon # Real headless Chrome, DOM rendering, and browser touch input. zig build web-e2e ``` Set `PARDES_CHROME` when Chrome/Chromium is not on `PATH` or in a normal Playwright/Puppeteer cache.