Skip to content

iOS: show Restoring session for a known paired Mac on launch - #5543

Merged
lawrencecchen merged 5 commits into
mainfrom
feat-ios-restoring-session-flash
Jun 7, 2026
Merged

lawrencecchen merged 5 commits into
mainfrom
feat-ios-restoring-session-flash

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fixes the iOS cold-launch flash of the empty "Add device" sheet before sessions restore for a returning (already-paired) user.
  • Adds MobileRootAuthGate.shouldShowRestoringStoredMac, gated by a persisted hasKnownPairedMac hint (covers the first rendered frame) plus isReconnectingStoredMac / didFinishStoredMacReconnectAttempt flags on MobileShellComposite. A returning user sees the existing RestoringSessionView during reconnect; a never-paired user still gets "Add device" instantly with no flash; a failed/offline reconnect falls through instead of spinning forever.

Testing

  • MobileRootAuthGateTests.shouldShowRestoringStoredMac (6 cases) — passing locally via swift test on CmuxMobileWorkspace (no GhosttyKit needed): 4 tests green.
  • CI validates the full iOS build.

Notes


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


Note

Medium Risk
Launch and reconnect UI routing changed across shell store and root view; overlapping reconnect/sign-out/forget paths rely on generation guards—wrong gating could spin forever or flash the wrong screen, but logic is covered by new auth-gate tests.

Overview
Fixes the iOS cold-launch flash where authenticated returning users briefly saw the empty Add device sheet before stored-Mac reconnect finished.

MobileShellComposite persists a hasKnownPairedMac hint in UserDefaults (sync read on init for the first frame), tracks isReconnectingStoredMac, didFinishStoredMacReconnectAttempt, and pairedMacHintUndetermined (missing key = legacy install that may already have SQLite pairings). reconnectActiveMacIfAvailable now drives those flags, clears the hint only when there is definitively no Mac or no route, keeps the hint on store read failures, and uses a reconnect generation so sign-out, forget, and overlapping attempts cannot clobber a newer reconnect. Pairing upsert sets the hint; disconnectLiveConnection no longer clears it—disconnectAndForgetActiveMac does.

CMUXMobileRootView adds reconnectStoredMacIfNeeded() on onAppear and auth changes (fixes stuck restoring when already signed in at mount). While disconnected and gated by MobileRootAuthGate.shouldShowRestoringStoredMac, it shows RestoringSessionView when a Mac is known or reconnecting, else MobilePairedMacDeterminingView until the first attempt resolves, then add-device or workspaces.

MobileRootAuthGate gains shouldShowRestoringStoredMac with unit tests.

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


Summary by cubic

Fixes the iOS cold-launch flash by showing the restoring UI for returning users and reliably starting the stored‑Mac reconnect on initial authenticated mount. Adds a neutral determining state and generation‑guarded reconnects to avoid misleading labels and racey flashes.

  • Bug Fixes
    • Added MobileRootAuthGate.shouldShowRestoringStoredMac, backed by hasKnownPairedMac, pairedMacHintUndetermined, isReconnectingStoredMac, and didFinishStoredMacReconnectAttempt. CMUXMobileRootView shows RestoringSessionView when reconnecting or hinted, and a neutral MobilePairedMacDeterminingView spinner while the hint is undetermined.
    • Persist and manage hasKnownPairedMac in UserDefaults: treat a missing key as “may have a Mac,” keep the hint on store read failures, clear it when no Mac or no usable route is found, and write true on successful reconnect/upsert. Use generation tokens so only the latest attempt can change hints/flags; bump on sign‑out and forget to prevent stale tasks from resolving the gate.
    • Extracted reconnectStoredMacIfNeeded() and call it from both onAppear and auth changes so cached‑session mounts always resolve the gate and never stick on restoring. Added unit tests covering the restoring‑stored‑Mac policy.

Written for commit 4d4b898. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • App persists a "known paired Mac" hint so returning users see a restoring‑session UI when appropriate.
    • Added a neutral "determining paired Mac" loading screen while the app checks for a paired Mac.
  • Improvements

    • More robust stored‑Mac reconnect flow with explicit in‑memory reconnect states, centralized UI gating, and predictable sign‑out/forget behavior.
  • Tests

    • Added unit test coverage for the restoring‑stored‑Mac UI gating logic.

@vercel

vercel Bot commented Jun 6, 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 Jun 7, 2026 8:09am
cmux-staging Building Building Preview, Comment Jun 7, 2026 8:09am

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Persists a "known paired Mac" hint in UserDefaults, adds in-memory reconnect gating flags and a reconnect-generation token, refactors stored‑Mac reconnection to manage those flags and persistence, centralizes view-layer reconnect orchestration, and adds a pure gating function with unit tests to show the restoring UI during reconnect attempts.

Changes

Stored Mac Reconnection Flow

Layer / File(s) Summary
State Model & Persistence Setup
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Introduces defaults key, hasKnownPairedMac persisted via injected pairingHintDefaults, pairedMacHintUndetermined, reconnect-generation token, and in-memory flags isReconnectingStoredMac / didFinishStoredMacReconnectAttempt; initializer loads persisted hint and preserves undetermined state.
Reconnection Logic & State Transitions
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Refactors reconnectActiveMacIfAvailable to claim/validate reconnect generations, early-resolve when prerequisites are missing, preserve or clear persisted hint depending on lookup/outcome, toggle reconnect flags around manual-host connect, and set/clear hasKnownPairedMac during persist/forget flows; adds helpers for generation-guarded persistence and completion.
UI Routing & View Orchestration
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift, Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift
Adds reconnectStoredMacIfNeeded() called onAppear and on auth changes, consumes UITest attach URL before stored reconnect, computes shouldShowRestoringStoredMac via MobileRootAuthGate.shouldShowRestoringStoredMac(...), routes rootContent to RestoringSessionView() or MobilePairedMacDeterminingView() while hint is resolved.
Gating Logic & Test Coverage
Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift, Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift
Adds shouldShowRestoringStoredMac(...) pure function that gates restoring-UI visibility based on authentication, connection state, reconnect progress, persisted hint, undetermined hint, and did-finish flag; includes a unit test covering scenario combinations.

Sequence Diagram

sequenceDiagram
  participant CMUXMobileRootView
  participant MobileShellComposite
  participant Store
  participant UserDefaults
  participant NetworkRoute
  CMUXMobileRootView->>MobileShellComposite: onAppear / reconnectStoredMacIfNeeded()
  MobileShellComposite->>UserDefaults: read hasKnownPairedMac key
  UserDefaults-->>MobileShellComposite: persisted hint or absent
  MobileShellComposite->>Store: reconnectActiveMacIfAvailable(stackUserID)
  Store-->>MobileShellComposite: ticket / no ticket
  MobileShellComposite->>NetworkRoute: attempt connect / check route
  NetworkRoute-->>MobileShellComposite: success / failure
  MobileShellComposite->>MobileShellComposite: set/clear hasKnownPairedMac, toggle isReconnectingStoredMac, finishStoredMacReconnectAttempt()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5513: Modifies disconnectAndForgetActiveMac() / live-connection teardown paths that interact with stored‑Mac teardown and hint handling.

Poem

🐰 I hopped through defaults, keys in paw,
Saved paired Macs with a gentle law,
Flags blinked true while the restore light gleamed,
Views waited patient as reconnect dreamed,
A happy rabbit cheered — the app sprang to thaw.


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Logging ❌ Error File-scoped Logger "mobileShellLog" at lines 12-15 of MobileShellComposite.swift violates swift-logging.md: must be "nonisolated private let" in @MainActor class (line 33). Change line 12 from "private let mobileShellLog" to "nonisolated private let mobileShellLog" per the preferred shape in .github/review-bot-rules/swift-logging.md.
Cmux Source Artifacts ❌ Error .claude/scheduled_tasks.lock contains generated local tool data (PID 75304, session UUID, timestamps) violating source control artifacts rules. Remove .claude/scheduled_tasks.lock from the commit or add it to .gitignore as a generated local artifact.
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: showing the Restoring session UI for returning users with a known paired Mac on app launch.
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 Blocking Runtime ✅ Passed No blocking synchronization primitives found in modified production files. UserDefaults operations are non-blocking and appropriately used in @MainActor context.
Cmux No Hacky Sleeps ✅ Passed All PR changes are in Swift files; check applies only to TypeScript, JavaScript, shell, or build/runtime scripts. No non-Swift runtime files modified.
Cmux Algorithmic Complexity ✅ Passed New code uses only O(1) operations: generation guards, boolean comparisons, database queries, and small fixed-size routes. Pre-existing O(n) patterns with n~5 remain unchanged and unmodified.
Cmux Swift Concurrency ✅ Passed No legacy async patterns found. All Task creation is within SwiftUI API boundaries (onAppear/onChange/onOpenURL), which are explicitly allowed exceptions.
Cmux Swift @Concurrent ✅ Passed PR complies with swift-concurrent-annotation.md rules. Async functions properly cross actor boundaries via await and intentionally remain MainActor-bound for connection state management.
Cmux Swift File And Package Boundaries ✅ Passed PR adds 149 lines to already-oversized MobileShellComposite (below 250-line threshold) with coherent connection-state responsibility. Meets focused bug fix exception for existing oversized files.
Cmux User-Facing Error Privacy ✅ Passed PR adds no new user-facing error messages or alerts. New code uses only existing L10n strings and non-user-visible identifiers; reconnect logic logs with privacy masking.
Cmux Full Internationalization ✅ Passed No user-facing text violations. New MobilePairedMacDeterminingView is intentionally label-free. Existing localized views reused. Logic and test code exempt.
Cmux Swiftui State Layout ✅ Passed MobileShellComposite uses @Observable correctly. CMUXMobileRootView uses @Bindable for the store. No @Published, @StateObject, lazy list issues, or render-time state mutations detected.
Cmux Architecture Rethink ✅ Passed Generation tokens prevent race conditions; clean property-based persistence; no timing repairs, duplicate entrypoints, or split lifecycle ownership. Passes swift-architectural-rethink rules.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds iOS SwiftUI Views (MobilePairedMacDeterminingView, CMUXMobileRootView), not standalone NSWindow/NSPanel/Window/WindowGroup, which are explicitly allowed by the rule.
Description check ✅ Passed PR description covers summary, testing approach, and includes required sections; however, missing demo video and incomplete checklist items.
✨ 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-restoring-session-flash

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 and usage tips.

@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: bb035eb6cc

ℹ️ 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".

Comment on lines +129 to +130
} else if store.connectionState != .connected && shouldShowRestoringStoredMac {
RestoringSessionView()

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 Badge Start reconnect when auth is already restored

When the auth restore finishes before CMUXMobileRootView is mounted, isAuthenticated is already true, so the .onChange(of: isAuthenticated) block that calls reconnectActiveMacIfAvailable never runs. With a persisted hasKnownPairedMac hint, this new branch then renders RestoringSessionView while didFinishStoredMacReconnectAttempt remains false, trapping that returning user on the restoring screen instead of either reconnecting or falling through. This can happen on cold launch when cached session validation completes during app startup; the initial-authenticated path needs to kick off the same reconnect attempt, e.g. from onAppear/an initial task.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the iOS cold-launch flash of the empty "Add device" sheet for returning users by introducing a hasKnownPairedMac UserDefaults hint (read synchronously at init), two in-memory flags (isReconnectingStoredMac / didFinishStoredMacReconnectAttempt), and a generation token to guard concurrent reconnect attempts. The root view now shows RestoringSessionView or the new neutral MobilePairedMacDeterminingView while the async paired-Mac lookup resolves.

  • MobileShellComposite: Adds the persisted hint, undetermined flag, generation-guarded finishStoredMacReconnectAttempt / setHasKnownPairedMac helpers, and updates signOut / disconnectAndForgetActiveMac to bump the generation and reset in-memory state; persistPairedMacFromTicket writes hasKnownPairedMac = true after an async upsert without any generation or pairing-attempt guard (see inline comment).
  • CMUXMobileRootView: Extracts reconnectStoredMacIfNeeded() and calls it from both onAppear and onChange(of: isAuthenticated); connectUITestAttachURLIfNeeded() is now only reached when isAuthenticated, which differs from the original unconditional onAppear call.
  • MobileRootAuthGate + tests: New pure shouldShowRestoringStoredMac function with 7 unit-test cases covering all documented states.

Confidence Score: 4/5

Safe to merge with the unguarded hasKnownPairedMac write in persistPairedMacFromTicket understood and accepted — it produces the same add-device flash this PR reduces, but only after a forget-during-upsert race.

The generation-token mechanism correctly guards every hint write on the reconnect path, but persistPairedMacFromTicket writes hasKnownPairedMac = true unconditionally after awaiting the SQLite upsert. If disconnectAndForgetActiveMac runs during that await, the forget's hasKnownPairedMac = false is overwritten by the completing upsert, leaving the hint dirty and causing a RestoringSessionView flash on the next cold launch — exactly the scenario this PR is trying to prevent.

Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift — specifically the hasKnownPairedMac = true write in persistPairedMacFromTicket after the async upsert.

Important Files Changed

Filename Overview
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds hasKnownPairedMac/pairedMacHintUndetermined persistence, isReconnectingStoredMac/didFinishStoredMacReconnectAttempt in-memory flags, and a generation-token guard on reconnect; but persistPairedMacFromTicket writes hasKnownPairedMac=true unguarded after an async upsert, racing with disconnectAndForgetActiveMac.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift Extracts reconnectStoredMacIfNeeded(), calls it from both onAppear and onChange(of:isAuthenticated), and adds the shouldShowRestoringStoredMac branch; connectUITestAttachURLIfNeeded() is now auth-gated which may break unauthenticated UITest launches.
Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift Adds shouldShowRestoringStoredMac pure function with clear priority ordering (actively reconnecting → persisted hint → undetermined → finished attempt); logic matches all documented states and unit-test cases.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift New neutral loading spinner with accessibility identifier; no user-facing text, no i18n concern, intentionally label-free per the doc comment.
Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift Adds 7 cases covering actively reconnecting, persisted hint, undetermined, undetermined-resolved, finished-failed, never-paired, already-connected, and unauthenticated — all exercising the new shouldShowRestoringStoredMac gate.

Sequence Diagram

sequenceDiagram
    participant View as CMUXMobileRootView
    participant Gate as MobileRootAuthGate
    participant Store as MobileShellComposite
    participant DB as PairedMacStore

    Note over View: onAppear (already authenticated)
    View->>Store: reconnectActiveMacIfAvailable
    Store->>Store: bump generation (G)
    Store->>DB: activeMac(stackUserID)
    Note over View,Gate: shouldShowRestoringStoredMac=true
    alt Mac found + route
        DB-->>Store: MobilePairedMac
        Store->>Store: setHasKnownPairedMac(true, G)
        Store->>Store: "isReconnectingStoredMac=true"
        Store->>Store: connectManualHost (await)
        Note over View: shows RestoringSessionView
        Store-->>Store: "isReconnectingStoredMac=false"
        Store-->>Store: "didFinishStoredMacReconnectAttempt=true"
    else No Mac / store error
        DB-->>Store: nil / error
        Store->>Store: setHasKnownPairedMac(false, G)
        Store->>Store: finishStoredMacReconnectAttempt(G)
        Note over View: falls through to add-device
    end

    Note over View,Store: User taps Forget
    View->>Store: disconnectAndForgetActiveMac()
    Store->>Store: bump generation (G+1)
    Store->>Store: "hasKnownPairedMac=false"
    Store->>Store: "isReconnectingStoredMac=false"
    Store->>Store: "didFinishStoredMacReconnectAttempt=false"
    Store-->>View: "connectionState=.disconnected"
Loading

Reviews (5): Last reviewed commit: "Restoring gate: neutral determining stat..." | Re-trigger Greptile

Comment on lines 568 to 573
hasKnownPairedMac = true
isReconnectingStoredMac = true
await connectManualHost(name: mac.displayName ?? host, host: host, port: port)
isReconnectingStoredMac = false
didFinishStoredMacReconnectAttempt = true
return connectionState == .connected

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 Launch-path and recovery-path reconnects can race, clearing isReconnectingStoredMac prematurely

reconnectActiveMacIfAvailable has no guard against concurrent invocations. recoverMobileConnection has recoveryInFlight that prevents recovery-on-recovery, but does not coordinate with the initial launch reconnect started by reconnectStoredMacIfNeeded. If a network-path event fires (e.g., Wi-Fi appears during an offline cold launch) while the launch reconnect's await connectManualHost is in flight, a second call to reconnectActiveMacIfAvailable starts: it calls beginPairingAttempt(), which rotates the attempt UUID and causes the launch call's connectManualHost to return as superseded. The launch call then executes isReconnectingStoredMac = false / didFinishStoredMacReconnectAttempt = true synchronously — while the recovery call's connectManualHost is still awaiting a response on the actor. shouldShowRestoringStoredMac now returns false (didFinishStoredMacReconnectAttempt = true, isReconnectingStoredMac = false), so the disconnected/add-device sheet flashes in for the duration of the recovery connect — exactly the flash this PR was designed to prevent.

@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: 1

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 201-203: The public initializer for MobileShellComposite currently
hardcodes UserDefaults.standard via the parameter pairingHintDefaults; remove
that default so the initializer requires an injected UserDefaults (i.e., change
the signature of init(...) to accept pairingHintDefaults: UserDefaults without a
default) and update any call sites to explicitly pass the desired UserDefaults
instance; locate the initializer in MobileShellComposite (the init that takes
reachability: ReachabilityProviding and pairingHintDefaults: UserDefaults) and
ensure no global .standard/.shared is referenced in package code.
🪄 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: 888c460e-5254-4c74-a6e3-efe9e0188633

📥 Commits

Reviewing files that changed from the base of the PR and between 81aa55a and be3279a.

📒 Files selected for processing (4)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift
  • Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift

@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: bacd377f95

ℹ️ 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".

guard authenticated, connectionState != .connected else { return false }
if isReconnectingStoredMac { return true }
guard !didFinishStoredMacReconnectAttempt else { return false }
return hasKnownPairedMac || pairedMacHintUndetermined

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 Don't treat a missing pair hint as a known Mac

On a fresh install this defaults key is absent too (pairedMacHintUndetermined is set from object(forKey:) == nil), so an authenticated never-paired user now satisfies this return path and sees RestoringSessionView until the async paired-Mac lookup finishes. That contradicts the intended “never paired → Add device immediately” path and can make first-time sign-in look like a stuck restore whenever the store read is slow or fails late; the undetermined compatibility case needs a way to exclude truly new installs or avoid blocking the add-device UI for them.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

585-588: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard reconnect completion flags against stale post-sign-out continuation.

After awaiting connectManualHost(...), this path always marks the reconnect attempt finished. If signOut() runs during that await, the stale continuation can overwrite the sign-out reset and leave reconnect-gate state inconsistent for the next auth transition.

Suggested fix
         hasKnownPairedMac = true
         isReconnectingStoredMac = true
         await connectManualHost(name: mac.displayName ?? host, host: host, port: port)
+        guard isSignedIn else {
+            // Sign-out/reset path owns these flags; avoid resurrecting state
+            // from a stale reconnect continuation.
+            return false
+        }
         isReconnectingStoredMac = false
         didFinishStoredMacReconnectAttempt = true
         return connectionState == .connected

Also applies to: 279-295

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 585 - 588, After awaiting connectManualHost(name:host:port:) avoid
blindly setting isReconnectingStoredMac and didFinishStoredMacReconnectAttempt
because a signOut() may have occurred during the await; instead capture the
current auth/session identity (or a local reconAttemptID) before the await and
after connectManualHost returns verify the identity/ID still matches the one
captured, and only then set isReconnectingStoredMac = false and
didFinishStoredMacReconnectAttempt = true; apply the same guard change to the
other reconnect path that touches
isReconnectingStoredMac/didFinishStoredMacReconnectAttempt (the block around the
earlier connectManualHost call referenced in the review).
🤖 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.

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 585-588: After awaiting connectManualHost(name:host:port:) avoid
blindly setting isReconnectingStoredMac and didFinishStoredMacReconnectAttempt
because a signOut() may have occurred during the await; instead capture the
current auth/session identity (or a local reconAttemptID) before the await and
after connectManualHost returns verify the identity/ID still matches the one
captured, and only then set isReconnectingStoredMac = false and
didFinishStoredMacReconnectAttempt = true; apply the same guard change to the
other reconnect path that touches
isReconnectingStoredMac/didFinishStoredMacReconnectAttempt (the block around the
earlier connectManualHost call referenced in the review).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7aeb7fc-0b5a-4712-a7b2-b5d3dc555454

📥 Commits

Reviewing files that changed from the base of the PR and between be3279a and bacd377.

📒 Files selected for processing (4)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift
  • Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift

@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 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

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 d22db46. Configure here.

@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: d22db4646c

ℹ️ 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".

RestoringSessionView()
} else if !isAuthenticated {
SignInView()
} else if store.connectionState != .connected && shouldShowRestoringStoredMac {

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 Resolve the paired-Mac gate on URL-only auth paths

When a fresh install has no paired-Mac hint (pairedMacHintUndetermined == true), this new branch can stay active forever on auth flows that intentionally skip reconnectStoredMacIfNeeded, such as an attach/deep-link connection launched while unauthenticated or a pending pairing URL consumed immediately after sign-in. If that URL connection fails, no stored-Mac lookup ever runs to set didFinishStoredMacReconnectAttempt, so the user remains on the neutral/restoring spinner instead of falling through to the add-device UI; these URL-only paths need to either run or explicitly resolve the stored-Mac determination gate after the URL attempt completes.

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: 1

🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift`:
- Around line 14-19: Add a localized accessibility label to the ProgressView so
screen readers announce meaningful context: update the ProgressView in
MobilePairedMacDeterminingView (the ProgressView() instance) to call
.accessibilityLabel(...) with a localized string key (e.g.
"mobile.pairedMacDetermining.accessibilityLabel" and sensible default like
"Loading") while keeping the existing
.accessibilityIdentifier("MobilePairedMacDetermining"); ensure you use
String(localized:..., defaultValue:...) (or your app's localization helper) so
the label is localized for VoiceOver users.
🪄 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: bdd779c1-2622-4024-8a5f-10a392ea298c

📥 Commits

Reviewing files that changed from the base of the PR and between cdb428b and d22db46.

📒 Files selected for processing (2)
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift

Comment on lines +14 to +19
var body: some View {
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.accessibilityIdentifier("MobilePairedMacDetermining")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a localized accessibility label for screen reader users.

The view is correctly label-free visually to avoid misleading users, but accessibility labels are not shown on screen — they provide context for assistive technologies. Without an explicit label, screen readers will announce a generic "In progress" message.

Consider adding a neutral localized accessibility label:

ProgressView()
    .controlSize(.large)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .accessibilityLabel(String(localized: "mobile.pairedMacDetermining.accessibilityLabel", defaultValue: "Loading"))
    .accessibilityIdentifier("MobilePairedMacDetermining")

This improves the experience for VoiceOver users without compromising the visual neutrality.

🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift`
around lines 14 - 19, Add a localized accessibility label to the ProgressView so
screen readers announce meaningful context: update the ProgressView in
MobilePairedMacDeterminingView (the ProgressView() instance) to call
.accessibilityLabel(...) with a localized string key (e.g.
"mobile.pairedMacDetermining.accessibilityLabel" and sensible default like
"Loading") while keeping the existing
.accessibilityIdentifier("MobilePairedMacDetermining"); ensure you use
String(localized:..., defaultValue:...) (or your app's localization helper) so
the label is localized for VoiceOver users.

lawrencecchen and others added 5 commits June 7, 2026 01:04
A returning (already-paired) user briefly saw the empty "Add device"
sheet flash on relaunch before the session restored. The root view fell
straight to DisconnectedWorkspaceShellView during the reconnect window
with no "restoring known Mac" state.

Adds a gate (MobileRootAuthGate.shouldShowRestoringStoredMac) backed by a
persisted hasKnownPairedMac hint plus isReconnectingStoredMac /
didFinishStoredMacReconnectAttempt flags on the store, so a returning
user sees RestoringSessionView during reconnect and a never-paired user
still gets "Add device" instantly (no flash). Gate is unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ount

Autoreview caught that the restoring gate (hasKnownPairedMac &&
!didFinishStoredMacReconnectAttempt) could stay on RestoringSessionView
forever: the reconnect that resolves the attempt only started from
onChange(of: isAuthenticated), which never fires when the view mounts
already authenticated (cached session, mock/fixture launch).

Extract reconnectStoredMacIfNeeded() and call it from both onAppear and
onChange so the attempt always resolves and the gate can never stick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview caught that existing installs (which have an active Mac in
SQLite but never wrote the new hasKnownPairedMac key) initialize the
hint as false, so their first launch after updating still flashes the
add-device flow until the async reconnect kicks in -- exactly the case
this PR fixes, for the existing installed base.

Add a pairedMacHintUndetermined flag (key absent at launch) and treat
"undetermined" like "may have a paired Mac" in the restoring gate until
the first reconnect attempt resolves and writes the hint. Determined-false
(genuinely never paired) still shows add-device immediately. Gate test
covers the undetermined case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview caught that reconnectActiveMacIfAvailable cleared the restoring
flags unconditionally after connectManualHost returns. With reconnect
Tasks started from multiple SwiftUI paths (onAppear, onChange, network
recovery), a superseded older attempt could clear the gate while a newer
reconnect was still in progress, reintroducing the add-device flash.

Each attempt now claims a monotonically-increasing generation; only the
current generation may write isReconnectingStoredMac /
didFinishStoredMacReconnectAttempt / hasKnownPairedMac. sign-out and
disconnect-and-forget bump the generation so an in-flight reconnect is
superseded and can't resolve the gate after those events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview round 4 flagged that fresh installs (no paired Mac, hint key
absent) would briefly show the labeled "Restoring session..." screen
before add-device. The paired-Mac store is a Swift actor, so the
fresh-vs-existing determination is inherently async and can't be made
before the first frame.

During the undetermined window show a neutral, label-free spinner
(MobilePairedMacDeterminingView) instead of "Restoring session..."; only
switch to the labeled RestoringSessionView once we actually know a Mac is
being reconnected (hasKnownPairedMac or isReconnectingStoredMac). Fresh
installs see a neutral spinner then add-device; existing installs see the
neutral spinner then "Restoring..." then their session. No misleading
label in either direction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
// A real, reconnectable Mac is now the active paired Mac: record the
// persisted hint so the next launch shows RestoringSessionView during
// the reconnect window instead of the empty add-device sheet.
hasKnownPairedMac = true

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 Unguarded hasKnownPairedMac = true write races with disconnectAndForgetActiveMac

persistPairedMacFromTicket writes hasKnownPairedMac = true directly after await pairedMacStore.upsert(...) returns, with no generation or pairingAttemptID check. If the user taps "Forget" while the upsert is in flight — disconnectAndForgetActiveMac bumps the generation and synchronously sets hasKnownPairedMac = false — the upsert's completion then overwrites that to true, leaving the hint dirty. Every subsequent cold launch will show RestoringSessionView, then fall through once the store read returns no active Mac — exactly the flash this PR was written to eliminate.

The reconnect path guards every hint write through setHasKnownPairedMac(_:generation:), but persistPairedMacFromTicket bypasses that guard entirely. pairingAttemptID is already rotated by disconnectLiveConnection() inside the forget path; capturing it before the upsert and checking isCurrentPairingAttempt after would prevent the stale write.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (source)

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

934-955: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forget the persisted active row, not the transient attach-ticket ID.

Line 935 uses activeTicket?.macDeviceID for removal, but this file also creates synthetic manual-* ticket IDs on the reconnect/manual-host path at Lines 984-994, and persistPairedMacFromTicket explicitly refuses to store those IDs at Lines 830-833. In that case, "Rescan QR" clears the UI hint but leaves the real paired-Mac row active in SQLite, so the next launch reconnects the supposedly forgotten Mac.

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 934 - 955, The code uses activeTicket?.macDeviceID (symbol:
activeTicket) to decide which persisted row to remove, but transient/manual-*
ticket IDs are never persisted (see persistPairedMacFromTicket) so the real
paired-Mac row remains; change disconnectAndForgetActiveMac to query the
persisted paired-Mac identifier from the pairedMacStore (or the same stored
field used by persistPairedMacFromTicket) and call
pairedMacStore.remove(macDeviceID:) with that persisted ID (symbols:
pairedMacStore.remove, persistPairedMacFromTicket); if no persisted row exists,
then fall back to removing activeTicket?.macDeviceID as before, and keep the
removal async with the same error logging.
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1803-1826: sendRemoteTerminalPasteImage is doing heavy base64
encoding on the `@MainActor`, causing UI stalls; fix by capturing values you need
(client, clientID, connectionGeneration, workspaceID.rawValue,
terminalID.rawValue, format) into local constants, then perform
data.base64EncodedString() and build the params dictionary inside a background
task (e.g. Task.detached) off the main actor, await that result, then hop back
to the MainActor to verify connectionGeneration still matches and call
client.sendRequest/handle response and errors on the main actor for state
updates; reference the sendRemoteTerminalPasteImage function and the symbols
remoteClient, clientID, connectionGeneration, workspaceID, terminalID, and
MobileCoreRPCClient.requestData when making the change.
- Around line 626-648: In reconnectActiveMacIfAvailable(stackUserID:), avoid
doing an unscoped lookup when stackUserID is nil: don't call
pairedMacStore.activeMac(stackUserID:) for a nil stackUserID since
pairedMacStore.fetchAllMacs adds the stack_user_id predicate only when non-nil
and activeMac(nil) can return another user’s Mac; instead treat nil as “no
scoped lookup” by short-circuiting (call
finishStoredMacReconnectAttempt(generation:) and return false) or only invoking
pairedMacStore.activeMac when stackUserID != nil so the persisted hint is not
cleared or the reconnect prematurely finished for the wrong user. Ensure you
reference storedMacReconnectGeneration/generation and
finishStoredMacReconnectAttempt when implementing the early-return behavior.

---

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 934-955: The code uses activeTicket?.macDeviceID (symbol:
activeTicket) to decide which persisted row to remove, but transient/manual-*
ticket IDs are never persisted (see persistPairedMacFromTicket) so the real
paired-Mac row remains; change disconnectAndForgetActiveMac to query the
persisted paired-Mac identifier from the pairedMacStore (or the same stored
field used by persistPairedMacFromTicket) and call
pairedMacStore.remove(macDeviceID:) with that persisted ID (symbols:
pairedMacStore.remove, persistPairedMacFromTicket); if no persisted row exists,
then fall back to removing activeTicket?.macDeviceID as before, and keep the
removal async with the same error logging.
🪄 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: d95c1df7-c9fc-4bd4-ae85-31122e8daa73

📥 Commits

Reviewing files that changed from the base of the PR and between d22db46 and 4d4b898.

📒 Files selected for processing (5)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift
  • Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift
  • Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

934-955: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Forget the persisted active row, not the transient attach-ticket ID.

Line 935 uses activeTicket?.macDeviceID for removal, but this file also creates synthetic manual-* ticket IDs on the reconnect/manual-host path at Lines 984-994, and persistPairedMacFromTicket explicitly refuses to store those IDs at Lines 830-833. In that case, "Rescan QR" clears the UI hint but leaves the real paired-Mac row active in SQLite, so the next launch reconnects the supposedly forgotten Mac.

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 934 - 955, The code uses activeTicket?.macDeviceID (symbol:
activeTicket) to decide which persisted row to remove, but transient/manual-*
ticket IDs are never persisted (see persistPairedMacFromTicket) so the real
paired-Mac row remains; change disconnectAndForgetActiveMac to query the
persisted paired-Mac identifier from the pairedMacStore (or the same stored
field used by persistPairedMacFromTicket) and call
pairedMacStore.remove(macDeviceID:) with that persisted ID (symbols:
pairedMacStore.remove, persistPairedMacFromTicket); if no persisted row exists,
then fall back to removing activeTicket?.macDeviceID as before, and keep the
removal async with the same error logging.
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1803-1826: sendRemoteTerminalPasteImage is doing heavy base64
encoding on the `@MainActor`, causing UI stalls; fix by capturing values you need
(client, clientID, connectionGeneration, workspaceID.rawValue,
terminalID.rawValue, format) into local constants, then perform
data.base64EncodedString() and build the params dictionary inside a background
task (e.g. Task.detached) off the main actor, await that result, then hop back
to the MainActor to verify connectionGeneration still matches and call
client.sendRequest/handle response and errors on the main actor for state
updates; reference the sendRemoteTerminalPasteImage function and the symbols
remoteClient, clientID, connectionGeneration, workspaceID, terminalID, and
MobileCoreRPCClient.requestData when making the change.
- Around line 626-648: In reconnectActiveMacIfAvailable(stackUserID:), avoid
doing an unscoped lookup when stackUserID is nil: don't call
pairedMacStore.activeMac(stackUserID:) for a nil stackUserID since
pairedMacStore.fetchAllMacs adds the stack_user_id predicate only when non-nil
and activeMac(nil) can return another user’s Mac; instead treat nil as “no
scoped lookup” by short-circuiting (call
finishStoredMacReconnectAttempt(generation:) and return false) or only invoking
pairedMacStore.activeMac when stackUserID != nil so the persisted hint is not
cleared or the reconnect prematurely finished for the wrong user. Ensure you
reference storedMacReconnectGeneration/generation and
finishStoredMacReconnectAttempt when implementing the early-return behavior.

---

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 934-955: The code uses activeTicket?.macDeviceID (symbol:
activeTicket) to decide which persisted row to remove, but transient/manual-*
ticket IDs are never persisted (see persistPairedMacFromTicket) so the real
paired-Mac row remains; change disconnectAndForgetActiveMac to query the
persisted paired-Mac identifier from the pairedMacStore (or the same stored
field used by persistPairedMacFromTicket) and call
pairedMacStore.remove(macDeviceID:) with that persisted ID (symbols:
pairedMacStore.remove, persistPairedMacFromTicket); if no persisted row exists,
then fall back to removing activeTicket?.macDeviceID as before, and keep the
removal async with the same error logging.
🪄 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: d95c1df7-c9fc-4bd4-ae85-31122e8daa73

📥 Commits

Reviewing files that changed from the base of the PR and between d22db46 and 4d4b898.

📒 Files selected for processing (5)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairedMacDeterminingView.swift
  • Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift
  • Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift
🛑 Comments failed to post (2)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (2)

626-648: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Definitions of activeMac/loadAll:"
rg -n -C3 '\bfunc\s+(activeMac|loadAll)\s*\(' --glob '*.swift'

echo
echo "Call sites that pass stackUserID:"
rg -n -C3 '\b(activeMac|loadAll)\s*\(stackUserID:|reconnectActiveMacIfAvailable\s*\(' --glob '*.swift'

Repository: manaflow-ai/cmux

Length of output: 18129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobileShellComposite.swift: loadPairedMacs / reconnect gating context ----"
sed -n '680,820p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobileShellComposite.swift: reconnectActiveMacIfAvailable core ----"
sed -n '600,720p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobilePairedMacStore.swift: activeMac/loadAll implementation ----"
sed -n '150,230p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

echo
echo "---- MobilePairedMacStoring.swift: activeMac/loadAll protocol/impl details ----"
sed -n '1,120p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 23670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobileShellComposite.swift: loadPairedMacs / reconnect gating context ----"
sed -n '680,820p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobileShellComposite.swift: reconnectActiveMacIfAvailable core ----"
sed -n '600,720p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobilePairedMacStore.swift: activeMac/loadAll implementation ----"
sed -n '150,230p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

echo
echo "---- MobilePairedMacStoring.swift: activeMac/loadAll protocol/impl details ----"
sed -n '1,140p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 23670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobileShellComposite.swift: loadPairedMacs / reconnect gating context ----"
sed -n '680,820p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobileShellComposite.swift: reconnectActiveMacIfAvailable core ----"
sed -n '600,720p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobilePairedMacStore.swift: activeMac/loadAll implementation ----"
sed -n '150,230p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

echo
echo "---- MobilePairedMacStoring.swift: activeMac/loadAll protocol/impl details ----"
sed -n '1,140p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 23670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobileShellComposite.swift: loadPairedMacs / reconnect gating context ----"
sed -n '680,820p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobileShellComposite.swift: reconnectActiveMacIfAvailable core ----"
sed -n '600,720p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobilePairedMacStore.swift: activeMac/loadAll implementation ----"
sed -n '150,230p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

echo
echo "---- MobilePairedMacStoring.swift: activeMac/loadAll protocol/impl details ----"
sed -n '1,140p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 23670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobileShellComposite.swift: loadPairedMacs / reconnect gating context ----"
sed -n '680,820p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobileShellComposite.swift: reconnectActiveMacIfAvailable core ----"
sed -n '600,720p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobilePairedMacStore.swift: activeMac/loadAll implementation ----"
sed -n '150,230p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

echo
echo "---- MobilePairedMacStoring.swift: activeMac/loadAll protocol/impl details ----"
sed -n '1,140p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 23670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobileShellComposite.swift: loadPairedMacs / reconnect gating context ----"
sed -n '680,820p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobileShellComposite.swift: reconnectActiveMacIfAvailable core ----"
sed -n '600,720p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

echo
echo "---- MobilePairedMacStore.swift: activeMac/loadAll implementation ----"
sed -n '150,230p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

echo
echo "---- MobilePairedMacStoring.swift: activeMac/loadAll protocol/impl details ----"
sed -n '1,140p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStoring.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 23670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobilePairedMacStore.swift: fetchAllMacs implementations ----"
rg -n "func\s+fetchAllMacs\s*\(" Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift

echo
echo "---- MobilePairedMacStore.swift: fetchAllMacs (context) ----"
rg -n -C3 "fetchAllMacs" Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift

Repository: manaflow-ai/cmux

Length of output: 1065


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '316,420p' Packages/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 5113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- Call site(s) for reconnectActiveMacIfAvailable ----"
rg -n "reconnectActiveMacIfAvailable\s*\(" --glob '*.swift'

echo
echo "---- Any tests referencing the reconnect nil stackUserID case ----"
rg -n "activeMac\s*\(stackUserID:\s*nil\)|stackUserID:\s*nil|reconnectActiveMacIfAvailable" --glob '*Test*.swift' --glob '*.swift' || true

echo
echo "---- MobileShellComposite: definition of isSignedIn / identityProvider usage ----"
rg -n "var\s+isSignedIn|isSignedIn\s*[:=]|identityProvider\?" Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift

Repository: manaflow-ai/cmux

Length of output: 2539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- MobilePairedMacStoreTests around stackUserID: nil (line ~110-160) ----"
sed -n '90,170p' Packages/CmuxMobilePairedMac/Tests/CmuxMobilePairedMacTests/MobilePairedMacStoreTests.swift | cat -n

echo
echo "---- CMUXMobileRootView: how stackUserID is computed/passed (around line ~200-240) ----"
sed -n '180,260p' Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift | cat -n

echo
echo "---- MobileShellComposite: init/sign-in state relationship (around where isSignedIn is set) ----"
sed -n '200,360p' Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 14739


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "struct\s+MobileRootAuthGate|enum\s+MobileRootAuthGate|class\s+MobileRootAuthGate" --glob '*.swift'

echo
rg -n "shouldReconnectStoredMac|shouldShowRestoringStoredMac|didFinishStoredMacReconnectAttempt|hasKnownPairedMac|pairedMacHintUndetermined" --glob '*.swift' Packages/CmuxMobileShellUI/Sources Packages/CmuxMobileShell/Sources

echo
# If multiple, just dump the most relevant file(s)
for f in $(rg -l "shouldReconnectStoredMac" --glob '*.swift' Packages/CmuxMobileShellUI/Sources Packages/CmuxMobileShell/Sources); do
  echo "---- $f ----"
  sed -n '1,220p' "$f" | cat -n
done

Repository: manaflow-ai/cmux

Length of output: 14930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift | cat -n
echo
sed -n '1,260p' Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift | cat -n

Repository: manaflow-ai/cmux

Length of output: 15411


Fix cross-user / non-scoped stored-Mac reconnect when stackUserID == nil

  • reconnectActiveMacIfAvailable(stackUserID:) forwards its optional stackUserID into pairedMacStore.activeMac(stackUserID:); the store’s fetchAllMacs(activeOnly: stackUserID:) only adds stack_user_id IS ? when stackUserID is non-nil, so activeMac(nil) can return an active Mac from any Stack user.
  • If that unscoped lookup returns nil, reconnectActiveMacIfAvailable clears the persisted “known paired Mac” hint and sets didFinishStoredMacReconnectAttempt, and the reconnect is only kicked off on onAppear / onChange(of: isAuthenticated in CMUXMobileRootView—so it won’t automatically retry once authManager.currentUser?.id becomes available.

Expected: treat stackUserID == nil as “no scoped lookup” (return nil / don’t query), or prevent calling activeMac until stackUserID is non-nil (matching the scoping behavior of loadPairedMacs).

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 626 - 648, In reconnectActiveMacIfAvailable(stackUserID:), avoid
doing an unscoped lookup when stackUserID is nil: don't call
pairedMacStore.activeMac(stackUserID:) for a nil stackUserID since
pairedMacStore.fetchAllMacs adds the stack_user_id predicate only when non-nil
and activeMac(nil) can return another user’s Mac; instead treat nil as “no
scoped lookup” by short-circuiting (call
finishStoredMacReconnectAttempt(generation:) and return false) or only invoking
pairedMacStore.activeMac when stackUserID != nil so the persisted hint is not
cleared or the reconnect prematurely finished for the wrong user. Ensure you
reference storedMacReconnectGeneration/generation and
finishStoredMacReconnectAttempt when implementing the early-return behavior.

1803-1826: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Encode pasted images off the main actor.

MobileShellComposite is @MainActor, so Line 1818 base64-encodes the entire image and builds the JSON payload on the UI thread. A multi-MB screenshot paste will stall the shell before the RPC even starts. Pre-encode off-main and only hop back for state/error updates.

♻️ Suggested direction
-            let params: [String: Any] = [
+            let imageBase64 = await Task.detached(priority: .userInitiated) {
+                data.base64EncodedString()
+            }.value
+            let params: [String: Any] = [
                 "workspace_id": workspaceID.rawValue,
                 "surface_id": terminalID.rawValue,
-                "image_base64": data.base64EncodedString(),
+                "image_base64": imageBase64,
                 "image_format": format,
                 "client_id": clientID,
             ]
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 1803 - 1826, sendRemoteTerminalPasteImage is doing heavy base64
encoding on the `@MainActor`, causing UI stalls; fix by capturing values you need
(client, clientID, connectionGeneration, workspaceID.rawValue,
terminalID.rawValue, format) into local constants, then perform
data.base64EncodedString() and build the params dictionary inside a background
task (e.g. Task.detached) off the main actor, await that result, then hop back
to the MainActor to verify connectionGeneration still matches and call
client.sendRequest/handle response and errors on the main actor for state
updates; reference the sendRemoteTerminalPasteImage function and the symbols
remoteClient, clientID, connectionGeneration, workspaceID, terminalID, and
MobileCoreRPCClient.requestData when making the change.

@lawrencecchen
lawrencecchen merged commit f1169e1 into main Jun 7, 2026
30 of 34 checks passed
lawrencecchen added a commit that referenced this pull request Jun 7, 2026
…ign-out

Address the actionable autoreview P2: signIn()'s untracked bootstrap Task could run
loadPairedMacs() after a fast sign-out, take its !isSignedIn path, and set
hasCompletedInitialPairedMacLoad = true after signOut() reset it to false. The next
sign-in could then evaluate hasNoPairedMacs before the new user's load completed and
flash/open pairing for a returning paired user.

Fix with a signInBootstrapGeneration (the same pattern #5543 uses for the stored-Mac
reconnect): signIn claims a generation and the bootstrap bails if it is superseded or
signed out before loadPairedMacs and again before the refresh; signOut bumps the
generation. So a bootstrap from a signed-out session can no longer resolve the gate
for the next session.

The remaining legacy-synthetic-ticket partition-key finding is the same effectively-
unreachable case already documented as residual risk (persistPairedMacFromTicket
rejects manual- ids, so a stored Mac always has a real macDeviceID; divergence needs
a version downgrade between pair and reconnect). Not perturbing the active-path
partition key for it untested.

20 CmuxMobileShell tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 8, 2026
…g it

Regression from the restoring-session gate (#5543): on launch, a returning
user's stored Mac whose route went stale (Tailscale address changed, or the
Mac is offline) makes connectManualHost hang on a slow connect timeout. Since
the gate shows RestoringSessionView while isReconnectingStoredMac is true,
the user was stuck on "Restoring session..." for the whole connect timeout
before it finally fell through to add-device.

Add a bounded, cancellable deadline (6s) around the launch reconnect: if the
stored Mac hasn't connected by then, resolve the restoring gate so the user
reaches add-device quickly. The connect keeps trying in the background, so a
later success still flips connectionState to .connected and shows the
workspaces. Generation-guarded so a superseded attempt can't fire it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 8, 2026
…g it (#5564)

Regression from the restoring-session gate (#5543): on launch, a returning
user's stored Mac whose route went stale (Tailscale address changed, or the
Mac is offline) makes connectManualHost hang on a slow connect timeout. Since
the gate shows RestoringSessionView while isReconnectingStoredMac is true,
the user was stuck on "Restoring session..." for the whole connect timeout
before it finally fell through to add-device.

Add a bounded, cancellable deadline (6s) around the launch reconnect: if the
stored Mac hasn't connected by then, resolve the restoring gate so the user
reaches add-device quickly. The connect keeps trying in the background, so a
later success still flips connectionState to .connected and shows the
workspaces. Generation-guarded so a superseded attempt can't fire it.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 4d4b8988 Deployed Jun 7, 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