Cloud sidebar: back to one big Freestyle machine hosting many workspaces (#11762)

Merged
AU
2mo ago
+1308-259

The model

Back to one big machine, many workspaces — the shape cmux Cloud had on Blaxel — now on Freestyle: a single Freestyle VM hosts many cmux-tui workspaces, and the right sidebar shows the machine with its Workspaces group (+ New Workspace; open / rename / close per workspace), then its Terminals / Displays / Ports / Browsers. Not "1 VM = 1 workspace", and never a flattened per-workspace-VM list.

Nothing built for the sidebar since is removed: port links, pending-create rows, the working context menus, tab-placement opens, Copy IP Address, .internal names all stay.

What drifted, and how it is restored

The drift landed in #11626 (2026-09-02, "workspaces-first"), after the Blaxel removal (#11566):

DriftEffectNow
VS Code-style folder-chain compaction folded "Workspaces" + a lone workspace into one row, Workspaces / mainOn exactly the fresh machine where a person makes a second workspace, the group and its + vanished; the tree read as "this machine is one workspace"Compaction removed (it only ever fired on this chain). The Workspaces group is always its own row with its own +; a workspace row shows its own name
An empty machine rendered a bare "No workspaces yet" placeholder instead of the groupNo + on the tree for the first workspace (only the machine menu)The group stays, with the placeholder as its child; + creates the first workspace
Pools (Terminals, Displays) sat above the Workspaces groupThe machine's face was its pools, not its workspacesOrder under a connected machine: Workspaces → Terminals (only terminals no workspace views) → Displays → Ports → Browsers — the order cmux vm tree already prints and the skill documents

The per-workspace member list (terminals it views, then browsers, then pinned displays) moves to Sources/Cloud/CloudTreeRemoteWorkspaces.swift and is shared with the socket.

Freestyle wiring

No web change was needed: the Freestyle driver already runs one cmux-tui session daemon per machine (server start --session cloud), whose durable state root is the platform default under /root (the persistent volume), so workspace create / rename / close inside one VM are the multi-workspace primitives and survive a daemon restart. The driver, the devbox image, and the build script have no per-workspace-VM logic; the only Blaxel mentions left are historical ("ported from the retired Blaxel image") plus the retired-provider regression test and the Vercel env audit's legacy-key list, which are correct as history. The one Blaxel-specific fixture (create-coordinator tests) now uses the Freestyle provider and a Freestyle snapshot id.

Parity (sidebar ↔ CLI), every verb intact

vm workspace new / open (new, --here, --tabs, --pane + side) / rename / close / rm, surface new-terminal --remote-workspace, and every row verb are unchanged. Two parity bugs surfaced by the many-workspaces flow are fixed through one shared resolution (CloudTreeNodeBuilder.lookupRemoteWorkspace):

  • vm.workspace_open read only a terminal's first view, so a terminal viewed in two workspaces was silently skipped under the second; it now counts every view and opens the row's own set. It also accepts an unambiguous workspace name.
  • An existing but empty workspace answered "workspace … not found" from the socket and "has no workspace" from cmux vm open <m>/<ws>. The socket now says Nothing to open: … cmux vm open <m>/<ws> starts a terminal there (D9: the row opens nothing for it either), and vm open <m>/<ws> resolves it from the machine's own workspace list and starts a shell in it, as its help already promised.

Found while dogfooding on Freestyle (fixed here)

  • Doubled "Creating…" row. The stand-in row for a create lived until the cmux vm new process exited, but the CLI's open step runs 60–240 s when the link cannot connect, and the fleet list (or the catalog) already showed the machine — "troll · Creating…" above "troll". The create launcher now streams CLI output, the coordinator captures the stable machine=<id> token as soon as the machine exists, and the stand-in steps aside when that id has a row (fleet or catalog). Never by label or timing.
  • cmux vpn up said "already up" and changed nothing. Liveness was decided by the tunnel-side address, and every enrollment gives this Mac the same one (100.64.0.1), so a cmux interface left up for the production account read as "this tunnel is up" after a dev build enrolled and wrote its own config — every private route stayed dead with nothing saying why. The app now records the digest of the config vpn up actually brought up (bound to the digest the enrollment handed the CLI, so a rewrite in between cannot mislabel the active peer), reports stale, and vpn up replaces a stale tunnel; the sidebar's link error leads with the tunnel as the blocker.
  • One tunnel per deployment, not per Mac. The root cause of the above: cmux.conf / the cmux interface / the /etc/hosts block were singletons, so a dev build (staging, the dogfood account) and the production app on one Mac could only replace each other's tunnel. VMTunnelManager is now scoped to the Cloud VM deployment (cmux for production, cmux-staging / cmux-local / cmux-dev otherwise), the completed config routes only the network's own /24 and /64 (the platform's 10.0.0.0/8, fd00::/8 could never be installed twice), liveness checks THIS interface's wg-quick name file, and vpn hosts publishes a per-scope block. A dev build and the production app can now both be up.

Trade-offs (stated, not absorbed)

  • Order change. Displays and the (orphan-only) Terminals pool now sit below the Workspaces group. Anyone on the last two days' builds sees the pools move down. Chosen because the model is workspace-centric and the CLI/skill already print workspaces first.
  • Compaction is gone entirely, not gated: the only chain that ever nested deep enough to fold was Workspaces → workspace, i.e. the exact case the model needs visible. Keeping the code as dead configuration would be worse.
  • Empty workspace via vm workspace open is still an error, not an implicit terminal — that keeps D9 ("open never creates") for the sidebar verb; vm open <m>/<ws> is the verb that creates.
  • Existing production tunnels re-apply once. A tunnel brought up before the applied-config record existed reads as stale on the first cmux vpn up and is bounced (down, then up) so the record can be written; links resume within their resume lease. Also, the narrowed AllowedIPs only take effect on the next vpn up re-enrollment.
  • Scope is derived from the API host, not from the signed-in account: two dev builds pointed at the same deployment share one dev tunnel, which is correct (same account, same network); a Debug build pointed at production (--prod-auth) shares the production tunnel with the production app, also correct.
  • The titlebar's cloud split-button (removed in #11626 as part of an unrelated titlebar refactor) is not restored: it only opened the New Machine sheet, which is machine-centric, and the issue is about the right sidebar.

Tests

  • Red commit: cmuxTests/CloudTreeOneMachineManyWorkspacesTests.swift (Swift Testing) pins the shape — lone workspace keeps its group row and +, workspaces lead then pools, empty machine keeps the group, several workspaces under one machine each with their own terminals — and fails on main. Green in the next commit.
  • Parity: lookup by id / unique name / every view, drag group == open group, empty-but-existing → found with no members, duplicate names → ambiguous.
  • MachinesPanelModelTests.testCloudTreePoolsThenWorkspacePointerLists still described the pre-#11626 full pool and "no Ports group"; it was red-but-tolerated in the sharded unit run (assertion failures never turn a shard red). Rewritten to the target shape as testCloudTreeWorkspacesLeadThenPools.
  • Localization audit: no new user-facing strings; the placeholder under the group reuses cloudTree.placeholder.noWorkspaces (en + ja present). Socket error text is agent-facing English like its siblings.

Follow-up

#11805 (stacked on this branch) reshapes the groups under a machine to Workspaces, Terminals (every terminal resource, detached ones greyed), Ports, VNC Displays and makes a workspace folder exactly its layout. This PR stays the one-machine-many-workspaces restoration.

Live verification

Real Freestyle machine, daemon side (done). Against the staging deployment's Freestyle VM vm-963942516e2b486dba8c4f8ed09b2967 (image sh-940ec3bc…, host freestyle-vm, cmux-tui daemon listening on *:1337), through the control plane's exec API, with the exact argument shapes CloudTuiCommandLine sends — one machine, many workspaces, end to end:

StepResult
session current snapshot[] — an empty machine (the case the sidebar now shows as Workspaces → No workspaces yet)
workspace create --name alpha / --name betaws_55093d78…, ws_605920879… — two workspaces in the one VM
workspace <beta> run -- bash -lc …term_0ba768df…, and the snapshot places it under beta only (terminal_hits: 1)
workspace <alpha> rename --name alpha-renamedname changes in place, id and index stable
workspace <alpha> close (keep-terminals close, vm workspace close)revision 5, workspace gone
terminal <term> close then workspace <beta> close (the sidebar's Close Workspace / vm workspace rm path)revisions 6–7
final snapshot[] — back to the initial state, nothing stranded

Sidebar leg on this Mac: blocked on one user action. The Debug app's links to that machine cannot connect from here: staging machines live on a different Freestyle VPC than this Mac's active WireGuard tunnel (which serves the user's production machines on 10.16.179.x), cmux vpn up for staging needs sudo (not available to an agent, and it would overwrite the shared ~/.cmuxterm/wireguard/cmux.conf), and the fleet Macs have neither WireGuard nor sudo. Production would reach real machines through the existing tunnel, but the tagged app's cmux auth login opens an ASWebAuthenticationSession that lands on GitHub's login form in Chrome, which only the account owner can complete. The shape the sidebar renders for that machine is pinned deterministically by CloudTreeOneMachineManyWorkspacesTests (strict lane results below); the remaining manual step is in the handoff.

Unit tests in CI. While this PR was open, cmuxTests did not compile on main (#11529 added a test for a CLI-only type; fixed by #11770, tracked and closed in #11785), so the strict test-e2e.yml lanes for cmuxTests/CloudTreeOneMachineManyWorkspacesTests and cmuxTests/MachinesPanelModelTests failed at the build step for reasons unrelated to this branch. The branch is rebased on a main that carries #11770; re-run those two lanes to see them green.

Closes #11762

🤖 Generated with Claude Code

https://claude.ai/code/session_018kYegCjuiNUKXASs3WZHDh