Skip to content

iOS: rebuild the disconnected Your Computers screen as a real list - #7156

Merged
lawrencecchen merged 4 commits into
mainfrom
feat-ios-disconnected-macs
Jul 2, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
feat-ios-disconnected-macs

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Before: the disconnected screen rendered every stored paired-Mac record as a centered bordered pill inside a ContentUnavailableView. A device that re-paired across dev builds showed dozens of identical "lawrence's MacBook Pro (2)" rows with no online status, no last-seen time, no way to remove a stale entry, and no feedback when a tap failed.

Now the screen shares the Computers screen's data path. Rows come from the coalesced displayPairedMacs snapshots (one row per logical Mac) through a shared MacComputerSnapshot.snapshots(from:) builder, and render with MacComputerRow in a new .reconnect style: machine-colored avatar, presence-led status line (Online / Last seen 2 hours ago), route diagnostic line, a green dot for Macs that are actually online, a spinner while a reconnect attempt is in flight, and an alert when the attempt fails instead of a silently ignored tap. Swipe or long-press removes the computer with the same alias-wide forgetMac confirmation the Computers screen uses. Presence and last-seen refresh on the same gentle 10-second refreshComputersScreen() cadence (no offline re-dial storm), plus pull-to-refresh. The never-paired empty state keeps the previous ContentUnavailableView layout and auto-presented pairing sheet.

New localized strings (mobile.common.ok, mobile.disconnected.*) have en + ja entries; all other row strings reuse existing Computers-screen keys. ios/CHANGELOG.md top entry updated.

Compile-verified with swift build --triple arm64-apple-ios18.0-simulator on CmuxMobileShellUI.

🤖 Generated with Claude Code


Note

Medium Risk
Touches foreground Mac switching UX and reconnect error handling; changes are UI-focused but switchToMac failure semantics must stay correct to avoid missed or false alerts.

Overview
Replaces the disconnected screen’s centered paired-Mac pills with the same coalesced computer list as the Computers screen: shared MacComputerSnapshot.snapshots(from:), MacComputerRow in a new .reconnect style (presence-led status, tap-to-reconnect, spinner, swipe/remove), plus 10s presence refresh, pull-to-refresh, and a failure alert when reconnect truly fails.

Adds isMacSwitchInFlight on MobileShellComposite so reconnect UI does not show “couldn’t connect” when switchToMac returns false because a newer switch is still in flight. Marks older duplicate same-named offline pairings as “Older pairing” on row diagnostics. macOS disconnected shell keeps the prior ContentUnavailable layout.

Reviewed by Cursor Bugbot for commit 18792a4. Bugbot is set up for automated code reviews on this repo. Configure here.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Rebuilt the iOS disconnected “Your Computers” screen into a real list that shows one row per Mac, leads with presence/last-seen, and adds clear reconnect/remove feedback. Stale same‑named records now show an “Older pairing” label so duplicates are obvious to clean up.

  • New Features

    • Shares the Computers screen’s coalesced data path via MacComputerSnapshot.snapshots(from:); flags stale same‑named entries as “Older pairing”.
    • New .reconnect style in MacComputerRow: presence-led status and dot, spinner while connecting, supersession‑aware failure alert.
    • Tap to reconnect; swipe or long‑press to remove with alias‑wide forgetMac confirmation.
    • Presence and last‑seen refresh every 10s via refreshComputersScreen(), plus pull‑to‑refresh; no offline re‑dial storm.
    • Empty state retained; added localizations mobile.common.ok, mobile.disconnected.*, and mobile.computers.olderPairing (en + ja).
  • Refactors

    • Moved isMacSwitchInFlight to MobileShellComposite+MacSwitchState.swift (widened macSwitchAttemptID to internal); no behavior change.

Written for commit 18792a4. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Redesigned the disconnected computers screen for iPhone/iPad with a clearer “saved Macs” list (one row per computer) and online/last-seen details.
    • Added tap-to-reconnect rows with reconnect loading feedback, plus swipe-to-remove support.
  • Bug Fixes
    • Improved status accuracy for in-progress Mac switching and reconnect attempts, including clearer online/offline labeling.
    • Added a reconnect failure alert when reconnection does not succeed.
  • Documentation
    • Updated iOS release notes and localized strings for the refreshed screen, reconnect messaging, and “Older pairing.”

The disconnected screen rendered every stored paired-Mac record as a
centered bordered pill, so a device that re-paired across dev builds
showed a wall of identical "MacBook Pro" rows with no status, no way to
remove one, and no feedback when a tap failed.

It now shares the Computers screen's data path: rows come from the
coalesced displayPairedMacs snapshots (one row per logical Mac) via a
shared MacComputerSnapshot.snapshots(from:) builder, rendered with
MacComputerRow in a new .reconnect style that leads with presence
(Online / Last seen) instead of the phone connection, shows a spinner
while a reconnect attempt is in flight, and alerts on failure. Rows get
the same swipe/context-menu Remove (forgetMac, alias-wide) with
confirmation. Presence and last-seen stay fresh with the same gentle
10s refresh the Computers screen uses (no offline re-dial storm), plus
pull-to-refresh. The empty state keeps the previous
ContentUnavailableView layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 2, 2026 7:08am
cmux-staging Building Building Preview, Comment Jul 2, 2026 7:08am

@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Mac switch in-flight state, shared Mac snapshot construction, a reconnect-capable computer row, and a rebuilt iOS disconnected workspace screen with refresh, reconnect, and removal flows. Updates changelog entries and localized strings.

Changes

Reconnect flow rebuild

Layer / File(s) Summary
Mac switch in-flight state exposure
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacSwitchState.swift
macSwitchAttemptID becomes module-internal and isMacSwitchInFlight reports whether a switch attempt is active.
Shared Mac snapshot builder
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerSnapshot.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerSnapshot+Store.swift, Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
MacComputerSnapshot gains an older-duplicate flag, snapshots are built from store data in one helper, and DeviceTreeView delegates to that helper.
MacComputerRow reconnect style
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift
Adds reconnect row behavior, in-flight UI, and style-specific status/diagnostic rendering.
Disconnected screen rebuild
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift
Reworks the iOS disconnected screen around snapshot-driven lists, reconnect/removal actions, refresh behavior, and failure alerts, while keeping a macOS fallback.
Changelog and localization
ios/CHANGELOG.md, ios/cmux/Resources/Localizable.xcstrings
Adds release-note bullets and new localized strings for the updated reconnect UI.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#7096: Refactors the same Mac switch attempt lifecycle that isMacSwitchInFlight and macSwitchAttemptID visibility depend on.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Fails: DisconnectedWorkspaceShellView adds a non-test polling loop with Task.sleep(for: .seconds(10)), which the rule forbids in production Swift. Replace the timer loop with a real signal/callback/async sequence for refresh updates, or move it behind a cancellable scheduler abstraction outside production view code.
Cmux Algorithmic Complexity ❌ Error FAIL: snapshots(from:) maps displayPairedMacs and calls workspaceCount(for:), which filters workspaces per row, on hot SwiftUI list paths without caching or a bound. Precompute [macID: count] in one pass and reuse a cached snapshot in the views, or add a benchmark-backed size bound before keeping the nested scan.
Cmux Full Internationalization ❌ Error MacComputerRow uses mobile.computers.buildLabelPrefix, but that key is missing from ios/cmux/Resources/Localizable.xcstrings. Replace it with the existing mobile.computers.buildLabel key or add a translated buildLabelPrefix entry for both en and ja.
Cmux Architecture Rethink ❌ Error Adds a view-owned 10s Task.sleep polling loop on the disconnected screen, expanding timing-based refresh ownership instead of a single source of truth. Move presence/last-seen refresh into one store/coordinator-owned refresh mechanism and have both screens consume snapshots; avoid screen-local polling.
✅ Passed checks (21 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: rebuilding the disconnected Your Computers screen into a real list.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Touched store and snapshot helpers are MainActor-scoped; the new Tasks only await MainActor store methods and no new Sendable/shared-reference isolation debt appears.
Cmux Browser Automation Off-Main ✅ Passed PR only changes iOS shell/UI files and Mac-switch snapshot plumbing; no browser.* commands, worker routing, or WebKit wait/callback paths were modified.
Cmux Expensive Synchronous Load ✅ Passed No agent-history sync loader was added; the new UI paths use async store loads and in-memory snapshot mapping, not RestorableAgentSessionIndex.load() or transcript/JSONL reads.
Cmux Cache Substitution Correctness ✅ Passed The new snapshot builder is UI-only; it loads paired Macs before use and explicitly labels older duplicates, so no durable path trusts a stale cache.
Cmux No Hacky Sleeps ✅ Passed PASS: the PR only changes Swift, localization, and docs; no non-Swift runtime script got fixed sleeps or polling.
Cmux Swift Concurrency ✅ Passed The actual diff only adds snapshot labeling and a stored Bool; no DispatchQueue, Combine, completion handlers, or unscoped Tasks were introduced.
Cmux Swift @Concurrent ✅ Passed Changed async APIs stay @MainActor-isolated, UI calls hop via Task/.task/.refreshable, and no nonisolated async or invalid @concurrent use was introduced.
Cmux Swift File And Package Boundaries ✅ Passed Touched UI/package files stay under limits; the only oversized file change is an incidental 1-line visibility tweak, and shared snapshot logic lives in the iOS package.
Cmux Swiftpm Lockfiles ✅ Passed No cmux-owned Package.swift/.gitignore/Package.resolved files changed; cmux.xcodeproj edits only removed sources, and vendor/bonsplit is a third-party submodule.
Cmux Swift Logging ✅ Passed PASS: The changed Swift files add no print/debugPrint/dump/NSLog or new Logger statements; the only Logger found is preexisting in MobileShellComposite.swift.
Cmux User-Facing Error Privacy ✅ Passed New alert/recovery copy is generic; added strings don’t expose vendor/internal details, and the alert title only interpolates the Mac name.
Cmux Swiftui State Layout ✅ Passed Changed SwiftUI views keep rows on immutable MacComputerSnapshot values and closures; no new ObservableObject/@Published, GeometryReader, or render-time state mutation found.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Touched files only adjust SwiftUI views/data models; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes were introduced.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/docs/localization files; no logs, temp dirs, build output, or other artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new debug/test-only seam was added; isMacSwitchInFlight is a real production UI dependency, and the changed files contain no test-guarded accessor patterns.
Cmux No Ambient Global State ✅ Passed No new file-scope funcs/vars/singletons; snapshots(from:) is an extension method on MacComputerSnapshot, which has instance state, not a static-only namespace.
Description check ✅ Passed The PR description clearly explains the change and testing, but it omits the template’s demo video and checklist sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-disconnected-macs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0d4a7f9. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Rebuilds the iOS disconnected "Your Computers" screen from a stacked pill layout into a proper List that reuses the Computers screen's data path — MacComputerSnapshot.snapshots(from:) and MacComputerRow in a new .reconnect style — delivering deduplicated rows, presence-led status, tap-to-reconnect with spinner and failure alert, swipe/long-press removal with the existing forgetMac confirmation, and a 10-second presence refresh loop. macOS retains the prior ContentUnavailableView layout.

  • Shared snapshot builder extracted into MacComputerSnapshot+Store.swift (snapshots(from:) + markOlderDuplicates), replacing the inline build in DeviceTreeView so both screens always show the same coalesced, presence-enriched computer set with "Older pairing" labels for pre-shared-device-id stale records.
  • .reconnect style on MacComputerRow swaps the navigation link for a Button, uses presence as the dot signal (green = Mac is online), replaces the workspace-count primary line with reconnectStatusPhrase, and shows a ProgressView spinner while a connect attempt is in-flight; the re-entry guard (connectingMacID == nil) lives at list scope, avoiding disabled-button flicker.
  • Failure alert triple-check (!connected, connectionState != .connected, !isMacSwitchInFlight) correctly suppresses "Couldn't connect" when a newer switch (e.g. from Settings) supersedes the tap. All five new strings have both en and ja catalog entries.

Confidence Score: 5/5

Safe to merge — the reconnect and remove flows reuse proven store methods, all edge cases (superseded switch, re-entry, cancellation) are explicitly guarded, and the shared snapshot builder is a pure extraction from DeviceTreeView with no behavioral change to the Computers screen.

The reconnect triple-check (!connected, connectionState != .connected, !isMacSwitchInFlight) correctly handles every supersession scenario. The connectingMacID re-entry guard is synchronous on MainActor so there are no races. The Task.sleep polling loop is wired to the .task lifecycle for cancellation. Localization covers the catalog's two locales completely. macOS surface is unchanged. No new globals, no blocking primitives, no test seams.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift Major iOS-only rewrite: adds list view with MacComputerRow(.reconnect), 10s Task.sleep polling loop, pull-to-refresh, forgetMac confirmation flow, and a triple-guarded failure alert. macOS fallback unchanged. Logic is sound — re-entry guard, supersession check, and cancellation wiring all correct.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift Added Style enum (.computers/.reconnect), rowContainer dispatch, isConnecting spinner, reconnectStatusPhrase, and style-aware dotColor/statusIdentifierSuffix/diagnosticLine. Existing String(format:) usage in diagnosticLine is pre-existing and not worsened; .reconnect path skips that allocation.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerSnapshot+Store.swift New file extracting the snapshot builder from DeviceTreeView into a @mainactor static factory plus markOlderDuplicates (O(N) single-pass, bounded collection). Clean extraction with no new state or globals.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacSwitchState.swift New thin extension adding isMacSwitchInFlight as a public computed property over the now-internal macSwitchAttemptID. Has a real production caller (DisconnectedWorkspaceShellView). Not a test seam.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerSnapshot.swift Adds isOlderDuplicate var (default false) to the value-type snapshot; Equatable equality correctly includes the flag, enabling list diffing to reflect label changes.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift Replaces 23-line inline snapshot build with MacComputerSnapshot.snapshots(from: store). Pure extraction — no behavioral change on the Computers screen.
ios/cmux/Resources/Localizable.xcstrings Five new keys added (mobile.common.ok, mobile.computers.olderPairing, mobile.disconnected.*): all have both en and ja entries, matching the catalog's two supported locales.
ios/CHANGELOG.md Two changelog entries added (Internal + External) for the disconnected screen rebuild. Correct placement under the top in-progress version entry.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant DisconnectedView
    participant MacComputerRow
    participant Store as CMUXMobileShellStore

    DisconnectedView->>Store: loadPairedMacs()
    Store-->>DisconnectedView: pairedMacs
    alt pairedMacs empty
        DisconnectedView->>User: show pairing sheet
    else has Macs
        DisconnectedView->>Store: loadRegistryDevices()
        loop every 10s (Task.sleep)
            DisconnectedView->>Store: refreshComputersScreen()
            Store-->>DisconnectedView: updated presence/lastSeen
        end
    end

    User->>MacComputerRow: tap row (.reconnect style)
    MacComputerRow->>DisconnectedView: connect(macDeviceID)
    DisconnectedView->>DisconnectedView: "guard connectingMacID == nil"
    DisconnectedView->>DisconnectedView: "connectingMacID = macDeviceID"
    DisconnectedView->>Store: switchToMac(macDeviceID)
    Store-->>DisconnectedView: connected: Bool
    DisconnectedView->>DisconnectedView: "connectingMacID = nil"
    alt "!connected && .notConnected && !isMacSwitchInFlight"
        DisconnectedView->>User: show failure alert
    end

    User->>MacComputerRow: swipe to remove
    MacComputerRow->>DisconnectedView: requestRemove(deviceId)
    DisconnectedView->>DisconnectedView: "computerPendingRemovalID = deviceId"
    MacComputerRow->>User: show confirmation dialog
    User->>MacComputerRow: confirm
    MacComputerRow->>DisconnectedView: confirmComputerRemoval()
    DisconnectedView->>Store: forgetMac(macDeviceID)
    DisconnectedView->>Store: loadPairedMacs()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant DisconnectedView
    participant MacComputerRow
    participant Store as CMUXMobileShellStore

    DisconnectedView->>Store: loadPairedMacs()
    Store-->>DisconnectedView: pairedMacs
    alt pairedMacs empty
        DisconnectedView->>User: show pairing sheet
    else has Macs
        DisconnectedView->>Store: loadRegistryDevices()
        loop every 10s (Task.sleep)
            DisconnectedView->>Store: refreshComputersScreen()
            Store-->>DisconnectedView: updated presence/lastSeen
        end
    end

    User->>MacComputerRow: tap row (.reconnect style)
    MacComputerRow->>DisconnectedView: connect(macDeviceID)
    DisconnectedView->>DisconnectedView: "guard connectingMacID == nil"
    DisconnectedView->>DisconnectedView: "connectingMacID = macDeviceID"
    DisconnectedView->>Store: switchToMac(macDeviceID)
    Store-->>DisconnectedView: connected: Bool
    DisconnectedView->>DisconnectedView: "connectingMacID = nil"
    alt "!connected && .notConnected && !isMacSwitchInFlight"
        DisconnectedView->>User: show failure alert
    end

    User->>MacComputerRow: swipe to remove
    MacComputerRow->>DisconnectedView: requestRemove(deviceId)
    DisconnectedView->>DisconnectedView: "computerPendingRemovalID = deviceId"
    MacComputerRow->>User: show confirmation dialog
    User->>MacComputerRow: confirm
    MacComputerRow->>DisconnectedView: confirmComputerRemoval()
    DisconnectedView->>Store: forgetMac(macDeviceID)
    DisconnectedView->>Store: loadPairedMacs()
Loading

Reviews (4): Last reviewed commit: "Label stale same-named pairing records a..." | Re-trigger Greptile

Comment on lines +204 to +207
.refreshable {
await store?.loadPairedMacs()
await store?.loadRegistryDevices()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Pull-to-refresh skips presence refresh

The .refreshable handler calls loadPairedMacs() and loadRegistryDevices() but not refreshComputersScreen(), which is what the 10-second timer uses to keep "Online" / "Last seen …" fresh. A user who pulls down expecting up-to-date status will see the spinner complete but the Online/Last-seen values on each row stay stale until the next timer tick (up to 10 s later). Since presence is the primary signal the .reconnect row leads with, this is a visible UX inconsistency — the affordance implies "refresh everything shown" but the handler only half-does it.

Comment on lines +89 to +91
for await _ in Timer.publish(every: 10, on: .main, in: .common).autoconnect().values {
await store?.refreshComputersScreen()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 New Combine timer where Swift Concurrency is the correct shape

Timer.publish(every: 10, on: .main, in: .common).autoconnect().values introduces a Combine publisher bridged to AsyncSequence in new iOS-only code. The cmux modernization rule flags new Combine app patterns in Swift where a concurrency-native alternative is available. An AsyncStream-based ticker or a for/while loop using Task.sleep(for: .seconds(10)) (or a Clock) would keep this in the Swift concurrency domain and not require a Combine import at all.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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.

2 issues found across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift Outdated
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 0d4a7f9d2f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…tus id

switchToMac returns false both for a genuine failure and for an attempt
superseded by a newer switch; the reconnect list now suppresses its
failure alert when another switch is in flight or already connected,
via a new public isMacSwitchInFlight on the shell composite (Bugbot,
cubic). Pull-to-refresh runs the same refreshComputersScreen() the 10s
loop uses so presence/last-seen actually update on pull (Greptile P1).
The Combine timer becomes a Task.sleep loop cancelled by the .task
lifecycle (Greptile P2). The status dot's automation identifier suffix
now derives from the same style-specific signal as its color:
online/offline on reconnect rows (cubic P3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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 3 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. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift">

<violation number="1" location="Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift:266">
P2: A superseded reconnect can still show a stale “couldn’t connect” alert after the newer Settings-sheet switch has already failed, because this check only suppresses while a newer switch is currently in flight or connected. Consider having `switchToMac` return a result reason/attempt token so the disconnected screen only alerts for its own non-superseded failure.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

connectingMacID = nil
if !connected,
store.connectionState != .connected,
!store.isMacSwitchInFlight {

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: A superseded reconnect can still show a stale “couldn’t connect” alert after the newer Settings-sheet switch has already failed, because this check only suppresses while a newer switch is currently in flight or connected. Consider having switchToMac return a result reason/attempt token so the disconnected screen only alerts for its own non-superseded failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift, line 266:

<comment>A superseded reconnect can still show a stale “couldn’t connect” alert after the newer Settings-sheet switch has already failed, because this check only suppresses while a newer switch is currently in flight or connected. Consider having `switchToMac` return a result reason/attempt token so the disconnected screen only alerts for its own non-superseded failure.</comment>

<file context>
@@ -245,14 +251,19 @@ struct DisconnectedWorkspaceShellView: View {
-            if !connected {
+            if !connected,
+               store.connectionState != .connected,
+               !store.isMacSwitchInFlight {
                 connectFailedComputerName = name
             }
</file context>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fdad2b74a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Cancellation is wired to this `.task`'s lifecycle.
await store?.loadRegistryDevices()
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(10))

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 Badge Replace the sleep-based refresh poll

When the disconnected screen has saved computers, this .task now stays alive by sleeping every 10 seconds and polling refreshComputersScreen(). The repo AGENTS.md Modern Swift concurrency rules explicitly forbid Task.sleep/Clock.sleep used to poll, because these loops are not the testable/cancellable signal path the codebase expects; use a timer/AsyncSequence-style refresh like the Computers screen instead.

Useful? React with 👍 / 👎.

…udget

workflow-guard-tests failed: the 9-line accessor pushed
MobileShellComposite.swift past its Swift file length budget. The
accessor moves to MobileShellComposite+MacSwitchState.swift with
macSwitchAttemptID widened to internal; the god file nets one line
smaller than main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 099a4b70f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

ForEach(computers) { computer in
MacComputerRow(
computer: computer,
requestRemove: { computerPendingRemovalID = $0 },

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 Badge Disable removal while a reconnect is in flight

When a user taps a reconnect row and then swipes or long-presses that same row before switchToMac finishes, this unconditional remove handler still allows forgetMac to run while the reconnect task from connect(to:) is active. forgetMac does not cancel the in-flight mac switch, so a late successful switch can leave the app connected to the computer the user just removed; either hide/disable removal for connectingMacID or cancel the switch before forgetting the row.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift`:
- Around line 282-304: The removal-confirmation flow is duplicated between
DisconnectedWorkspaceShellView and DeviceTreeView, so extract the shared state
machine into one reusable component instead of keeping separate copies. Move the
computerPendingRemovalID/removalConfirmationBinding(for:)/confirmComputerRemoval()
behavior into a shared type or view modifier with a consistent API such as
request(_:), binding(for:), and confirm(), then have both views own and use that
shared implementation. Ensure both DisconnectedWorkspaceShellView and
DeviceTreeView call the same shared logic so future changes to removal behavior
only need to be made once.
- Around line 82-95: The polling loop in DisconnectedWorkspaceShellView’s iOS
`.task` still uses `Task.sleep`, which violates the cancellation-aware
scheduling guideline. Replace the `while !Task.isCancelled` / `Task.sleep(for:)`
refresh loop with the same timer-based async sequence pattern already used in
`DeviceTreeView` (for example, `Timer.publish(...).autoconnect().values`) so
cancellation is handled by the task lifecycle and both screens share the same
refresh mechanism.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift`:
- Around line 188-203: The reconnect badge in MacComputerRow.badge is missing a
stable accessibility identifier on the ProgressView branch. Add an
accessibilityIdentifier to the spinner that matches the existing
MobileComputerStatus-<id>-<suffix> pattern used by the status dot, so automation
can target it consistently during isConnecting states. Keep the identifier logic
aligned with the current statusIdentifierSuffix/computer.deviceId formatting in
badge.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 57d90bc1-637e-4d7f-8d25-8285e6826a95

📥 Commits

Reviewing files that changed from the base of the PR and between a0a77af and 099a4b7.

📒 Files selected for processing (8)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+MacSwitchState.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerSnapshot+Store.swift
  • ios/CHANGELOG.md
  • ios/cmux/Resources/Localizable.xcstrings

Comment on lines +82 to +95
#if os(iOS)
// Registry + presence enrich the rows (online dots, build
// labels). The loop then keeps presence and last-seen fresh
// while the app is parked on this screen; like the Computers
// screen it deliberately does NOT dial offline Macs (see
// `refreshComputersScreen()`), so no reconnect storm.
// Cancellation is wired to this `.task`'s lifecycle.
await store?.loadRegistryDevices()
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(10))
guard !Task.isCancelled else { break }
await store?.refreshComputersScreen()
}
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Replace the new Task.sleep polling loop with the same timer abstraction the Computers screen already uses.

This loop polls every 10s using Task.sleep, but the coding guidelines explicitly call this out: sleep/poll-based coordination must use "a real cancellation-aware scheduler, timer abstraction, async sequence, callback, notification, or state transition." DeviceTreeView.swift's .task already solves the identical "refresh every 10s while visible, cancel on dismiss" problem with Timer.publish(every: 10, on: .main, in: .common).autoconnect().values, which is exactly the timer-abstraction/async-sequence alternative the guideline prescribes. Introducing a second, non-compliant mechanism for the same job is both a guideline violation and a source of behavioral drift between the two screens.

🔧 Proposed fix — reuse the Timer.publish pattern
                     await store?.loadRegistryDevices()
-                    while !Task.isCancelled {
-                        try? await Task.sleep(for: .seconds(10))
-                        guard !Task.isCancelled else { break }
-                        await store?.refreshComputersScreen()
-                    }
+                    for await _ in Timer.publish(every: 10, on: .main, in: .common).autoconnect().values {
+                        await store?.refreshComputersScreen()
+                    }

As per coding guidelines, "Do not use Task.sleep in production Swift code just because it is inside an async function, including for animation timing; retry backoff, keepalive loops, readiness waits, delayed dispatch, and polling must use a real cancellation-aware scheduler, timer abstraction, async sequence, callback, notification, or state transition."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#if os(iOS)
// Registry + presence enrich the rows (online dots, build
// labels). The loop then keeps presence and last-seen fresh
// while the app is parked on this screen; like the Computers
// screen it deliberately does NOT dial offline Macs (see
// `refreshComputersScreen()`), so no reconnect storm.
// Cancellation is wired to this `.task`'s lifecycle.
await store?.loadRegistryDevices()
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(10))
guard !Task.isCancelled else { break }
await store?.refreshComputersScreen()
}
#endif
`#if` os(iOS)
// Registry + presence enrich the rows (online dots, build
// labels). The loop then keeps presence and last-seen fresh
// while the app is parked on this screen; like the Computers
// screen it deliberately does NOT dial offline Macs (see
// `refreshComputersScreen()`), so no reconnect storm.
// Cancellation is wired to this `.task`'s lifecycle.
await store?.loadRegistryDevices()
for await _ in Timer.publish(every: 10, on: .main, in: .common).autoconnect().values {
await store?.refreshComputersScreen()
}
`#endif`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift`
around lines 82 - 95, The polling loop in DisconnectedWorkspaceShellView’s iOS
`.task` still uses `Task.sleep`, which violates the cancellation-aware
scheduling guideline. Replace the `while !Task.isCancelled` / `Task.sleep(for:)`
refresh loop with the same timer-based async sequence pattern already used in
`DeviceTreeView` (for example, `Timer.publish(...).autoconnect().values`) so
cancellation is handled by the task lifecycle and both screens share the same
refresh mechanism.

Source: Coding guidelines

Comment on lines +282 to +304
private func removalConfirmationBinding(for deviceID: String) -> Binding<Bool> {
Binding(
get: { computerPendingRemovalID == deviceID },
set: { isPresented in
if isPresented {
computerPendingRemovalID = deviceID
} else if computerPendingRemovalID == deviceID {
computerPendingRemovalID = nil
}
}
)
}

private func confirmComputerRemoval() {
guard let deviceID = computerPendingRemovalID else {
return
}
computerPendingRemovalID = nil
Task {
await store?.forgetMac(macDeviceID: deviceID)
await store?.loadPairedMacs()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate removal-confirmation state machine — extract a shared component instead of copy-pasting per screen.

computerPendingRemovalID + removalConfirmationBinding(for:) + confirmComputerRemoval() here are a line-for-line duplicate of the same trio in DeviceTreeView.swift (lines 146-172). Both screens implement independent copies of the same "confirm, then forget the alias-wide computer" flow. A future change to the removal semantics (e.g., an additional confirmation step, telemetry, or error handling) has to be applied twice and can silently diverge.

Consider extracting this into a small reusable type (e.g., an @Observable ComputerRemovalController or a shared view modifier) that both DeviceTreeView and DisconnectedWorkspaceShellView own via @State, exposing request(_:), binding(for:), and confirm().

As per the repo's shared-behavior policy: "When a behavior is exposed through multiple entrypoints ... implement one shared action/model path and verify every entrypoint that should invoke it. Do not patch one surface while leaving the others with duplicated logic."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DisconnectedWorkspaceShellView.swift`
around lines 282 - 304, The removal-confirmation flow is duplicated between
DisconnectedWorkspaceShellView and DeviceTreeView, so extract the shared state
machine into one reusable component instead of keeping separate copies. Move the
computerPendingRemovalID/removalConfirmationBinding(for:)/confirmComputerRemoval()
behavior into a shared type or view modifier with a consistent API such as
request(_:), binding(for:), and confirm(), then have both views own and use that
shared implementation. Ensure both DisconnectedWorkspaceShellView and
DeviceTreeView call the same shared logic so future changes to removal behavior
only need to be made once.

Source: Coding guidelines

Comment on lines +188 to 203
/// `.reconnect` rows show a spinner while their connect attempt is in flight.
@ViewBuilder
private var badge: some View {
Image(systemName: "circle.fill")
.font(.caption2)
.foregroundStyle(dotColor)
.accessibilityLabel(connectionPhrase)
.accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-\(isConnected ? "connected" : "disconnected")")
if isConnecting {
ProgressView()
.controlSize(.small)
.accessibilityLabel(
L10n.string("mobile.deviceTree.reconnecting", defaultValue: "Reconnecting…"))
} else {
Image(systemName: "circle.fill")
.font(.caption2)
.foregroundStyle(dotColor)
.accessibilityLabel(primaryStatusPhrase)
.accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-\(statusIdentifierSuffix)")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Connecting spinner has no accessibility identifier.

The status dot exposes a stable MobileComputerStatus-<id>-<suffix> identifier for automation, but the ProgressView shown while isConnecting has none. Any UI test asserting on this identifier during a reconnect attempt loses its anchor.

🔧 Proposed fix
             ProgressView()
                 .controlSize(.small)
                 .accessibilityLabel(
                     L10n.string("mobile.deviceTree.reconnecting", defaultValue: "Reconnecting…"))
+                .accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-connecting")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// `.reconnect` rows show a spinner while their connect attempt is in flight.
@ViewBuilder
private var badge: some View {
Image(systemName: "circle.fill")
.font(.caption2)
.foregroundStyle(dotColor)
.accessibilityLabel(connectionPhrase)
.accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-\(isConnected ? "connected" : "disconnected")")
if isConnecting {
ProgressView()
.controlSize(.small)
.accessibilityLabel(
L10n.string("mobile.deviceTree.reconnecting", defaultValue: "Reconnecting…"))
} else {
Image(systemName: "circle.fill")
.font(.caption2)
.foregroundStyle(dotColor)
.accessibilityLabel(primaryStatusPhrase)
.accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-\(statusIdentifierSuffix)")
}
}
/// `.reconnect` rows show a spinner while their connect attempt is in flight.
`@ViewBuilder`
private var badge: some View {
if isConnecting {
ProgressView()
.controlSize(.small)
.accessibilityLabel(
L10n.string("mobile.deviceTree.reconnecting", defaultValue: "Reconnecting…"))
.accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-connecting")
} else {
Image(systemName: "circle.fill")
.font(.caption2)
.foregroundStyle(dotColor)
.accessibilityLabel(primaryStatusPhrase)
.accessibilityIdentifier("MobileComputerStatus-\(computer.deviceId)-statusIdentifierSuffix")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift`
around lines 188 - 203, The reconnect badge in MacComputerRow.badge is missing a
stable accessibility identifier on the ProgressView branch. Add an
accessibilityIdentifier to the spinner that matches the existing
MobileComputerStatus-<id>-<suffix> pattern used by the status dot, so automation
can target it consistently during isConnecting states. Keep the identifier logic
aligned with the current statusIdentifierSuffix/computer.deviceId formatting in
badge.

Before the shared Mac device id (PR #6772, 2026-06-25) every tagged Mac
dev build minted its own device UUID, so a dogfooder's account backup
holds many identically named records that do not coalesce (each dials a
different port). Rows whose name matches a fresher row and that are not
online now carry an "Older pairing" prefix on the diagnostic line, on
both the Computers screen and the disconnected reconnect list (shared
snapshot builder), so the entries stop looking interchangeable and the
stale ones are obvious removal candidates. en + ja localized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 18792a49 Deployed Jul 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant