Skip to content

iOS: complete Cloud workspace parity - #15935

Merged
azooz2003-bit merged 238 commits into
mainfrom
feat-cloud-parity-ux
Oct 1, 2026
Merged

azooz2003-bit merged 238 commits into
mainfrom
feat-cloud-parity-ux

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Completes iOS Cloud parity with paired Macs across workspace creation, computer management, terminal routing, browser presentation, and workspace actions.

  • Cloud machines appear in the All Computers creation picker and use the same shared workspace and terminal paths as paired Macs.
  • Cloud computer rows support the paired Mac hide/show toggle, swipe actions, context menu, hidden-row ordering, and per-row workspace creation.
  • Cloud terminal input, output, local emulation, browser mode selection, title menus, close confirmation, and toolbar/composer layout use the shared shell behavior.
  • Cloud onboarding explains the workspace topology, includes the System VPN toggle, and links to a visual VPN explanation.
  • Cloud machine deletion confirms from the row action. The New Machine sheet matches the Mac feature set: size, plan locks, usage, upgrade, Create, and Cancel, with no Type picker.
  • Restores main’s newer All Computers picker and SSH target model after the Cloud merge.

Verification

  • 143 CmuxMobileCloud tests passed across 13 suites.
  • 51 CmuxMobileCloudBridge tests passed across 10 suites.
  • 22 Cloud API tests passed across 3 suites.
  • python3 scripts/verify-local.py --only test-wiring passed.
  • Fleet iOS archive and isolated simulator install passed for tag cparity; bundle IDs for the app and both extensions were verified.
  • Independent remote iOS verification passed at artifacts/verify-remote/20260930-025234-cparity-ios-cmux-app-review-v.
  • Physical iPhone installation was unavailable because the connected device was unreachable; no physical-device behavior is claimed.

Changelog

Changed: Cloud workspaces now follow paired Mac workspace, terminal, browser, and computer-management behavior on iOS.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Completes iOS Cloud parity with paired Macs: Cloud machines appear in the workspace list and All Computers picker, open into the same detail screen and terminal, and are managed from the Cloud tab, including from the iPad sidebar.

Cloud workspaces and terminals

  • Cloud machine rows support the paired Mac hide/show toggle (persisted; hiding the open machine pops back to the list), swipe actions, context menus, status, and per-row workspace creation. New Workspace and New Terminal create on the machine's daemon and open the new row, New Browser routes through the system VPN, and the workspace-list destination picker offers Cloud machines alongside connected Macs, excluding paused or disconnected machines as creation targets.
  • Machine lists and catalogs retry on their own with backoff; a machine whose daemon refuses a connect shows the server's reason on its row with Try Again, and a provisioning machine re-reads itself until it settles. Mac teardown, team switches, and a redialing Mac with no rows never mark a Cloud-served list as down.
  • Daemon transport is a prebuilt Rust client pinned by release tag and checksum in the CmuxTerminalClient Swift package; a new workflow publishes its xcframework.
  • Cloud terminals use the shared shell behavior, resolve to their real workspace and name via the daemon's session snapshot, take the phone's grid, use the phone's local pixel scroll path, buffer offline keystrokes, serialize attaches, re-attach after a lost link, and re-paint from a fresh snapshot when the daemon's grid moves. Reconnect drops the machine's link and re-reads its catalog. Machines without a label use the control plane's generated name or short id, never the dev build tag.
  • Cloud onboarding explains the workspace topology, includes the System VPN toggle, links to a visual VPN explanation, and uses the app's first-run onboarding visual design. An account with no Mac reaches the Cloud tab from a two-tab bar on the no-computers screen.

System VPN and lifecycle

  • The System VPN is a separate packet-tunnel peer that runs only while enabled and lets Safari and other apps reach private Cloud addresses. It is bounded and serialized with deadlines that apply even while queued, replacements are transactional, a timed-out enrollment never installs, late completions reconcile against the final state, and a successful start with a transient disconnected status keeps transitioning until iOS reports the live state. Sign-out revokes it through the same gate, with failed revocations persisted per account and team scope, bounded in storage, for the next sign-in; cross-account switches defer them. The installed config is route-validated before saving, and unavailable devices never touch VPN storage.
  • Pause, resume, and delete run through one controller path with per-row status and confirmations; the New Machine sheet matches the Mac.

Rollout note: Release lanes are not updated yet. The TestFlight/App Store upload paths still sign exactly one extension, and the release App IDs need the Network Extensions capability.

Written for commit 2337a9a. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a Cloud tab for browsing and managing cloud machines, creating workspaces and terminals, and connecting to remote terminal sessions.
    • Cloud machines now appear in the workspace list with connection status, visibility controls, and workspace creation.
    • Added optional System VPN access to private Cloud machine addresses, along with Cloud onboarding.
    • Added controls for creating, pausing, resuming, and deleting machines.
  • Improvements

    • Cloud catalogs and terminal sessions recover from interruptions with retries and preserved workspace listings.
    • Cloud terminal input and screen updates are retained during reconnection.
    • Added clearer status, error, and recovery messages across Cloud and VPN screens.

azooz2003-bit and others added 30 commits September 25, 2026 12:03
…in layer

The iOS app has no Cloud VM concept: no /api/vm client, no device identity,
no tunnel lifecycle. This adds the two Swift packages the Cloud tab will sit
on, deliberately UI-free and binary-free so every behavior is unit-tested:

- CmuxMobileCloud: the /api/vm client (list, tunnel enroll, attach,
  invitation approve), the device identity (ios-<uuid> plus a Curve25519
  WireGuard key held in the Keychain, only the public half ever sent, a
  locked store fails closed), the wg-quick builder that fills the server's
  blank PrivateKey and pins PersistentKeepalive, a CloudSessionController
  that starts the tunnel when a Cloud screen appears and tears it down on
  disappear or background, and the raw-output reducer that turns
  snapshot/output/resize/exit events into grid and byte writes. The Rust
  transport is a protocol seam; the composition root satisfies it later.
- CmuxTerminalClient: the SwiftPM wrapper for the prebuilt Rust xcframework
  (a local downloaded copy wins over the pinned release; model-only mode
  builds without any binary) plus the pure-Swift catalog decoding.

Ported from the reference branches onto current main; the system-VPN role
(an explicit user toggle) is deliberately left out of this slice per product
decision, so CloudSessionController no longer takes an optional system VPN.

swift test: CmuxMobileCloud 54 tests / 9 suites pass;
CMUX_TERMINAL_CLIENT_MODEL_ONLY=1 CmuxTerminalClient 6 tests pass.

Co-authored-by: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A cmux Cloud machine now reaches the phone's workspace experience the way a
paired Mac does, instead of through a parallel Cloud UI.

The store gains one seam: a host that is not a paired Mac can contribute
workspaces and serve terminals (MobileExternalHostSource). It publishes a
MacWorkspaceState into the same per-host map a Mac's snapshot lands in, and
the store routes that host's terminal input, viewport reports and replay
requests back to it instead of the Mac RPC pipeline. Those fork points sit
beside the ones demonstration content already uses, which is the shipping
precedent for a non-Mac host in these surfaces. Everything above the store
reads only workspace values and a surface id, so the workspace list, the
detail screen, its top toolbar and title menu, the terminal, the accessory
row and the composer are the same views with the same behavior, whichever
computer serves the terminal.

CmuxMobileCloudBridge is the only place that names both Cloud and the shell:
it projects a machine's daemon catalog into workspace rows, attaches on
mount, feeds the daemon's snapshot and live bytes in, and carries keystrokes
and grid reports back. Attachment is single-slot per machine, matching the
daemon's own model.

The Cloud tab is therefore the management hub: it lists the account's
machines and creates them, and does not host a second terminal experience.
The bespoke Cloud terminal, catalog and workspace-detail screens are
deleted, along with the Cloud picker the reference wove into the workspace
list. The shell keeps no Cloud dependency at all: the tab's content arrives
from the composition root through the environment.

The system-VPN role is deferred to a later phase, so its controls, its
preferences and its strings are not part of this change.

swift test: CmuxMobileCloudBridge 10 tests, CmuxMobileCloud 54 tests.
swift build: CmuxMobileCloudBridge, CmuxMobileShell, CmuxMobileCloud.
Localization: 40 keys added across the nine required locales, 6 documented
invariant literals, catalog parity unchanged from main at 562.

Co-authored-by: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reusing the paired-Mac UX must not mean reusing the paired-Mac transport.
Three places still routed a Cloud row into machinery that describes a Mac
connection it does not have.

Opening a Cloud workspace called switchToMac for a host no Mac transport
knows; the switch fails and the caller rolls the selection back, so the row
was effectively unopenable. The composer's availability check required a Mac
RPC client, so it would have been disabled. And the full-load reconcile nils
every workspacesByMac key that is not a visible stored paired Mac, so a
Cloud host's rows were deleted on every load and only reappeared on the
bridge's next publish, which is a visible flicker.

All three now fence on host ownership, which the source answers from its own
identifier namespace (MobileExternalHostSource.externalHostOwnsHost).

Visibility lands as a filter over the derivation rather than a deletion of
the entry: an external host republishes on its own schedule, so deleting
would lose the race and the rows would return. setExternalHost(_:hidden:)
plus externalHostSummaries give the Computers screen what it needs.

Tests: 8 behavior tests covering rows appearing, input reaching the host
while the Mac send-status pipeline stays idle, a foreign surface being left
alone, repaint routing, composer availability, hide surviving a republish,
summaries, and unregistration disowning surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The iOS package conventions reject a caseless enum whose members are all
static: a namespace is not a type. Both offenders read better as values
anyway.

CloudSurfaceIdentity's static string plumbing becomes CloudAddress, a real
address with a machineID and an optional component (nil naming the machine
itself), parsed by init?(parsing:) and rendered by its identifier property.
That also tightens ownership: a host address is now exactly one with no
component, and a surface address exactly one with a component, so the two
fences can no longer be confused for each other.

CloudWorkspaceProjection's static function becomes CloudWorkspaceProjector,
constructed per machine with the identity it projects for.

scripts/lint-ios-package-conventions.sh: no unjustified violations.
swift test CmuxMobileCloudBridge: 10 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found reviewing my own unreviewed code.

Terminal output could be reordered. The daemon's callback runs on library
threads and the bridge started a fresh unstructured task per event to reach
the main actor; those have no ordering guarantee, so two chunks could be
applied in the wrong order, which splits an escape sequence and corrupts the
screen. Events now yield into one AsyncStream per machine, which preserves
arrival order across the thread boundary, and a single consumer applies them
on the main actor in that order.

Keystrokes typed before the attachment landed were dropped. The terminal view
is on screen and accepting input from its first frame, while attaching is
async, so the first characters after opening a terminal went nowhere. They
are now held per surface and flushed in order once the link is up, after the
grid report.

Every repaint request forced a detach and reattach. Replay is requested on
mount and again after view resets and resync sweeps, so a live terminal could
have its link torn down repeatedly. A request for the surface already being
attached is now satisfied by that attach's own snapshot.

Teardown is one path (teardownAttachment) so the attachment, its stream, its
consumer and the in-flight marker cannot be released in different subsets.

Tests: 2 covering the ordering guarantee the fix relies on (12 in the package).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
selectedWorkspace falls back to the first remaining row when the selected id
is absent. So hiding the machine whose terminal is open left the id naming a
row that no longer existed and moved the user into another computer's
workspace with no indication, while the id still claimed the hidden one.

Hiding now clears the selection when it belongs to that host, which pops the
detail back to the list. That is what a failed cross-Mac open already does,
for the same reason.

Tests: hiding the open host clears the selection even with another host
visible to fall back to; hiding an unrelated host leaves it alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A catalog read attempted before the tunnel was ready published the machine as
reconnecting with no workspaces, and nothing re-read it. The only other
trigger is a change to the machine list, so on the common path — open the app,
the tab appears before the tunnel finishes — the rows stayed reconnecting
indefinitely and the machine looked broken.

The composition root now refreshes every admitted machine when the tunnel
reaches ready.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three attachment fixes had no behavioral coverage, because the bridge
depended on CloudSessionController and CloudMachineConnection directly, so
exercising them needed a tunnel and a daemon.

The bridge now takes a CloudMachineLinkProviding, which the controller
satisfies in one extension and a fake satisfies in tests. Production wiring is
unchanged.

Seven tests, each verified to fail without its fix rather than merely to pass
with it:
- input typed while the link comes up arrives in order once it is up (fails
  with the old drop: sentText was empty)
- a grid reported before the attach is replayed afterwards
- repeated repaint requests do not restart a running attach (fails with the
  old force-reattach: detachCount was 2)
- once attached, a repaint request does reattach, since only a fresh attach
  yields the daemon's whole screen
- retiring a machine detaches it and disowns its surfaces
- a surface on a machine that is not admitted is disowned
- with no tunnel the machine is still published rather than dropped

19 tests in the package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The session controller closes its machine links whenever the tunnel stops,
which is what backgrounding the app does. The bridge kept its attachments, and
sending into a closed link is discarded silently rather than failing, so the
first keystrokes typed into a still-open terminal after returning went
nowhere, with nothing to reattach until a view reset happened to ask for a
repaint.

Losing the tunnel now drops the attachments and their delivery, so the next
interaction attaches again, and the machines republish as reconnecting so the
list shows their liveness instead of emptying out.

Test verified to fail without the fix: after a tunnel loss the reattach never
happened (attachCount stayed 1) and input went into the dead link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The attachment tests settled by yielding a fixed number of times, which is a
race: under load the bridge's attach task may not have run yet, so the test
would fail for scheduling reasons rather than behavior.

Waits that expect something to happen now poll for it with a bound far past
any real scheduling delay; waits that assert nothing happens still settle
plainly, since there is no condition to poll.

Hardening this exposed a flaw in one test. It waited for the attach to be
requested and then asserted a second repaint request reattaches, but a
request is not a live link: the attach task had not finished, so the second
request correctly hit the in-flight guard and no reattach happened. It now
waits until input actually reaches the terminal, which is the state a second
request has to act against.

Re-verified that both fixes are still what their tests detect: reverting the
in-flight guard and the input buffering each fails its test. 20 tests pass
three runs in a row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First iOS compile of the shell UI (it is iOS-only, so no macOS-hosted build
ever reached it) surfaced dangling pieces of the reference branch's parallel
Cloud UI that the bridge design replaced: a cloudSessionController
environment read, a cloudSessionLifetime modifier, and Mac-picker injection
of cloud machines by a synthetic picker id. Under the bridge, cloud hosts
enter the picker through the aggregated workspaces like any other computer,
so all of it goes.

ios/scripts/reload.sh --tag cloudt --simulator-only now builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live run on a simulator signed into a production account showed the
tab bar never appearing and no Cloud machines loading. Five causes, all in
this branch's wiring:

- The machine list was fetched when session restore finished. A fresh
  sign-in never toggles restore, so the fetch ran once while signed out and
  never again. It is now keyed on the signed-in account and team.
- The tunnel was only wanted while a Cloud screen was visible. Cloud
  terminals open from the Workspaces tab, so the tunnel would never be up
  where it is used. The composition root now holds a shell-wide lease while
  the account owns at least one machine; an account with none never enrolls
  a tunnel peer. Scene phase is forwarded from the root.
- The bridge re-pointed its weak store reference at every store a scene
  re-render built; SwiftUI keeps only the first, so the reference went nil.
  The first live store now wins. Sign-out resets instead of detaching, so
  sign-out then sign-in works without a relaunch.
- The Cloud tab rendered its machine list only when the tunnel was ready, so
  an account with no machines saw a blank tab with no create action, under a
  permanent "Connecting" spinner from the idle tunnel. The list now renders
  from its own phase, a failed first load offers a retry, and the connecting
  row only shows once there is a machine to reach.
- stopTunnel cancelled the in-flight machine list read.

Also corrects the create sheet's "type unavailable" copy, which blamed the
user's plan for what is a deployment's published image set.

Tests: CmuxMobileCloud 58 (lease with no Cloud screen visible, lease yields
to background, list survives a tunnel stop, sign-out reset keeps the device
identity; the lease test verified to fail without the fix). Bridge 22 (first
live store wins, sign-out then sign-in republishes).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Cloud tab is the management hub, but it could only list and create.

Machine lifecycle. Each machine row shows a localized status mapped from the
control plane's vm_status enum (an unknown future state shows the server's
own word rather than hiding the machine), and offers Pause, Resume and Delete
by swipe and context menu, gated by state, with a confirmation before the
destructive delete. One controller path runs every action: it refuses a
second action on a machine while one is running, records a failure against
the machine and action it hit (so the Pro gate's server message shows on the
right row), and reconciles from the server list instead of guessing state.
Destroyed machines never reach a screen, and the bridge no longer dials a
paused, provisioning or failed machine; it publishes it unreachable.

A machine that is still provisioning is re-read every five seconds through
the controller's injected clock while the app is in the foreground, so a new
machine turns from Starting to Running without a pull to refresh; the poll
ends by itself once every machine settles.

A no-Mac account can reach Cloud. The no-computers screen had no tab bar, so
the Cloud tab was unreachable for exactly the account that needs it to create
a first machine. When Cloud is available that screen now sits in a two-tab
bar beside Cloud; Mac-only builds render it exactly as before.

Computers sheet. Cloud machines get their own section beside the Macs, with
the Macs' own visibility switch. Rows hold value snapshots only.

One visibility concept. The controller's persisted hidden-machine set is now
the store behind the shell's filter: the bridge seeds the filter from it on
admit and the store writes a user's change back through a new source
callback, so hiding survives relaunch. The controller's unused parallel
visibility API is gone.

Tests: CmuxMobileCloud 65, bridge 25, ExternalHostHostingTests 10. Lint
clean. Localization: 12 new keys in nine locales, parity unchanged at 562.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live verification on the simulator found a running machine whose attach
the control plane refused (HTTP 502). The phone hid that completely: the
Cloud tab said "Running", the Workspaces tab stayed empty, and nothing
ever tried again.

- A failed catalog read is recorded on the machine's connection. The Cloud
  tab row shows "Couldn't connect. Retrying automatically." with the
  server's own reason, and offers Try Again Now; pull to refresh also
  re-dials every failed link from scratch.
- The workspace bridge retries a failed catalog read on its own: 5 s,
  doubling, capped at a minute, cancelled when the tunnel goes away.
- A machine without a label is shown by its short id (vm-04a498bf) in the
  Cloud tab and the Computers sheet alike, instead of a 35-character id in
  one place and a generic "Cloud" in the other.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…I does

Live verification against production found three more gaps:

- Right after signing in, the machine list read hit a token step still in
  flight. The composition turned that into "not signed in" (`try?`), so
  the Cloud tab said the session had expired and nothing tried again.
  Only a rejected session now counts as a sign-out; a transient token
  state is its own retryable error, and the list retries on its own: once
  quietly after 2 s (still showing the spinner), then visibly with 5 s
  doubling to a minute. A sign-out or a refused (4xx) request is shown and
  not retried.
- Machines without a label now use the control plane's generated name
  (`whimsical-cobalt-butterfly`, the CLI's LABEL column) before the short
  id.
- Terminals decoded a `name` the daemon never sends, so every row showed a
  raw `term_...` id. They now use the daemon's `title` and `cwd`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… them

A live terminal that never received output left no trace: the bridge
swallowed attach failures and the link's one message was info level,
which the unified log does not persist. Attach start, success, failure
(with the error), first output, input held for a pending attach, and each
catalog read now log at notice or error level. Ids only, never bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…output

Fails before the fix: workspaceID(forTerminalID:) has a demonstration fork
but none for external hosts, so a Cloud surface resolves to nil and the
mounted terminal never opens its output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live verification found every Cloud terminal blank: the view opens its
output only after prepareTerminalViewport resolves the surface's
workspace, and that lookup is scoped to the foreground Mac (or to rows
with no host). A Cloud row has neither, so the answer was nil, the output
never started, no replay was requested, and nothing attached. External
host surfaces now resolve to their host's row directly; their ids are
namespaced, so the sibling-build ambiguity the scoping guards against
cannot arise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fails before the fix: requestColdAttachTerminalReplay forks demonstration
surfaces but not external hosts, so with no Mac client it parks the
request for a Mac that never connects and the view stays blank.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The mounted view's cold attach went down the Mac path, which waits for a
Mac client and arms Mac replay barriers. An external host serves its own
screen, so the request now goes straight to it, beside the demonstration
fork. Same audit as the viewport fix: this was the only demonstration
fork on the terminal path without an external-host counterpart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fails before the fix: clearRemoteConnectionContext downgrades every host
entry except the demonstration one, markSecondaryMacUnavailable accepts a
Cloud key, and currentTeamDidChange drops every entry but the foreground
Mac's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live verification showed a Cloud terminal as Disconnected with its
keyboard blocked while its link was healthy: the paired Mac was stuck
reconnecting, and each teardown marked every host entry but the
demonstration one unavailable, Cloud machines included. The detail view
blocks input for an unavailable host. Mac teardown and the secondary-Mac
downgrade now skip external hosts, which report their own liveness, and a
team switch leaves their rows for their source to republish or retire.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Terminals from terminal.list carry a tab id, not a workspace, so every
Cloud terminal landed in a catch-all row while the real workspaces showed
zero terminals. The daemon's session.snapshot has the missing links
(terminal.tab_id -> tab.pane_id -> pane.screen_id -> screen.workspace_id)
and the tab names the Mac shows. TerminalCatalogDecoding.catalog(fromSnapshot:)
places each terminal under its workspace in workspace, screen, pane and
tab order, names it after its tab, and lists pool terminals last.

CloudTerminalSession gains loadCatalog(), defaulting to the two list
calls, so the Kit session can answer from one snapshot once the client
library exposes it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The create sheet's footer read "Creating your machine. This takes a
moment." before anything was tapped. It now shows only while the create
is in flight, and the code's default matches the catalog's English.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fails before the fix: losing the link or failing a catalog read republished
the machine with no rows, and a repaint asked for while the link was down
was dropped with nothing to retry it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live verification: after a trip to the background the open Cloud terminal
kept its old screen and never streamed again until the user typed. The
bridge republished each machine with an empty catalog when the tunnel
dropped (its own comment said the rows stay), so on return the terminal
view could not resolve its surface and never restarted output; and a
repaint requested while the link was down was dropped.

The bridge now keeps each machine's last catalog and republishes it as
reconnecting on link loss or unavailable on a failed read, and remembers
the terminal each machine was showing (or was asked to repaint) and
re-attaches it as soon as a catalog read proves the link is back. A
terminal that no longer exists is forgotten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The iPad split layout's bottom-bar destination control only offered
Workspaces and Notifications, so Cloud was unreachable on iPad with a
paired Mac. It now offers Cloud too, and the sidebar renders the Cloud
screen inside the navigation container it already owns (a stack of
Cloud's own would take over the sidebar's bars), with the same sidebar
toggle Notifications gets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no Mac and no Cloud machine, the Workspaces tab only explained Mac
pairing. In a build with Cloud it now adds that a Cloud machine's
workspaces appear there too, created from the Cloud tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Uses the two client entry points from the updated library:

- The Cloud session reads the catalog from one session snapshot, so each
  terminal lands under the workspace that shows it, named after its tab.
  A daemon that cannot answer the snapshot falls back to the two lists.
- The phone opts into viewer-size priority on every link, so on a
  terminal a Mac also shows, the pseudo-terminal takes the phone's grid
  instead of the smallest of both. Daemons or terminal hosts that predate
  it keep the shared minimum.

Needs a CmuxTerminalClient.xcframework built from
#14682 at e0496c9 or later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A stored Mac that is redialing but contributes no rows no longer puts
Reconnecting under All Computers while a Cloud machine or secondary Mac
serves the visible list. Its own state stays on the Computers screen.
Hidden Cloud machines no longer count toward a healthy list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

CI fast guards passes on 2337a9af8f (https://github.com/manaflow-ai/cmux/actions/runs/36908473469).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/cmux-terminal-client-xcframework.yml">

<violation number="1" location=".github/workflows/cmux-terminal-client-xcframework.yml:236">
P2: `touch -t 198001010000` interprets the literal in the runner's local timezone, so the normalized mtime is a function of that zone. The job's runner expression (line 29) can select `macos-26`, `MACOS_RUNNER_15`, or `blacksmith-6vcpu-macos-15` — independent macOS fleets whose OS timezone is not pinned. A same-build rerun that lands on a runner in a different timezone then produces a different mtime epoch, and if ditto's zip writer derives entry timestamps from it (UTC-based rather than zone-neutralized), the archive bytes — and its SHA-256 — change. The release job treats a mismatched published checksum as a hard error, so the idempotent rerun fails. Pin the timezone for both the touch and the ditto invocation so the normalization — and any time field ditto derives from it — is independent of the runner.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment on lines +236 to +237
find "$xcf" -exec touch -t 198001010000 {} +
(cd "$out" && ditto -c -k --keepParent CmuxTerminalClient.xcframework CmuxTerminalClient.xcframework.zip)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: touch -t 198001010000 interprets the literal in the runner's local timezone, so the normalized mtime is a function of that zone. The job's runner expression (line 29) can select macos-26, MACOS_RUNNER_15, or blacksmith-6vcpu-macos-15 — independent macOS fleets whose OS timezone is not pinned. A same-build rerun that lands on a runner in a different timezone then produces a different mtime epoch, and if ditto's zip writer derives entry timestamps from it (UTC-based rather than zone-neutralized), the archive bytes — and its SHA-256 — change. The release job treats a mismatched published checksum as a hard error, so the idempotent rerun fails. Pin the timezone for both the touch and the ditto invocation so the normalization — and any time field ditto derives from it — is independent of the runner.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/cmux-terminal-client-xcframework.yml, line 236:

<comment>`touch -t 198001010000` interprets the literal in the runner's local timezone, so the normalized mtime is a function of that zone. The job's runner expression (line 29) can select `macos-26`, `MACOS_RUNNER_15`, or `blacksmith-6vcpu-macos-15` — independent macOS fleets whose OS timezone is not pinned. A same-build rerun that lands on a runner in a different timezone then produces a different mtime epoch, and if ditto's zip writer derives entry timestamps from it (UTC-based rather than zone-neutralized), the archive bytes — and its SHA-256 — change. The release job treats a mismatched published checksum as a hard error, so the idempotent rerun fails. Pin the timezone for both the touch and the ditto invocation so the normalization — and any time field ditto derives from it — is independent of the runner.</comment>

<file context>
@@ -232,6 +232,8 @@ jobs:
           plutil -lint "$xcf/Info.plist"
           out="$RUNNER_TEMP/xcframework"
+          # Keep release ZIP bytes stable across reruns of the same build.
+          find "$xcf" -exec touch -t 198001010000 {} +
           (cd "$out" && ditto -c -k --keepParent CmuxTerminalClient.xcframework CmuxTerminalClient.xcframework.zip)
           archive="$out/CmuxTerminalClient.xcframework.zip"
</file context>
Suggested change
find "$xcf" -exec touch -t 198001010000 {} +
(cd "$out" && ditto -c -k --keepParent CmuxTerminalClient.xcframework CmuxTerminalClient.xcframework.zip)
# Keep release ZIP bytes stable across reruns of the same build.
TZ=UTC find "$xcf" -exec touch -t 198001010000 {} +
(cd "$out" && TZ=UTC ditto -c -k --keepParent CmuxTerminalClient.xcframework CmuxTerminalClient.xcframework.zip)

@cursor

cursor Bot commented Oct 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 1 new issue found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/iOS/CmuxMobileCloud/Sources/CmuxMobileCloud/CloudSessionController.swift">

<violation number="1" location="Packages/iOS/CmuxMobileCloud/Sources/CmuxMobileCloud/CloudSessionController.swift:120">
P2: Scoped controllers now ignore existing v2 visibility preferences, so an upgrade silently makes every previously hidden machine visible. Migrate the legacy list into the first account/team partition without reusing it across unrelated scopes.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

self.visibilityDefaults = visibilityDefaults
let normalizedVisibilityScope = Self.normalizedVisibilityScope(visibilityScope)
self.visibilityScope = normalizedVisibilityScope
self.hiddenMachineIDs = Self.loadHiddenMachineIDs(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Scoped controllers now ignore existing v2 visibility preferences, so an upgrade silently makes every previously hidden machine visible. Migrate the legacy list into the first account/team partition without reusing it across unrelated scopes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/iOS/CmuxMobileCloud/Sources/CmuxMobileCloud/CloudSessionController.swift, line 120:

<comment>Scoped controllers now ignore existing v2 visibility preferences, so an upgrade silently makes every previously hidden machine visible. Migrate the legacy list into the first account/team partition without reusing it across unrelated scopes.</comment>

<file context>
@@ -113,8 +115,11 @@ public final class CloudSessionController {
-            (visibilityDefaults.array(forKey: visibilityDefaultsKey) as? [String]) ?? []
+        let normalizedVisibilityScope = Self.normalizedVisibilityScope(visibilityScope)
+        self.visibilityScope = normalizedVisibilityScope
+        self.hiddenMachineIDs = Self.loadHiddenMachineIDs(
+            from: visibilityDefaults,
+            scope: normalizedVisibilityScope
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/iOS/CmuxMobileCloud/Sources/CmuxMobileCloud/CloudOperationGate.swift">

<violation number="1" location="Packages/iOS/CmuxMobileCloud/Sources/CmuxMobileCloud/CloudOperationGate.swift:110">
P1: `Hold.deinit` releases the attachment gate without detaching the underlying session. Because `CloudTerminalAttachment.deinit` only releases this hold, dropping an attachment lets the next attach start while the native session is still attached; detach the session before releasing the lease on attachment deallocation.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

}

func release() {
onRelease()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Hold.deinit releases the attachment gate without detaching the underlying session. Because CloudTerminalAttachment.deinit only releases this hold, dropping an attachment lets the next attach start while the native session is still attached; detach the session before releasing the lease on attachment deallocation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/iOS/CmuxMobileCloud/Sources/CmuxMobileCloud/CloudOperationGate.swift, line 110:

<comment>`Hold.deinit` releases the attachment gate without detaching the underlying session. Because `CloudTerminalAttachment.deinit` only releases this hold, dropping an attachment lets the next attach start while the native session is still attached; detach the session before releasing the lease on attachment deallocation.</comment>

<file context>
@@ -55,11 +67,51 @@ final class CloudOperationGate {
+        }
+
+        func release() {
+            onRelease()
+        }
+
</file context>

@azooz2003-bit
azooz2003-bit merged commit badcbbc into main Oct 1, 2026
98 of 99 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-cloud-parity-ux branch October 1, 2026 19:06
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Merge receipt for 2337a9af8f, merged 2026-10-01 19:06:49 UTC

  • Not verified at merge: ci-status (not reported), macOS compile admission (in progress)
  • Verified: auth-refresh-tests, catalog-structure, CI fast guards, cpp consumer, cpp package, detect-ios-changes, Fast static checks, GhosttyKit release check, go consumer, go package, guards (19), inventory, and 39 more
  • Skipped by policy: admission-placement, agent-session-web-resources, browser, Claude wrapper regressions, diff-sidecar-check, Dogfood build #​${{ github.event.pull_request.number }}, full-suite-coverage, react-apps-check, remote-daemon, suite-coverage, swift-package-tests, ui-tests, and 3 more
  • Full suite: runs on main after merge.

Labeled merged-unverified: if main breaks near this merge, look here first.

@github-actions github-actions Bot added the merged-unverified A judging check was not green at merge; see the merge receipt comment label Oct 1, 2026
teamleaderleo added a commit that referenced this pull request Oct 2, 2026
)

#15935 added a second queued message to the inbox fixture but kept the
single-id mark_read expectation. The CLI marks every listed message read,
as its help text documents, so expect both ids.


Claude-Session: https://claude.ai/code/session_01FrR7YbsQtGw2eFtDeyiTcK

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merged-unverified A judging check was not green at merge; see the merge receipt comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants