back to blog

Day Seven — Rooms With Windows

Build 2.8 is out. One new surface in this release — a persistent graphical desktop, available on both Termux and WSL2, reachable from any browser through the tunnel that was already there — and with it, the shape of the platform shifts sideways. What was a command-line system until last week is now a command-line system that also happens to be a desktop. No new port. No new client. No second password.

The stack is the same on both sides. A headless Xvnc display bound strictly to loopback, Openbox layered on top of it, websockify bridging VNC to WebSocket while also serving the static noVNC 1.5.0 client from a single port. nginx handles the rest under /desktop/, and — this is the part that matters — the location is guarded by the same auth_request against /cli-auth.php that already protects the CLI. The bcrypt cookie is shared. One login opens /cli/, /desktop/, and everything else on the node. The new surface doesn't bring a new authentication story because there wasn't a good reason to invent one.

The nginx piece has more subtle edges than we expected. noVNC by default builds its WebSocket URL as wss://host/websockify, root-relative, which is fine if you own the root but useless when your proxy only exists under /desktop/ — so we force the path into the query string as path=desktop/websockify and let an exact-match location issue a canonical 302 to the right URL. Because Cloudflare Tunnel speaks plain HTTP to nginx rather than TLS, every redirect derives its scheme from X-Forwarded-Proto into a $real_scheme variable and uses $host rather than $http_host; otherwise the Location header leaks the backend :8080 and the browser bounces straight off the tunnel. The PHP dashboard card has its own version of the same problem — the CGI SAPI rewrites any relative Location against SERVER_PORT, so it has to emit an absolute URL with the port stripped by hand. Most of building this was finding out where HTTP assumptions leak, and closing each one.

The Termux build is the leaner of the two, and it earns that the hard way. Openbox with pcmanfm, tint2, xfce4-terminal, rofi, htop, Papirus icons reachable only after we symlink ~/.icons into $PREFIX/share/icons because tint2's search paths are compiled for mainline Linux and don't include Termux's prefix. websockify installs with --no-deps on purpose: the default pulls numpy, which on ARM tries to compile from source and sits there for hours. The real piece of work, though, is ~/.config/openbox/desktop-watchdog.sh. On Android, DBus autolaunch starts gvfsd without XDG_RUNTIME_DIR, so gvfsd never publishes its peer-to-peer socket, pcmanfm falls back to the session bus for every file-info lookup, and on the first user click the desktop window dies quietly — leaving openbox's default root menu exposed and all the icons gone. We spent more time than I want to admit chasing that bug through logs that kept saying nothing. The watchdog restarts gvfsd with the right environment before pcmanfm ever looks at the disk, then keeps pcmanfm and tint2 on a three-second heartbeat for the rest of the X session. We also had to strip Middle and Right mousebinds from the Root context in rc.xml so that in the brief window between a crash and a respawn a stray click doesn't pop the wrong menu. Nothing about this is glamorous. All of it is why the desktop now feels stable.

The WSL2 build is the heavier one, closer to a conventional Linux desktop — xfdesktop for the background and icon layer, thunar for files, xfsettingsd for theming, a real Google Chrome pulled directly from dl.google.com. The only quirk is that running as root inside WSL puts the WSLg runtime directory out of reach (it's owned by uid 1000), so autostart carves out its own XDG_RUNTIME_DIR at /run/user/0 before DBus launches. Past that, everything lights up the way you'd expect.

The quiet headline, if you want one, is that Claude Code installs cleanly inside both desktops. It used to live only in the NodePulse CLI shell. Now an agent session can sit in a tiled terminal next to pcmanfm, tint2, Chrome, whatever — same machine, same repo, reachable from any browser tab. That's a small sentence with a lot of weight behind it.

We tested the WSL2 build the way you'd expect. Opened Chrome inside the noVNC session. Typed the NodePulse URL. Pressed Enter. Chrome, being earnest, served us back the noVNC desktop we were already sitting in — and its Chrome, and its desktop, and so on, frames shrinking into a recursive vanishing point like a digital Droste. For about four seconds it was pure joy. Then nginx noticed it was upgrading WebSockets to itself, websockify started colliding session IDs between the outer and inner tabs, and the whole thing deadlocked so thoroughly that no subsequent attempt could reproduce the loop. Only a hard restart of the physical client shook it loose. Lesson logged. Nested noVNC-in-noVNC is, empirically, not supported. The screenshot is worth keeping anyway.

There's a version of this project we described in the first week as 'a network that knows itself'. What we built in this release is closer to the opposite — a node that shows itself. The desktops are unremarkable once you're inside them: a panel at the bottom, a file manager, a terminal, a browser, the usual furniture. The remarkable part is everything outside them — that the rendering is happening inside a process on a phone, or inside a WSL distribution on someone's laptop, that the pixels are arriving over a Cloudflare tunnel, that the cookie authorizing the connection was set by the same gate that protects the CLI, that an agent reading the repo from one tab is the same agent editing files from another.

A week ago the tunnel carried text. It now carries a room. That's enough.

end of transmission