Skip to content

iOS pairing: surface network / auth / trust as individual check marks - #6109

Closed
austinywang wants to merge 27 commits into
mainfrom
issue-6084-ios-pairing-checkmarks
Closed

austinywang wants to merge 27 commits into
mainfrom
issue-6084-ios-pairing-checkmarks

Conversation

@austinywang

@austinywang austinywang commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #6084.

The iOS pairing flow collapsed every failure into one opaque "could not connect", so a network problem was indistinguishable from an auth or trust problem. This adds a discrete network → authentication → trust checklist whose gates resolve individually — each with an in-progress, success, or failure state and an actionable message on failure.

What changed

  • MobilePairingChecklist model (CmuxMobileShellModel): three gates (network / authentication / trust), each pending / inProgress / succeeded / failed(message, guidance), plus .connecting and .connected snapshots.
  • MobilePairingFailureCategory.stage + clearsPriorGates and a pure MobilePairingChecklist.resolving(_:) builder — the single projection from a classified failure to which check mark fails and which earlier gates the failure proves were cleared:
    • on-the-wire auth rejection (authFailed, ticketExpired) → network ✓, auth ✗
    • account mismatch → network ✓, auth ✓, trust ✗
    • offline / unreachable / invalid code / no route → network ✗ (earlier of nothing)
    • untrusted manual route → trust ✗ without falsely claiming network/auth (refused client-side)
  • MobileShellComposite paints the checklist through its existing failure/success choke points (begin/resolve/connected/clear), gated on an in-flight attempt.
  • PairingView renders the checklist once the user starts a pairing attempt from the screen (so a background reconnect never surfaces a stale checklist); the failed gate carries the headline + guidance inline, replacing the single error banner for connection attempts.
  • Localized all new strings (en + ja) in both Localizable.xcstrings catalogs.

Tests

  • Pure mapping tests: category.stage, clearsPriorGates, and MobilePairingChecklist.resolving(_:) for every category.
  • Composite end-to-end tests driving a real connect: offline preflight → network ✗; unauthorized → auth ✗ (network ✓); account_mismatch → trust ✗ (network ✓, auth ✓); success → all ✓.

All CmuxMobileShell package tests pass locally (162 tests). The model package and the two new standalone view files build with swift build; the full iOS UI/app build is validated by the iOS CI lanes (GhosttyKit is not built in this sandbox).

How to verify on the iOS Simulator

  1. Run the iOS app, sign in, open Add device.
  2. Tap Pair with a deliberately bad input to see a gate fail:
    • airplane mode / no network → Network ✗ with an offline message;
    • a reachable Mac signed into a different cmux account → Network ✓, Authentication ✓, Trust ✗ ("different cmux account");
    • a valid QR/host on the same account → all three resolve to ✓ and the workspaces appear.
  3. Each row shows a spinner while in progress, a green check on success, and a red ✗ + actionable guidance on failure. Rows expose accessibility identifiers MobilePairingChecklistRow.{network,authentication,trust}.

🤖 Generated with Claude Code


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


Summary by cubic

Adds a three-step pairing checklist on iOS (Network → Authentication → Trust) with per-stage progress and inline guidance, shown only for foreground Add Device attempts. The Network check only turns green after a request is sent on a connected channel (including the manual mobile.attach_ticket.create probe); unreachable routes and pre‑send auth/token failures do not, and any superseding background attempt clears a stale checklist. Fixes #6084.

  • New Features

    • Checklist model in CmuxMobileShellModel (MobilePairingStage, MobilePairingStageStatus, MobilePairingChecklist) and a pure resolver MobilePairingChecklist.resolving(_:reachedMac:) driven by MobileCoreRPCClient.didAttemptHostSend().
    • Accurate “reached Mac”: didAttemptHostSend flips only after ensureConnected(). The manual attach-ticket probe (success or failure) also marks reached; invalid/unrecognized pairing URLs map to Network, and emailMismatch maps to Trust.
    • Foreground-only UI: surfaced via MobileShellComposite, rendered in PairingView as “Connection status” once the user starts pairing with the current inputs; background reconnects/host switches never publish a checklist and clear any stale one. The banner is suppressed while the checklist is visible.
    • Localized strings (en, ja) with accessible status labels and stable row identifiers MobilePairingChecklistRow.{network,authentication,trust}.
  • Refactors

    • Split checklist and helpers into focused files (MobilePairingChecklistResolution, MobilePairingChecklistState, MobileShellComposite+PairingFormat, CmxAttachTicket+ConstrainingRoutes, MobileManualAttachTicketCreateResponse) in CmuxMobileShell; no behavior change.

Written for commit 06977d0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Added an interactive mobile pairing checklist that reports network, authentication, and trust gate progress.
    • Shows per-gate status (pending / in progress / succeeded / failed) with inline failure details and guidance.
    • Connection status is surfaced only after pairing is initiated from the pairing screen; the checklist replaces the top-level headline failure messaging.
  • Localization

    • Added English and Japanese translations for the checklist UI, including stage titles, status labels, and failure text/guidance.
  • Tests

    • Added unit and end-to-end coverage for checklist stage mapping and UI state transitions.

The pairing flow collapsed every failure into one opaque "could not connect",
so users couldn't tell a network problem from an auth or trust problem
(#6084). This adds a discrete network / authentication / trust checklist whose
gates resolve individually, each with an in-progress, success, or failure state
and an actionable message on failure.

- MobilePairingChecklist model (CmuxMobileShellModel): three gates
  (network/authentication/trust) each holding pending / inProgress / succeeded /
  failed(message, guidance), plus .connecting and .connected snapshots.
- MobilePairingFailureCategory.stage + clearsPriorGates and a pure
  MobilePairingChecklist.resolving(_:) builder: the single projection from a
  classified failure to which check mark fails and which earlier gates the
  failure proves were cleared (an on-the-wire auth/account rejection proves the
  network gate; a pre-transport or route-refused failure proves nothing).
- MobileShellComposite paints the checklist through the existing failure/success
  choke points (begin/resolve/connected/clear), gated on an in-flight attempt.
- PairingView renders the checklist once the user starts a pairing attempt from
  the screen, so a background reconnect never shows a stale checklist; the failed
  gate carries the headline + guidance inline (replacing the single error banner
  for connection attempts).
- Localized the new strings (en + ja) in both string catalogs.
- Tests: pure stage/clearsPriorGates/resolving mapping, plus composite
  end-to-end coverage for offline, auth rejection, account mismatch, and success.

Fixes #6084

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

vercel Bot commented Jun 14, 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 20, 2026 1:50am
cmux-staging Building Building Preview, Comment Jun 20, 2026 1:50am

@coderabbitai

coderabbitai Bot commented Jun 14, 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

Adds a three-gate pairing checklist (network → authentication → trust) to the iOS pairing flow. A new MobilePairingChecklist model tracks per-gate status; the RPC layer introduces didAttemptSend tracking to distinguish pre-send vs. post-send failures; MobilePairingFailureCategory is extended to classify each failure into the matching gate; MobileShellComposite manages checklist lifecycle with foreground/background flow gating; new SwiftUI views render per-gate status rows; and localization strings (en/ja) are added to both string catalogs.

Changes

Mobile Pairing Checklist Feature

Layer / File(s) Summary
Pairing checklist model types
Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobilePairingStage.swift, MobilePairingStageStatus.swift, MobilePairingChecklist.swift
Introduces MobilePairingStage enum (network/authentication/trust with order property), MobilePairingStageStatus enum (pending/inProgress/succeeded/failed with failure message/guidance and derived accessors), and MobilePairingChecklist value type with per-gate status properties, accessor, derived state, and static presets.
RPC send-state tracking
Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift, MobileCoreRPCClient.swift, Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift, TransportTestDoubles.swift
Introduces didAttemptSend flag in session (set after ensureConnected() succeeds) to distinguish pre-send failures from post-send failures; exposes didAttemptHostSend() async query on client; adds ConnectFailingTransport test double and test verifying send-state remains false when transport never connects.
Failure classification and checklist resolution
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
Extends MobilePairingFailureCategory with stage (maps failures to network/authentication/trust or nil) and clearsPriorGates (true for auth/identity failures); adds MobilePairingChecklist.resolving(_:) factory that marks matching gate failed, conditionally marks earlier gates succeeded, leaves later gates pending, and returns all-pending for nil-stage categories.
Display helpers and accessibility
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairingStage+Display.swift
Adds localized title, detail, accessibilityIdentifierSuffix for each stage and symbolName, tintColor, accessibilityValue for each status, supporting UI test targeting and VoiceOver.
SwiftUI checklist views and integration
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingChecklistView.swift, PairingView.swift, CMUXMobileRootView.swift
Introduces PairingChecklistRows/PairingChecklistRow components rendering per-gate status; updates PairingView to render "Connection status" section post-attempt and suppress connection-error banners when checklist visible; wires pairingChecklist from store.
MobileShellComposite checklist lifecycle
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Exposes public private(set) var pairingChecklist: MobilePairingChecklist?; tracks attempt-scoped pairingAttemptReachedMac and isForegroundPairingAttempt flags; gates checklist updates to foreground flows while allowing background flows to bypass; adds lifecycle helpers for initialization, resolution, success marking, and teardown; integrates with attempt start/reset/success/failure/invalidation paths.
Localization strings
Resources/Localizable.xcstrings, ios/cmux/Resources/Localizable.xcstrings
Adds mobile.pairing.checklist.* keys (section titles, detail text, status labels) with en/ja stringUnit translations to both string catalogs.
Unit tests: model, classification, and resolution
Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingChecklistTests.swift
Validates failure-category-to-stage exhaustiveness, .cancelled has no stage, clearsPriorGates semantics, per-gate statuses from resolving(_:) across failure scenarios, connecting/connected presets, and status(for:) accessor correctness.
Integration tests: end-to-end checklist transitions
Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeChecklistTests.swift
Drives MobileShellComposite through eight pairing scenarios (offline, background reconnect, auth rejection, account mismatch, attach-ticket rejection, token-provider failure, unreachable-route pre-send, and success) using LivenessTestRuntime; introduces StubReachability, ChecklistErrorTransport, FirstCallSucceedsTokenProvider, and ConnectFailingTransport test doubles; asserts per-gate states after each scenario.
Test support harness update
Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
Updates makeConnectedStore() to handle needsUserApproval pairing result by accepting the version warning before proceeding.

Sequence Diagram

sequenceDiagram
  participant User
  participant PairingView
  participant MobileShellComposite
  participant MobileCoreRPCClient
  participant MobilePairingChecklist

  User->>PairingView: Taps "Connect"
  PairingView->>MobileShellComposite: beginPairingAttempt(method:)
  MobileShellComposite->>MobileShellComposite: isForegroundPairingAttempt = true
  MobileShellComposite->>MobileShellComposite: pairingAttemptReachedMac = false
  MobileShellComposite->>MobilePairingChecklist: beginPairingChecklist() → .connecting
  MobileShellComposite-->>PairingView: pairingChecklist = .connecting

  rect rgba(100, 150, 200, 0.5)
  Note over MobileShellComposite,MobileCoreRPCClient: Transport connection loop
  MobileShellComposite->>MobileCoreRPCClient: send RPC request
  MobileCoreRPCClient->>MobileCoreRPCClient: ensureConnected() succeeds
  MobileCoreRPCClient->>MobileCoreRPCClient: didAttemptSend = true
  end

  MobileShellComposite->>MobileCoreRPCClient: await didAttemptHostSend()
  MobileCoreRPCClient-->>MobileShellComposite: true/false
  MobileShellComposite->>MobileShellComposite: pairingAttemptReachedMac = true

  alt Pairing succeeds
    MobileShellComposite->>MobilePairingChecklist: markPairingChecklistConnected() → .connected
    MobileShellComposite-->>PairingView: pairingChecklist = .connected
  else Pairing fails
    MobileShellComposite->>MobilePairingChecklist: resolvePairingChecklist(failureCategory)
    MobileShellComposite-->>PairingView: pairingChecklist = resolved
  end

  PairingView->>PairingView: renders PairingChecklistRows per gate
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • manaflow-ai/cmux#5713: Introduced MobilePairingFailureCategory classifier that this PR extends with stage and clearsPriorGates to drive the checklist model.
  • manaflow-ai/cmux#6028: Adds emailMismatch failure category that the PR's checklist stage-mapping logic must account for.

Poem

🐇 Three gates to hop, each one so clear—
Network first: is the host near?
Auth next: do credentials pass?
Then trust: is the bond amassed?
Each step shows green, yellow, or red,
So users know exactly what's ahead! ✅


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces blocking sleep patterns in production code: (1) ContinuousClock().sleep(6 seconds) for UI deadline in reconnect flow; (2) exponential backoff loop using clock.sleep() for presence ret... Replace reconnect deadline with a proper timer/scheduler; implement presence retry using real cancellation-aware mechanisms (async sequence, callback, or state transition) instead of backoff sleep loop.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main change: adding individual check marks for network, authentication, and trust stages in the iOS pairing flow.
Linked Issues check ✅ Passed All coding requirements from issue #6084 are fully addressed: distinct per-stage indicators, individual state resolution (pending/inProgress/succeeded/failed), and actionable failure messages.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the three-stage pairing checklist; no unrelated modifications were introduced.
Cmux Swift Actor Isolation ✅ Passed All new production code properly follows Swift 6 actor isolation rules: model types conform to Sendable, RPC session remains an actor with isolated didAttemptSend property, MobileShellComposite cor...
Cmux Expensive Synchronous Load ✅ Passed The PR adds a lightweight pairing checklist that performs pure value computations on the main actor. MobilePairingChecklist.resolving() is a pure function doing O(1) enum/status comparisons with...
Cmux Cache Substitution Correctness ✅ Passed pairingChecklist is transient, non-persisted UI state, not a cache replacing authoritative reads. pairingAttemptReachedMac is a per-attempt in-flight flag with generation-aware writes preventing st...
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift source and localization files; rule scope explicitly covers TypeScript/JavaScript/shell only, with Swift timing covered separately by swift-blocking-runtime.md.
Cmux Algorithmic Complexity ✅ Passed All algorithmic operations in the pairing checklist feature are O(1): fixed-size collections (3 pairing stages, 17 failure categories) with only switch statements and single-pass value mappings; no...
Cmux Swift Concurrency ✅ Passed PR follows Swift concurrency modernization guidelines: new async APIs use modern async/await (didAttemptHostSend()), all new models are Sendable value types with no legacy patterns, and no Dispatch...
Cmux Swift @Concurrent ✅ Passed All async functions properly access actor state via await without isolation violations. MobileCoreRPCClient.didAttemptHostSend() correctly accesses actor-isolated session.didAttemptSend via await,...
Cmux Swift File And Package Boundaries ✅ Passed PR respects Swift file package boundaries. MobilePairingFailure.swift (489 lines) has clear coherent responsibility (failure classification/checklist projection). MobileShellComposite adds 119 line...
Cmux Swift Logging ✅ Passed PR contains no print/debugPrint/dump/NSLog in app/runtime code, no improper Logger declarations, and no exposure of secrets or personal data in production files.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages comply with privacy rules. Host/port/email are user-provided data from QR codes and device configuration. No raw upstream messages, credentials, implementation detail...
Cmux Full Internationalization ✅ Passed PR fully complies with internationalization rules: all user-facing Swift text uses L10n.string() with matching translations; Resources/Localizable.xcstrings and ios/cmux/Resources/Localizable.xcstr...
Cmux Swiftui State Layout ✅ Passed All SwiftUI changes comply with state layout rules: uses @Observable (not ObservableObject), MobilePairingChecklist is a value struct, PairingChecklistRows/Row are pure value views with immutable d...
Cmux Architecture Rethink ✅ Passed PR exhibits clear architectural ownership (MobileShellComposite), pure projections (MobilePairingChecklist.resolving), and generation-based concurrency control preventing stale state. No timing rep...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup declarations. All UI changes are View structs within an existing .sheet() modal presentation, which is an allowed ca...
Cmux Source Artifacts ✅ Passed All 18 changed files are legitimate source code, tests, and localization catalogs in permanent package locations; no build artifacts, generated files, or scratch directories were added to source co...
Description check ✅ Passed The PR description comprehensively covers the changes, testing approach, and verification steps, following the template structure with all major sections completed.
✨ 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 issue-6084-ios-pairing-checkmarks

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.

@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 `@Resources/Localizable.xcstrings`:
- Around line 198459-198463: The Japanese localization string value for the
trust-detail copy at the ja locale entry is grammatically incorrect and unclear.
Replace the current value "あなたの Mac か確認" with grammatically correct and clear
Japanese text that properly conveys the intended meaning of verifying or
checking the user's Mac. Ensure the corrected text follows proper Japanese
grammar and is production-ready for user-facing display.
🪄 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: 50addaf8-4f77-4773-8211-405af64faffa

📥 Commits

Reviewing files that changed from the base of the PR and between df758a9 and 801e143.

📒 Files selected for processing (11)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingChecklistTests.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeChecklistTests.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobilePairingChecklist.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePairingStage+Display.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingChecklistView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift
  • Resources/Localizable.xcstrings
  • ios/cmux/Resources/Localizable.xcstrings

Comment thread Resources/Localizable.xcstrings
@greptile-apps

greptile-apps Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the opaque iOS pairing "could not connect" error with a discrete Network → Authentication → Trust checklist, where each gate resolves individually to pending / in-progress / succeeded / failed with an actionable inline message. The checklist is scoped to foreground Add Device attempts only — background reconnects never paint it — and hidden when the user edits the route inputs after a prior attempt.

  • Model layer (CmuxMobileShellModel): three new value types — MobilePairingStage, MobilePairingStageStatus, MobilePairingChecklist — plus a pure resolving(_:reachedMac:) builder; didAttemptSend on MobileCoreRPCSession is the single source of truth for whether the network gate was genuinely reached.
  • Shell layer (MobileShellComposite): MobilePairingChecklistState struct owns all mutations; performConnectManualHost (internal, with real production callers) separates foreground vs. background attempts; clearChecklist() is called in both store-side validation-failure early-return paths so a stale .connected checklist cannot mask subsequent errors.
  • UI layer (PairingView + new row/view files): startedPairingInputSignature scopes checklist visibility to the exact inputs that started the attempt; complete en + ja translations in both xcstrings catalogs.

Confidence Score: 5/5

Safe to merge. All checklist mutations are confined to the @MainActor-isolated MobileShellComposite, previous-review issues are fully addressed, and the new paths have end-to-end test coverage.

The checklist state machine is a pure value type driven by well-defined entry points. The didAttemptSend flag provides an accurate signal for whether the network gate was genuinely reached before any failure. Background reconnects are correctly excluded from publishing the checklist. The stale .connected checklist scenario from the prior review is closed by clearChecklist() in the store-side validation-failure early returns. Localization is complete across both catalogs in both supported locales. Test coverage spans every distinct failure shape as well as the background-superseding scenario.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Threads pairingChecklistState through all pairing choke-points; performConnectManualHost correctly gates checklist on foreground flag; markReachedMac gated by didAttemptHostSend and isCurrentConnectionAttempt. Two resolveFailure call-sites are separate paths that each call it once.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingChecklistState.swift New struct owning foreground-only checklist projection; isForegroundAttempt flag gates all mutations so background reconnects never publish a checklist.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingChecklistResolution.swift Pure stage-mapping and clearsPriorGates logic. emailMismatch deliberately excluded from clearsPriorGates (pre-connection client-side check); intentional and tested.
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift Adds didAttemptSend flag set after ensureConnected() succeeds; correct placement ensures unreachable routes do not mark the network gate as reached.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift Adds startedPairingInputSignature to scope checklist display to the exact inputs that triggered the attempt; showsChecklist suppresses the error banner when checklist is visible.
Resources/Localizable.xcstrings All 11 new checklist strings have both en and ja translations, mirrored identically in ios/cmux/Resources/Localizable.xcstrings.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeChecklistTests.swift End-to-end tests cover offline preflight, auth rejection, account mismatch, background-reconnect isolation, and superseding-attempt clearing.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant PV as PairingView
    participant MSC as MobileShellComposite
    participant MCLS as MobilePairingChecklistState
    participant RPC as MobileCoreRPCSession

    PV->>MSC: connectManualHost / connectPairingInput
    MSC->>MCLS: setForegroundAttempt(true)
    MSC->>MCLS: "beginValidationAttempt(hasMethod:true) → checklist=.connecting"
    PV-->>PV: "showsChecklist=true (spinner)"

    alt Network unreachable
        MSC->>MCLS: "resolveFailure(.offline) → network=failed"
        PV-->>PV: Network failed
    else Transport connects
        RPC-->>RPC: "didAttemptSend=true"
        MSC->>MCLS: markReachedMac()
        alt Auth rejection
            MSC->>MCLS: resolveFailure(.authFailed, reachedMac:true)
            PV-->>PV: Network succeeded, Authentication failed
        else Account mismatch
            MSC->>MCLS: resolveFailure(.accountMismatch, reachedMac:true)
            PV-->>PV: Network succeeded, Auth succeeded, Trust failed
        else Success
            MSC->>MCLS: markConnected()
            PV-->>PV: All gates succeeded
        end
    end

    alt Background reconnect
        MSC->>MCLS: setForegroundAttempt(false)
        MSC->>MCLS: "beginValidationAttempt → checklist=nil"
        PV-->>PV: "showsChecklist=false"
    end
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 PV as PairingView
    participant MSC as MobileShellComposite
    participant MCLS as MobilePairingChecklistState
    participant RPC as MobileCoreRPCSession

    PV->>MSC: connectManualHost / connectPairingInput
    MSC->>MCLS: setForegroundAttempt(true)
    MSC->>MCLS: "beginValidationAttempt(hasMethod:true) → checklist=.connecting"
    PV-->>PV: "showsChecklist=true (spinner)"

    alt Network unreachable
        MSC->>MCLS: "resolveFailure(.offline) → network=failed"
        PV-->>PV: Network failed
    else Transport connects
        RPC-->>RPC: "didAttemptSend=true"
        MSC->>MCLS: markReachedMac()
        alt Auth rejection
            MSC->>MCLS: resolveFailure(.authFailed, reachedMac:true)
            PV-->>PV: Network succeeded, Authentication failed
        else Account mismatch
            MSC->>MCLS: resolveFailure(.accountMismatch, reachedMac:true)
            PV-->>PV: Network succeeded, Auth succeeded, Trust failed
        else Success
            MSC->>MCLS: markConnected()
            PV-->>PV: All gates succeeded
        end
    end

    alt Background reconnect
        MSC->>MCLS: setForegroundAttempt(false)
        MSC->>MCLS: "beginValidationAttempt → checklist=nil"
        PV-->>PV: "showsChecklist=false"
    end
Loading

Reviews (21): Last reviewed commit: "refactor: split pairing checklist helper..." | Re-trigger Greptile

Comment thread Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift Outdated
Resolve conflicts from the iOS pairing version-warning + email-match work
(#6028) landing on main alongside this branch's pairing checklist:

- MobileShellComposite: keep both the checklist state/helpers and main's
  pairingVersionWarning state + emailFailure/versionWarning helpers; thread the
  pairing phase into the checklist resolver so a pre-network failure never shows
  a false cleared gate.
- MobilePairingFailureCategory: map the new .emailMismatch case to the trust
  gate; resolving(_:reachedMac:) gates "prior gates cleared" on actually reaching
  the Mac.
- PairingView / CMUXMobileRootView: keep both the version-warning section and the
  network/auth/trust checklist; match the merged memberwise-init argument order.
- Tests: cover emailMismatch and the pre-network authFailed path; drive the
  composite checklist tests (and the shared makeConnectedStore helper) through
  the new "Continue anyway" version-warning approval so the scripted connect
  proceeds.
- .github/swift-file-length-budget.tsv: regenerated for the merged tree via
  scripts/swift_file_length_budget.py --write-budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
austinywang and others added 4 commits June 14, 2026 01:33
… reached

Autoreview caught a false-success: `reachedMac` was inferred from the coarse
pairing phase string, but `MobileCoreRPCClient` can throw `.authorizationFailed`
/`.attachTicketExpired` locally (Stack token provider fails, or an attach token
is expired) before any packet leaves the device. Those categories clear prior
gates, so the network check mark went green for a purely local auth/session
failure — actively misleading.

Replace the phase guess with a real signal: `MobileCoreRPCSession` records
`didAttemptSend` once a request reaches the transport-send stage (after its auth
is built), exposed via `MobileCoreRPCClient.didAttemptHostSend()`. `connect()`
records whether any client reached that stage into `pairingAttemptReachedMac`
(reset per attempt in the shared `beginPairingValidationAttempt` funnel), and the
checklist resolver uses it instead of the phase. A host-returned rejection still
clears the network gate; a pre-send local failure leaves it untested.

Adds a regression test (`preSendTokenFailureLeavesNetworkGateUntested`) for the
exact scenario.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the budget TSV conflict (cross-PR churn) by regenerating it for the
merged tree via scripts/swift_file_length_budget.py --write-budget. No code
conflicts.

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

Autoreview follow-up: `didAttemptSend` was flipped before `ensureConnected()`, so
a route that failed to connect still reported a host send. With the per-attempt
sticky `pairingAttemptReachedMac`, a multi-route ticket whose first route was
unreachable could then green the network gate for a later local pre-send auth
failure — the exact false positive the feature avoids.

Move the flag to after `ensureConnected()` succeeds, so it reflects a genuinely
connected channel. Add regression coverage:
- CmuxMobileRPC: a connect-failing transport leaves `didAttemptHostSend()` false.
- CmuxMobileShell: an unreachable first route followed by a pre-send token
  failure leaves the network gate untested (`.pending`), not cleared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview follow-up: `connect()` awaited `client.didAttemptHostSend()` before
re-checking the connection generation. On the `@MainActor` store that await can
suspend while a newer attempt resets `pairingAttemptReachedMac`; the superseded
attempt could then set it back to true, falsely greening the network gate for the
new attempt's later local failure.

Read the per-client signal into a local, re-check `isCurrentConnectionAttempt`
after the await, and only write the shared flag when still current.

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

@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 (2)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (2)

2943-2962: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check the connection attempt after the store write.

isCurrentConnectionAttempt(generation) is only enforced before await persistPairedMacFromTicket(ticket). If a new pairing or switch starts while that await is suspended, this stale success path still resumes and overwrites workspaces, connectionState, and the live client/polling state for the newer attempt.

💡 Suggested guard placement
                     clearPairingError()
                     await persistPairedMacFromTicket(ticket)
+                    guard isCurrentConnectionAttempt(generation), remoteClient === client else {
+                        return nil
+                    }
                     applyRemoteWorkspaceList(response, preferActiveTicketTarget: workspaceListRequest.preferActiveTicketTarget)
                     syncSelectedTerminalForWorkspace()
                     connectionState = .connected
🤖 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 2943 - 2962, The isCurrentConnectionAttempt(generation) validation
only happens at the start of this connection handling block, but there is an
await call to persistPairedMacFromTicket(ticket) that suspends execution,
allowing a new pairing or connection switch to start before this stale attempt
resumes. After the await in persistPairedMacFromTicket(ticket) returns, re-check
isCurrentConnectionAttempt(generation) and return early if the generation is no
longer current, preventing the stale attempt from overwriting workspaces,
connectionState, client, and polling state that belong to the newer connection
attempt.

3197-3403: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract the pairing flow out of MobileShellComposite.

This change adds another full state machine here: QR validation, version-warning gating, attempt bookkeeping, checklist resolution, and analytics. In a production file that is already far past the repo’s Swift size boundary, that keeps networking, persistence, terminal transport, and UI/pairing state coupled in one place. Please move this pairing flow into a dedicated collaborator instead of extending the monolith further.

As per coding guidelines, "Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility."

🤖 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 3197 - 3403, Extract the pairing flow out of MobileShellComposite
into a dedicated collaborator class to address the file size and separation of
concerns issue. Move all pairing-related methods—beginPairingValidationAttempt,
recordPairingSucceeded, recordPairingFailed, isCurrentPairingAttempt,
invalidatePairingAttempt, applyPairingFailure, applyPairingValidationFailure,
clearPairingError, beginPairingChecklist, resolvePairingChecklist,
markPairingChecklistConnected, clearPairingChecklist,
clearPairingVersionWarning, emailFailure, and versionWarning—along with their
corresponding state properties (pairingAttemptID, pairingAttemptMethod,
pairingAttemptStartedAt, pairingAttemptIsFirstPair, pairingAttemptReachedMac,
pairingChecklist, pairingVersionWarning, pendingPairingVersionWarningURL) into a
new PairingFlowManager or similar collaborator class. Update
MobileShellComposite to instantiate and delegate pairing operations to this new
class, removing the pairing state machine logic from the monolith.

Source: Coding guidelines

🤖 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 3361-3365: In the MobileShellComposite file where the macUserID
mismatch check occurs (the guard statement checking actualUserID ==
expectedUserID), replace the return value of .authFailed with an appropriate
trust-stage or identity mismatch error that aligns with how .emailMismatch is
handled. This ensures that a macUserID mismatch (indicating a QR code bound to a
different account) is treated as an identity/trust problem rather than an
authentication failure, maintaining consistency with the stage contract for
identity and email failures.

In
`@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift`:
- Around line 400-407: The dismissAddDeviceSheet() function in
CMUXMobileRootView is calling cancelPairing() when store.pairingVersionWarning
is not nil, but the cancel button in PairingView already calls cancelPairing()
before invoking the cancel callback that triggers dismissAddDeviceSheet(). This
results in cancelPairing() executing twice for a single user action. Remove the
redundant cancelPairing() call from the dismissAddDeviceSheet() function and
keep only the clearAttachTicketAuthenticationIfNeeded() logic, or determine what
other cleanup is needed for the pairingVersionWarning case without duplicating
the cancellation.

---

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 2943-2962: The isCurrentConnectionAttempt(generation) validation
only happens at the start of this connection handling block, but there is an
await call to persistPairedMacFromTicket(ticket) that suspends execution,
allowing a new pairing or connection switch to start before this stale attempt
resumes. After the await in persistPairedMacFromTicket(ticket) returns, re-check
isCurrentConnectionAttempt(generation) and return early if the generation is no
longer current, preventing the stale attempt from overwriting workspaces,
connectionState, client, and polling state that belong to the newer connection
attempt.
- Around line 3197-3403: Extract the pairing flow out of MobileShellComposite
into a dedicated collaborator class to address the file size and separation of
concerns issue. Move all pairing-related methods—beginPairingValidationAttempt,
recordPairingSucceeded, recordPairingFailed, isCurrentPairingAttempt,
invalidatePairingAttempt, applyPairingFailure, applyPairingValidationFailure,
clearPairingError, beginPairingChecklist, resolvePairingChecklist,
markPairingChecklistConnected, clearPairingChecklist,
clearPairingVersionWarning, emailFailure, and versionWarning—along with their
corresponding state properties (pairingAttemptID, pairingAttemptMethod,
pairingAttemptStartedAt, pairingAttemptIsFirstPair, pairingAttemptReachedMac,
pairingChecklist, pairingVersionWarning, pendingPairingVersionWarningURL) into a
new PairingFlowManager or similar collaborator class. Update
MobileShellComposite to instantiate and delegate pairing operations to this new
class, removing the pairing state machine logic from the monolith.
🪄 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: 600cdb6e-ab18-4265-aee3-248a23a8fbcc

📥 Commits

Reviewing files that changed from the base of the PR and between 801e143 and 03f836a.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (12)
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift
  • Packages/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift
  • Packages/CmuxMobileRPC/Tests/CmuxMobileRPCTests/TransportTestDoubles.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingChecklistTests.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeChecklistTests.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift
  • ios/cmux/Resources/Localizable.xcstrings
💤 Files with no reviewable changes (1)
  • ios/cmux/Resources/Localizable.xcstrings

@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 (2)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (2)

2943-2962: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check the connection attempt after the store write.

isCurrentConnectionAttempt(generation) is only enforced before await persistPairedMacFromTicket(ticket). If a new pairing or switch starts while that await is suspended, this stale success path still resumes and overwrites workspaces, connectionState, and the live client/polling state for the newer attempt.

💡 Suggested guard placement
                     clearPairingError()
                     await persistPairedMacFromTicket(ticket)
+                    guard isCurrentConnectionAttempt(generation), remoteClient === client else {
+                        return nil
+                    }
                     applyRemoteWorkspaceList(response, preferActiveTicketTarget: workspaceListRequest.preferActiveTicketTarget)
                     syncSelectedTerminalForWorkspace()
                     connectionState = .connected
🤖 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 2943 - 2962, The isCurrentConnectionAttempt(generation) validation
only happens at the start of this connection handling block, but there is an
await call to persistPairedMacFromTicket(ticket) that suspends execution,
allowing a new pairing or connection switch to start before this stale attempt
resumes. After the await in persistPairedMacFromTicket(ticket) returns, re-check
isCurrentConnectionAttempt(generation) and return early if the generation is no
longer current, preventing the stale attempt from overwriting workspaces,
connectionState, client, and polling state that belong to the newer connection
attempt.

3197-3403: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract the pairing flow out of MobileShellComposite.

This change adds another full state machine here: QR validation, version-warning gating, attempt bookkeeping, checklist resolution, and analytics. In a production file that is already far past the repo’s Swift size boundary, that keeps networking, persistence, terminal transport, and UI/pairing state coupled in one place. Please move this pairing flow into a dedicated collaborator instead of extending the monolith further.

As per coding guidelines, "Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility."

🤖 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 3197 - 3403, Extract the pairing flow out of MobileShellComposite
into a dedicated collaborator class to address the file size and separation of
concerns issue. Move all pairing-related methods—beginPairingValidationAttempt,
recordPairingSucceeded, recordPairingFailed, isCurrentPairingAttempt,
invalidatePairingAttempt, applyPairingFailure, applyPairingValidationFailure,
clearPairingError, beginPairingChecklist, resolvePairingChecklist,
markPairingChecklistConnected, clearPairingChecklist,
clearPairingVersionWarning, emailFailure, and versionWarning—along with their
corresponding state properties (pairingAttemptID, pairingAttemptMethod,
pairingAttemptStartedAt, pairingAttemptIsFirstPair, pairingAttemptReachedMac,
pairingChecklist, pairingVersionWarning, pendingPairingVersionWarningURL) into a
new PairingFlowManager or similar collaborator class. Update
MobileShellComposite to instantiate and delegate pairing operations to this new
class, removing the pairing state machine logic from the monolith.

Source: Coding guidelines

🤖 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 3361-3365: In the MobileShellComposite file where the macUserID
mismatch check occurs (the guard statement checking actualUserID ==
expectedUserID), replace the return value of .authFailed with an appropriate
trust-stage or identity mismatch error that aligns with how .emailMismatch is
handled. This ensures that a macUserID mismatch (indicating a QR code bound to a
different account) is treated as an identity/trust problem rather than an
authentication failure, maintaining consistency with the stage contract for
identity and email failures.

In
`@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift`:
- Around line 400-407: The dismissAddDeviceSheet() function in
CMUXMobileRootView is calling cancelPairing() when store.pairingVersionWarning
is not nil, but the cancel button in PairingView already calls cancelPairing()
before invoking the cancel callback that triggers dismissAddDeviceSheet(). This
results in cancelPairing() executing twice for a single user action. Remove the
redundant cancelPairing() call from the dismissAddDeviceSheet() function and
keep only the clearAttachTicketAuthenticationIfNeeded() logic, or determine what
other cleanup is needed for the pairingVersionWarning case without duplicating
the cancellation.

---

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 2943-2962: The isCurrentConnectionAttempt(generation) validation
only happens at the start of this connection handling block, but there is an
await call to persistPairedMacFromTicket(ticket) that suspends execution,
allowing a new pairing or connection switch to start before this stale attempt
resumes. After the await in persistPairedMacFromTicket(ticket) returns, re-check
isCurrentConnectionAttempt(generation) and return early if the generation is no
longer current, preventing the stale attempt from overwriting workspaces,
connectionState, client, and polling state that belong to the newer connection
attempt.
- Around line 3197-3403: Extract the pairing flow out of MobileShellComposite
into a dedicated collaborator class to address the file size and separation of
concerns issue. Move all pairing-related methods—beginPairingValidationAttempt,
recordPairingSucceeded, recordPairingFailed, isCurrentPairingAttempt,
invalidatePairingAttempt, applyPairingFailure, applyPairingValidationFailure,
clearPairingError, beginPairingChecklist, resolvePairingChecklist,
markPairingChecklistConnected, clearPairingChecklist,
clearPairingVersionWarning, emailFailure, and versionWarning—along with their
corresponding state properties (pairingAttemptID, pairingAttemptMethod,
pairingAttemptStartedAt, pairingAttemptIsFirstPair, pairingAttemptReachedMac,
pairingChecklist, pairingVersionWarning, pendingPairingVersionWarningURL) into a
new PairingFlowManager or similar collaborator class. Update
MobileShellComposite to instantiate and delegate pairing operations to this new
class, removing the pairing state machine logic from the monolith.
🪄 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: 600cdb6e-ab18-4265-aee3-248a23a8fbcc

📥 Commits

Reviewing files that changed from the base of the PR and between 801e143 and 03f836a.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (12)
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift
  • Packages/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift
  • Packages/CmuxMobileRPC/Tests/CmuxMobileRPCTests/TransportTestDoubles.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingChecklistTests.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeChecklistTests.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellRenderGridLivenessTestSupport.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift
  • ios/cmux/Resources/Localizable.xcstrings
💤 Files with no reviewable changes (1)
  • ios/cmux/Resources/Localizable.xcstrings
🛑 Comments failed to post (2)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

3361-3365: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return a trust-stage mismatch here, not .authFailed.

A ticket.macUserID mismatch is the same identity/trust problem as .emailMismatch, but this branch maps it to .authFailed. That will fail the authentication gate and show the wrong guidance for QR codes bound to a different account, which breaks the stage contract this PR introduces for identity/email failures.

🤖 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 3361 - 3365, In the MobileShellComposite file where the macUserID
mismatch check occurs (the guard statement checking actualUserID ==
expectedUserID), replace the return value of .authFailed with an appropriate
trust-stage or identity mismatch error that aligns with how .emailMismatch is
handled. This ensures that a macUserID mismatch (indicating a QR code bound to a
different account) is treated as an identity/trust problem rather than an
authentication failure, maintaining consistency with the stage contract for
identity and email failures.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift (1)

400-407: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid double-cancel side effects in the cancel callback chain.

Line 403 makes dismissAddDeviceSheet() call cancelPairing() when store.pairingVersionWarning != nil, but PairingView’s cancel button already calls cancelPairing() before cancel() (Line 278 in Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift). This can execute cancellation logic twice for one user action.

Proposed fix
 private func dismissAddDeviceSheet() {
     isShowingAddDeviceSheet = false
-    if store.pairingVersionWarning != nil {
-        cancelPairing()
-    } else {
-        clearAttachTicketAuthenticationIfNeeded()
-    }
+    clearAttachTicketAuthenticationIfNeeded()
 }
🤖 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/CMUXMobileRootView.swift`
around lines 400 - 407, The dismissAddDeviceSheet() function in
CMUXMobileRootView is calling cancelPairing() when store.pairingVersionWarning
is not nil, but the cancel button in PairingView already calls cancelPairing()
before invoking the cancel callback that triggers dismissAddDeviceSheet(). This
results in cancelPairing() executing twice for a single user action. Remove the
redundant cancelPairing() call from the dismissAddDeviceSheet() function and
keep only the clearAttachTicketAuthenticationIfNeeded() logic, or determine what
other cleanup is needed for the pairingVersionWarning case without duplicating
the cancellation.

austinywang and others added 3 commits June 14, 2026 02:12
Autoreview follow-up: the store painted `pairingChecklist` for every instrumented
attempt, including background reconnects (`reconnectActiveMacIfAvailable`), host
switches (`switchToMac`), and device-tree taps (`connectToRegistryInstance`),
which all route through `connectManualHost`. Combined with the sheet's sticky
`hasStartedPairing`, a background reconnect while the Add Device sheet stayed open
could overwrite and render a checklist for an attempt the user never started.

Split `connectManualHost` into the public foreground entry (the Pair button) and
an internal `performConnectManualHost(isForegroundPairing:)`; mark QR/link pairing
foreground too. Only foreground attempts publish the checklist (begin/resolve/
connected are gated on `isForegroundPairingAttempt`); the non-foreground callers
pass `false`. Adds a regression test that a background reconnect leaves
`pairingChecklist` nil.

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

Autoreview follow-up: the foreground manual-host flow runs a pre-connect
`mobile.attach_ticket.create` RPC before `connect(ticket:)`. A host rejection
there (unauthorized / account mismatch) was resolved with `reachedMac: false`, so
the checklist showed Network/Authentication untested even though the Mac was
reached. Sample the probe client's `didAttemptHostSend()` (re-checking the
connection generation after the await) and set `pairingAttemptReachedMac` so an
auth/trust rejection from that probe clears the earlier gates. Adds a regression
test driving the manual flow's attach-ticket probe to an account-mismatch
rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address autoreview (Aziz) policy findings: split MobilePairingChecklist.swift
into MobilePairingStage.swift, MobilePairingStageStatus.swift, and
MobilePairingChecklist.swift (matching the package's one-type-per-file
convention), and add the missing Swift-DocC comments to the checklist's public
properties and initializer.

(Co-located private SwiftUI sub-views like PairingChecklistRow and the shared
test-doubles files are left as-is, consistent with existing patterns such as
AddDeviceField in PairingView.swift and the multi-double TransportTestDoubles.swift.)

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

@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

Caution

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

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

1126-1160: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear or reinitialize the checklist before manual-input validation returns.

On Lines 1134-1160, the invalid-host / invalid-port exits happen before beginPairingAttempt(method: "manual"), so a prior non-nil pairingChecklist survives this new submission. Because PairingView renders any stored checklist snapshot, the Add Device sheet can keep showing stale stage results for a fresh validation error.

Suggested fix
 func performConnectManualHost(
     name: String,
     host: String,
     port: Int,
     isForegroundPairing: Bool
 ) async {
     isForegroundPairingAttempt = isForegroundPairing
+    if isForegroundPairing {
+        clearPairingChecklist()
+    }

     let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
     guard let normalizedHost = MobileShellRouteAuthPolicy.normalizedManualHost(host) else {
🤖 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 1126 - 1160, In the performConnectManualHost function, the early
return statements in both the invalid-host guard block (after
normalizedManualHost check fails) and the invalid-port guard block (after port
range check fails) do not clear the pairingChecklist. This allows stale
checklist data from previous pairing attempts to persist and display in
PairingView when new validation errors occur. Add code to clear or reinitialize
the pairingChecklist property to nil before each of these two return statements,
ensuring the UI does not show outdated stage results when validation fails on a
fresh submission.

3239-3395: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract the pairing-checklist state machine out of MobileShellComposite.

These additions add another attempt-lifecycle subsystem to a production type that is already far past the repo’s size limit. Moving the checklist / foreground-attempt bookkeeping into a focused collaborator or dedicated file would make the connection flow easier to reason about and test without expanding this 5k+ line class further.

As per coding guidelines, "Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility."

🤖 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 3239 - 3395, Extract the pairing-checklist state machine out of
MobileShellComposite into a focused collaborator type to reduce the size of the
already oversized class. Create a new dedicated type (either a struct or class)
that encapsulates the checklist state (pairingChecklist,
pairingAttemptReachedMac, isForegroundPairingAttempt) and owns the four
checklist-related methods: beginPairingChecklist(), resolvePairingChecklist(),
markPairingChecklistConnected(), and clearPairingChecklist(). Then refactor
MobileShellComposite to instantiate and delegate to this new collaborator
instead of managing the checklist logic directly, passing necessary dependencies
like isForegroundPairingAttempt and pairingAttemptMethod through method
parameters or initializer as needed.

Source: Coding guidelines

🤖 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 2443-2467: The code currently sets pairingAttemptReachedMac to
true only in the catch block when the mobile.attach_ticket.create RPC fails (but
only if an attempt to send was made). However, a successful RPC call is equally
valid proof that the Mac was reached. Add pairingAttemptReachedMac = true in the
success path as well, after the do block completes successfully (after the try
await client.sendRequest call succeeds). This ensures that whether the RPC
succeeds or fails with a send attempt, the flag is properly recorded, preventing
resolvePairingChecklist from incorrectly treating the network gate as unproven.

---

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 1126-1160: In the performConnectManualHost function, the early
return statements in both the invalid-host guard block (after
normalizedManualHost check fails) and the invalid-port guard block (after port
range check fails) do not clear the pairingChecklist. This allows stale
checklist data from previous pairing attempts to persist and display in
PairingView when new validation errors occur. Add code to clear or reinitialize
the pairingChecklist property to nil before each of these two return statements,
ensuring the UI does not show outdated stage results when validation fails on a
fresh submission.
- Around line 3239-3395: Extract the pairing-checklist state machine out of
MobileShellComposite into a focused collaborator type to reduce the size of the
already oversized class. Create a new dedicated type (either a struct or class)
that encapsulates the checklist state (pairingChecklist,
pairingAttemptReachedMac, isForegroundPairingAttempt) and owns the four
checklist-related methods: beginPairingChecklist(), resolvePairingChecklist(),
markPairingChecklistConnected(), and clearPairingChecklist(). Then refactor
MobileShellComposite to instantiate and delegate to this new collaborator
instead of managing the checklist logic directly, passing necessary dependencies
like isForegroundPairingAttempt and pairingAttemptMethod through method
parameters or initializer as needed.
🪄 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: b7f3e639-135d-4805-9bae-cb715ba69533

📥 Commits

Reviewing files that changed from the base of the PR and between d4d2648 and dbb114b.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositeChecklistTests.swift

austinywang and others added 2 commits June 14, 2026 03:08
…c reached

Autoreview follow-up: `requestManualAttachTicket` only recorded the reached-Mac
signal on its catch path. A successful `mobile.attach_ticket.create` round trip
also proves the Mac was reached, so if the subsequent `connect(ticket:)` failed
locally before sending (e.g. the Stack token provider fails on the workspace-list
request), the checklist showed Network untested. Set `pairingAttemptReachedMac`
on the probe's success path too (with the same generation check). Adds a
regression test driving a successful probe followed by a pre-send token failure
in `connect`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Autoreview follow-up: the foreground/background gate was applied only when
publishing, so a non-foreground attempt (background reconnect, host switch,
device-tree tap) that superseded an in-flight Add Device attempt left the old
`pairingChecklist` (e.g. a stuck `.connecting`) in the store. In PairingView a
non-nil checklist hides `connectionError`, so the user could see a stale spinner
with the real failure suppressed.

Make the checklist attempt-scoped like `connectionError`: reset it at the start
of every instrumented attempt — a foreground attempt starts the network gate, any
other attempt clears it. Adds a regression test that a superseding background
attempt clears a foreground checklist.

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

# Conflicts:
#	.github/swift-file-length-budget.tsv
#	Resources/Localizable.xcstrings
…g-checkmarks

# Conflicts:
#	.github/swift-file-length-budget.tsv
…g-checkmarks

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: pairing flow should show network / auth / trust states as individual check marks

3 participants