cmux NEXT: web UI edits show up live in a running NIGHTLY (web-dev.json)
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 fleetbuilds them on the fleet (cmux-ci build cmux, the Linux pool) and installs them;builduses this checkout's own web build. Each surface switches with one symlink rename,manifest.jsonis 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 offrestores the shipped pages. - Reload:
WebDevOverrideWatcherwatches 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
appHostServerplugin sets the Vite client's socket address throughserver.wson any server with a fixed port, so HMR reaches the server when the page's origin isn't the server's. Vite 8 ignoresserver.hmr.host/clientPort, andvp devpasses an emptyws. The plugin also allowsschemas/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}inweb-dev.json, on a fleet host frommachines/fleet-registry.json(a CI mini or the GCP pool), never a personal machine.server --slottunnels the same port number (HMR connects to it) and falls back to--localwhen 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 APPtakes 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:
- Leo's own channel: a per-user appcast. The fleet builds the
feat-cmux-nexttip plus opted-in PRs on every merge, signs and notarizes the build, and Sparkle installs it at a quiet moment (UpdateRelaunchGatealready waits for foreground commands). It's the nightly-next path with a smaller audience and a faster cadence, and a red tip publishes nothing. - 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.
- Electron-style shim: the
cmuxPagebridge already acts likeipcRenderer.invoke/on. The missing piece is native capability. Acmux.native.*op namespace (menus, open/save panels, clipboard, open URL/reveal, notifications, window controls), granted per page likePageNativeOp, would let more UI move into hot-reloadable web code. Proposed only, not built here. - Bridge skew is the one reliability risk left. A page newer than the app can call an op the app lacks.
manifest.jsonrecords 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.wsfix it went to the page's own origin and no edit arrived. - An edit to
App.tsxran 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.
- The HMR socket went to the dev server's port. Before the
web-overlay.shran on macOS against temporary directories: repeated installs (symlink swaps, manifest last, the two newest generations kept), a pages-only install that keeps the generationagent-panestill uses, a leftover.nextlink from a crashed run,server(a non-loopback URL is refused),off,status.server --localon 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.htmland/settings/index.htmlanswer 200,web-dev.jsongets the server,statuslists the process, andserver offstops it and clears the key.server --slotwith no declared slot falls back to local.- Not run:
web-overlay.sh fleet(cmux-ci build, artifact, install) andserver --slotagainst a real declared slot. Theinstall-appstepfleetends 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 -sfnfor leftovers,~/remote paths, oneweb-dev.jsonwrite 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) andAgentPaneDevServerTests(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, anddefaults_guard.py check-freshfails 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-layersandcheck-module-resourcesreport nothing from this change.scripts/web-lint.shandprepush-gate.pyreport nothing from this change; the web lint's remaining findings (format issues in Settings files andagent-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