cmux NEXT: web UI edits show up live in a running NIGHTLY (web-dev.json)

Merged
TE
16d ago
+975-12

Summary

A running NIGHTLY (or Debug) cmux NEXT now picks up web UI edits while it runs. No Swift build, no relaunch, no wait for CI. Until now, pointing the app at a Vite server needed a launch variable that only Debug builds read (#16433, #18339), so the nightly could only show the web code it shipped with.

Debug and NIGHTLY builds now read ~/.config/cmux/web-dev.json. Release and RC builds never read it: WebDevOverride.isAvailable applies the DevTools rule. Debug Settings > Pages > "Live web reload (web-dev.json)" is on by default; switching it off ignores the file without editing it.

{"server": "http://127.0.0.1:4200/", "overlay": "~/Library/Application Support/cmux-next/web-overlay"}
  • server: a loopback Vite server, normally the developer's own (web-overlay.sh server --local). The React pages, the diff, markdown and editor viewers, and the bundled agent pane load their files from it, and edits hot reload. Each page keeps its own origin (cmux-page://<id>, cmux-agent://pane): the scheme handler fetches the file from the server and answers under that origin. The host bridge, the CSP and the trust checks stay the shipped ones.
  • overlay: built bundles that replace the bundled ones. web-overlay.sh fleet builds them on the fleet (cmux-ci build cmux, the Linux pool) and installs them; build uses this checkout's own web build. Each surface switches with one symlink rename, manifest.json is written last, and the app reloads when it changes.
  • Fallback: both are read on every request. A request the server can't answer (connection refused, 4xx, 5xx) gets the overlay file, else the bundled one. web-overlay.sh off restores the shipped pages.
  • Reload: WebDevOverrideWatcher watches the file and the manifest with vnode events, no polling. It reloads every web view the two scheme handlers have served. The first page served starts the watch, so the app's launch path is unchanged.
  • Dev server: the appHostServer plugin sets the Vite client's socket address through server.ws on any server with a fixed port, so HMR reaches the server when the page's origin isn't the server's. Vite 8 ignores server.hmr.host/clientPort, and vp dev passes an empty ws. The plugin also allows schemas/settings, which the Settings page imports. Test servers on port 0 are untouched.

Not covered: pages the engine policy hosts in Chromium keep their bundled files. CMUX_NEXT_PAGES_DEV_URL and CMUX_NEXT_AGENT_PANE_DEV_URL work as before and win over the file.

Turning it on (anyone on the team)

From any cmux checkout on your own Mac, with a Debug or NIGHTLY app that includes this PR:

scripts/cmux-next/web-overlay.sh server --local                       # this checkout
scripts/cmux-next/web-overlay.sh server --local --worktree <lane worktree>

It installs the webviews dependencies when missing, generates the string tables, starts the webviews dev server on 127.0.0.1:4200 (--port N), and points web-dev.json at it. Open pages and agent panes reload from it; later edits hot reload. web-overlay.sh server off stops it and restores the shipped pages; web-overlay.sh status shows what runs.

  • A shared slot: declare "slot": {"host": "<ssh host>", "port": N} in web-dev.json, on a fleet host from machines/fleet-registry.json (a CI mini or the GCP pool), never a personal machine. server --slot tunnels the same port number (HMR connects to it) and falls back to --local when the slot doesn't answer.
  • No dev server: web-overlay.sh fleet [--ref SHA | --working-tree] builds on the fleet and installs the web bundles as the overlay; install-app APP takes the bundles of any built cmux app.

The NIGHTLY must include this PR first, so the first nightly after it lands.

Longer term: native changes without a local build

Full plan in plans/cmux-next/web-live-reload.md. In short:

  1. Leo's own channel: a per-user appcast. The fleet builds the feat-cmux-next tip plus opted-in PRs on every merge, signs and notarizes the build, and Sparkle installs it at a quiet moment (UpdateRelaunchGate already waits for foreground commands). It's the nightly-next path with a smaller audience and a faster cadence, and a red tip publishes nothing.
  2. Incremental: Sparkle deltas between consecutive channel builds. Web bundles ship between native builds as overlays checked against the channel's manifest, so a web-only merge never waits for a native build.
  3. Electron-style shim: the cmuxPage bridge already acts like ipcRenderer.invoke/on. The missing piece is native capability. A cmux.native.* op namespace (menus, open/save panels, clipboard, open URL/reveal, notifications, window controls), granted per page like PageNativeOp, would let more UI move into hot-reloadable web code. Proposed only, not built here.
  4. Bridge skew is the one reliability risk left. A page newer than the app can call an op the app lacks. manifest.json records the build commit, and the next step is to refuse an overlay built for a newer bridge.

Testing

  • Web half, measured against a dev server running this branch, in headless Chromium. A proxy on another port stood in for the scheme handler, so the page's origin differed from the server's, as in the app.
    • The HMR socket went to the dev server's port. Before the server.ws fix it went to the page's own origin and no edit arrived.
    • An edit to App.tsx ran in the page in 0.47โ€“0.67 s (4 runs). A rule added to the shared stylesheet applied in under 0.1 s, and the first CSS edit applied without a reload.
    • /settings/, /history/, /cloud/ and /agent-pane/ loaded with no 4xx and no page errors. Chromium only; WebKit was not run.
  • web-overlay.sh ran on macOS against temporary directories: repeated installs (symlink swaps, manifest last, the two newest generations kept), a pages-only install that keeps the generation agent-pane still uses, a leftover .next link from a crashed run, server (a non-loopback URL is refused), off, status.
  • server --local on macOS (temporary config and state dirs, port 4291): the command returns in about 7 s with the dev server detached (the caller's pipe closes), /agent-pane/index.html and /settings/index.html answer 200, web-dev.json gets the server, status lists the process, and server off stops it and clears the key. server --slot with no declared slot falls back to local.
  • Not run: web-overlay.sh fleet (cmux-ci build, artifact, install) and server --slot against a real declared slot. The install-app step fleet ends with was run against the real NIGHTLY app.
  • Independent review (subagent): no blockers. Its should-fix items are fixed in c4bc1821; the local-first rework (61a619ec) changes only the script and docs: prune only unused generations, ln -sfn for leftovers, ~/ remote paths, one web-dev.json write per command (one reload), and a read after arming the manifest watch.
  • Swift: WebDevOverrideTests (parsing, the build gate, overlay roots, server answer, server down and 404 falling back to the bundle, overlay over bundle) and AgentPaneDevServerTests (URL mapping, other origins and dot segments refused). Not compiled or run locally: this Mac doesn't build the app, so CI is the first compile.
  • CI on 951d3b4b: one failure, defaults-snapshot (setting.appearance.backgroundRotation). It comes from Settings sources this PR doesn't touch, and defaults_guard.py check-fresh fails the same way on the base. The other 81 checks passed when last read.
  • Lints: check-concurrency, check-no-godfiles --base, check-main-actor-work, check-layers and check-module-resources report nothing from this change. scripts/web-lint.sh and prepush-gate.py report nothing from this change; the web lint's remaining findings (format issues in Settings files and agent-pane-links-blue.test.ts) come from the base.
  • Not verified yet: the switch inside a real NIGHTLY. That needs a nightly built with this PR.

Changelog

none

๐Ÿค– Generated with Claude Code

Agent session: cosmic-gray