iOS: complete Cloud workspace parity - #15935
Conversation
…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>
Bugbot is paused — on-demand spend limit reachedBugbot 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. |
|
|
There was a problem hiding this comment.
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
| find "$xcf" -exec touch -t 198001010000 {} + | ||
| (cd "$out" && ditto -c -k --keepParent CmuxTerminalClient.xcframework CmuxTerminalClient.xcframework.zip) |
There was a problem hiding this comment.
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>
| 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) |
Bugbot is paused — on-demand spend limit reachedBugbot 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. |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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>
# Conflicts: # tests/test_ci_change_areas.py
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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>
|
Merge receipt for
Labeled |
) #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>
Summary
Completes iOS Cloud parity with paired Macs across workspace creation, computer management, terminal routing, browser presentation, and workspace actions.
Verification
python3 scripts/verify-local.py --only test-wiringpassed.cparity; bundle IDs for the app and both extensions were verified.artifacts/verify-remote/20260930-025234-cparity-ios-cmux-app-review-v.Changelog
Changed: Cloud workspaces now follow paired Mac workspace, terminal, browser, and computer-management behavior on iOS.
Need help on this PR? Tag
@codesmith-botwith 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
CmuxTerminalClientSwift package; a new workflow publishes its xcframework.System VPN and lifecycle
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.
Summary by CodeRabbit
New Features
Improvements