cmux NEXT: Cloud machines show Pausing…, Resuming… and Deleting…, and a failed change says why
Leo, 2026-10-29: "let's just get the cloud and its respective ui up to snuff" (and, on the composer: "as well as the ui usability of the cloud stuff").
Friction fixed: Cloud machine in-between states
The cmux-next backend (backend/packages/protocol/src/cloud-machine-ops.ts, domains/cloud-power.ts) answers Pause, Resume and Kill at once and runs the provider call after. Until it finishes, the record reads pausing, starting or deleting. If the call fails, the machine reverts and the record's error says why. The app hid all of that:
CloudMachine.init(next:)mappedpausingto Paused,startingto Provisioning anddeletingto Destroyed. A pausing machine read Paused and offered Resume. A resuming machine looked like a new machine being created.- The pause reason was read as
pauseReason, but the wire key ispause_reason, so idle pauses were never recognised as idle. - The record's
errorwas dropped. A pause that failed after the server answered OK left no trace.
This PR fixes those:
- States.
CloudMachine.Statusgainspausing,startinganddeleting. The stage, the sidebar machine header and the hover card show them as Pausing…, Resuming… and Deleting…, with no needs-attention badge. The header draws the on-its-way spinner (MachineLinkState.connecting, from #26786), and the hover card uses the attention tone. The strings are localized into all 21 locales ofCloud.xcstringsand the sidebar catalog. - Power actions. Pause and Resume are off while any change is underway, with a tooltip naming the change. Kill is off only while the machine is being deleted, so a machine stuck pausing or resuming can still be killed. The gate keeps any reason an earlier target-reason binding set, so it composes with #27944's state gating whichever lands first.
pause_reason. It is read as the backend spells it.pauseReasonis still accepted.no_report(the cost backstop) counts as an idle pause.error. It maps toCloudMachine.lastChangeFailure. A failed machine's stage shows that message. A machine that reverted (for example back to Paused) shows "The last change to this machine failed: …" in its header tooltip.- From the click. Pause, Resume and Kill show the change as soon as you click (
pendingChange). If the call throws, the machine goes back to its real state. The next list read replaces the record. - Settling. The machine list is not polled. While a listed machine is changing, the list is read again after 3 s, at most 40 times in a row. The reads stop when nothing is changing, when a read fails, and on
CloudService.stop()(a read already in flight then schedules nothing), untilstart(). That covers the timer's stop conditions and cleanup.
Risk
Medium. Behaviour changes are named here:
- After a Kill, the machine stays in the sidebar as Deleting… and disconnected until a list drops it. Before, it was removed at once and could reappear when the next list still held the deleting record.
- The CLI and MCP machine status (
cloud_machines.rs) now reportspausing,startinganddeletinginstead of folding them into paused, provisioning and destroyed. A deleting machine reportsdeleted: falseuntil it is gone. - One bounded re-read loop (above).
- The sidebar header's status vocabulary grows by three.
plans/cmux-next/sidebar-owner.mdnotes it, and the commit carries aSidebar-Look:trailer.
Cloud.xcstrings shows a large diff only because of how its JSON lines up. Under git diff --histogram it is 1,320 lines of pure additions (10 keys × 21 locales). The code is about 620 lines.
Overlap: none of these fix the in-between states, so this is not a duplicate.
- #27944 and its carry #28208 gate Pause/Resume by paused or live. Both bind through
setTargetReason. This PR's gate keeps any reason bound before it. #28208'spauseResumeMismatchshould do the same, or it should run before this gate, so the two add up. - #28010 and its carry #28229 route records through
apply(_:to:read:). This PR only addschanges.listed(list)afterreconcile, andisLivestill countsstartingas live, so the two compose. Whichever lands second has a one-line conflict inrefresh(). - #28011 adds a Disconnected header. It touches the same
SidebarMachine.Status, header and string lists, so expect textual conflicts and no semantic clash: Disconnected is a link state, and these are record states. - #26872 (landed), #27031, #27104, #26791, #26786 and #27923 touch some of the same files but not these mappings or states.
Screenshots
No capture: the stills below are AACloudMachineChangeGalleryTests renders of the real SidebarListView, made on a fleet Mac with a stubbed machine status. A live cmux-ci capture would need a real Cloud machine caught mid-pause, and the capture fixture has no Cloud account (fleet-registry: the cloud-capture environment has no secrets yet).
These come from AACloudMachineChangeGalleryTests on a fleet Mac. They show a stubbed staging machine, staging-vm, with two workspaces, in dark and light. Each workspace row's subtitle carries the machine status.
The columns are: before, where a pausing record read "Paused" (and offered Resume); then after, showing Pausing…, Resuming… and Deleting….

| Before (pausing read as Paused) | After | |
|---|---|---|
| Dark | ![]() | ![]() |
| Light | ![]() | ![]() |
Follow-up, not in this PR: in app-server mode, Pause, Resume and Kill still call the /api/vm REST methods instead of cloud.machine.pause/start/delete. That is the same as on base.
UI review checklist
I ran design/ui-review-checklist.md:
- The header uses the existing status column and the spinner that #26786 draws for a machine on its way. This PR adds no new layout, animation, narration or highlight.
- The states are shown, not narrated. Copy is single-spaced, and the row keeps one weight.
Tests
CloudMachineChangeStateTests(new) covers every wire status,pause_reasonin snake case and camel case,errormapping (including/api/vmdecode), stage and strings, header status, words and tone, the failed-change tooltip, show and roll back from the click, settling, and the gate rules with the bound actions' target reasons.AACloudMachineChangeGalleryTests(GUI,CMUX_TEST_REQUIRE_GUI=1) renders the screenshots above.- At 0b146c3, a fleet compile (
cmux-ci compile-check --app) passed for the app and cmux-tui. On a fleet Mac,CloudMachineChangeStateTests,CloudIdlePauseTests(21),CloudMachineUnnamedWorkspaceTests,CloudMachineCapabilityTests,CloudMachineCostTests,CloudListMachinesTests,CloudTrackedResultsTestsandMachineLinkStateTestspassed.AACloudMachineChangeGalleryTestspassed at 9c0fb6c, after the rebase.
Agent session: agent session
🤖 Generated with Claude Code



