Skip to content

Require matching email for iOS pairing#6028

Merged
lawrencecchen merged 35 commits into
mainfrom
feat-ios-pairing-email-version-warning
Jun 14, 2026
Merged

Require matching email for iOS pairing#6028
lawrencecchen merged 35 commits into
mainfrom
feat-ios-pairing-email-version-warning

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • include Mac signed-in email plus Mac app version/build in iOS attach tickets and compact QR payloads
  • block iOS pairing before dialing when the phone email differs from the Mac email
  • replace the hard same-version requirement with a warning that the user can accept

Validation

  • swift test in Packages/CMUXMobileCore
  • swift test in Packages/CmuxMobileRPC
  • swift test in Packages/CmuxMobileShell
  • swift test in Packages/CmuxMobileWorkspace
  • xcodebuild build -workspace ios/cmux.xcworkspace -scheme cmux-ios -destination id=5E1A1E60-52E0-4ACE-B75B-67CFB27EECAB -derivedDataPath /tmp/cmux-emver-ios-build
  • xcodebuild test -workspace cmux.xcworkspace -scheme cmux-unit -only-testing:cmuxTests/MobileHostAuthorizationTests/testStackEmailAuthorizationRequiresMatchingEmail -only-testing:cmuxTests/MobileHostAuthorizationTests/testStackEmailAuthorizationRequiresSignedInMacUser -destination platform=macOS -derivedDataPath /tmp/cmux-emver-unit-test

Notes

  • Direct swift test in ios/cmuxPackage is blocked by the package manifest defaulting library products to macOS 10.13 while dependencies require macOS 14.0; the normal iOS app Xcode build passed.
  • The wider MobileHostAuthorizationTests class run had one unrelated failure in testMobileHostConnectionCloseLeavesViewportReportsForPollingClient; the two changed email-authorization tests passed on targeted rerun.

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


Note

Medium Risk
Changes sit on the pairing and Stack authorization path (account matching and pre-connect gates), but behavior is covered by targeted mobile-core/RPC/shell tests and legacy QR decoding remains tolerant.

Overview
Pairing payloads on Mac and iOS now embed non-secret context: opaque Stack user id on v2 QR (ub), pairing compatibility level, and app version/build, wired through CmxAttachTicket, compact v1 JSON, and Mac ticket minting. Public QRs prefer user id over email; legacy compact payloads that still carry an email in u keep decoding.

Before dialing, the iOS shell compares the ticket to the signed-in identity (user id when present, otherwise normalized email) and surfaces a new emailMismatch failure with localized copy. Compatibility skew no longer hard-fails: mismatched or missing pc yields needsUserApproval, a stored warning, and Continue anyway in the pairing sheet and host picker; attach-ticket auth is not cleared while approval is pending.

Mac host responses add mac_app_version / mac_app_build, and Stack authorization is aligned on user id matching. Smaller follow-ons include extracting PhonePushPayload, pairing-flow connection-attempt guards, and test harness updates (including MobileHostAuthorizationTests → Swift Testing).

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


Summary by cubic

Require the same account for iOS pairing (QR uses an opaque user id; legacy uses normalized email). Add a user‑approved compatibility warning shown in the host picker and pairing view, restore Mac↔iPhone notification dismiss/badge sync, keep existing connections intact, and record pairing validation analytics.

  • New Features

    • Pairing: v2 QR adds ub (user id), pc (compat), av (version), ab (build); compact tickets add u/pc/av/ab. iOS preflights with user id when present, else compares normalized emails; mismatches show a localized error. Validation analytics are recorded for mismatches and warnings.
    • Version warning: missing or skewed pc returns .needsUserApproval. The warning is presented in the host picker and pairing sheet; users can accept to continue. Existing connections aren’t cleared, attach URLs wait for auth restore, the pairing sheet stays reachable, and the host picker dismisses after a retry even if it fails.
    • Notification sync: Mac pushes dismiss/badge updates; iOS clears delivered banners by id, sets absolute badges, reconciles on subscribe, and flushes a durable dismiss outbox.
  • Refactors

    • Embed account/build context in tickets, QR, and mobile.host.status; expose mac_app_version/mac_app_build and a shared pairingCompatibilityVersion. Legacy QR inputs without compatibility decode as 0 for warning-only checks.
    • Split pairing validation from connection with a .needsUserApproval result and guarded acceptance taps. Extract PhonePushPayload/PhonePushPayloadKind. Move the no‑op delivered‑notification clearer to its own file and update tests/UI to surface the new warning.

Written for commit 454c29f. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Stronger pairing checks: email-mismatch errors with tailored guidance and a compatibility-version warning that requires explicit “Continue anyway” approval.
    • Pairing links can carry limited derived metadata (user ID, app version/build) while avoiding direct email exposure.
    • Cross-device notification sync: durable dismiss queue, delivered-banner clearing, reconciliation, and badge updates.
  • Documentation / Localization

    • Updated pairing/localized messages for email guidance and version-warning UI.

@vercel

vercel Bot commented Jun 13, 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 14, 2026 4:56am
cmux-staging Building Building Preview, Comment Jun 14, 2026 4:56am

@coderabbitai

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

Extend attachment tickets with Mac email, pairing-compat version, app version, and build fields; expand v2 pairing QR to encode/decode those fields; add email-mismatch classification and an approval-gated pairing-version warning flow with UI, localization, and tests.

Changes

Mac Pairing with Email, App Metadata, and Version Skew Handling

Layer / File(s) Summary
Attachment ticket data model with Mac metadata fields
Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxTransport.swift
CmxAttachTicket adds five optional stored properties (macUserEmail, macUserID, macPairingCompatibilityVersion, macAppVersion, macAppBuild), updates Codable keys and init(from:), and extends the public initializer with corresponding optional parameters defaulting to nil.
QR/pairing URL grammar and encoding for metadata query parameters
Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
v2 URL grammar expanded to encode/decode optional ub/pc/av/ab query parameters; encode(_:) builds ordered query items beginning with v=2, percent-encodes normalized metadata (disallowing &, =, +), then appends r= route items; decode(_:) populates ticket metadata fields from query parameters.
Compact attach ticket DTO with metadata fields
Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxAttachTicketCompactCoder.swift, Packages/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift
CompactAttachTicket adds compact DTO keys u, av, ab, maps them from/to CmxAttachTicket with normalization, and documentation/tests updated to reflect new short keys.
Build identity extraction and service integration with metadata
Sources/Mobile/MobileHostBuildIdentity.swift, Sources/Mobile/MobileAttachTicketStore.swift, Sources/Mobile/MobileHostService.swift, Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileHostStatusResponse.swift, cmux.xcodeproj/project.pbxproj
New MobileHostBuildIdentity reads normalized CFBundle version/build; MobileHostService.identityStatusPayload includes mac_app_version/mac_app_build; createAttachTicket forwards signed-in local user email/ID and app version/build into ticket creation; MobileHostStatusResponse decodes RPC app metadata; Xcode project updated to include new source.
Email-based Stack authorization policy
Sources/Mobile/MobileHostService.swift
Authorization policy updated to accept optional remote user-id (authorizeStackUserID(localUserID:remoteUserID:)) and normalize IDs; verifier/cache updated to optional userID and nullable remote fetch; tests migrated to Swift Testing reflect new semantics.
Email mismatch pairing failure and messaging
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
Adds emailMismatch(expected:actual:) case, maps analytics reason to "email_mismatch", classifies it as an authorization failure, and adds localized message/guidance formatting; tests extended accordingly.
Pairing version warning state and flow in MobileShellComposite
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Adds pairingVersionWarning and pendingPairingVersionWarningURL, refactors connect URL handling to detect email mismatches and version-skew warnings, transitions to .needsUserApproval when required, preserves ticket metadata when adopting identity, and introduces connection-attempt generation guards.
Connection result case and auth gate handling for pending approval
Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobilePairingURLConnectionResult.swift, Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift
Adds needsUserApproval enum case and updates MobileRootAuthGate.shouldClearAttachTicketAuthentication to not clear attach-ticket auth while approval is pending.
UI components for version warning prompt and acceptance
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift, Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift
Centralizes add-device sheet, passes store.pairingVersionWarning and acceptVersionWarning to PairingView; PairingView renders a version-warning section with a destructive “Continue anyway” button that calls the accept handler.
Notification dismiss/reconcile sync, delivery clearer, and outbox
Packages/CmuxMobileShell/Sources/CmuxMobileShell/DeliveredNotificationClearing.swift, Packages/CmuxMobileShell/Sources/CmuxMobileShell/SystemDeliveredNotificationClearer.swift, Packages/CmuxMobileShell/Sources/CmuxMobileShell/PendingNotificationDismissQueue.swift, Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationDismissSync.swift, Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/*
New Decodable event types for dismissed/badge/reconcile payloads; DeliveredNotificationClearing protocol and Noop implementation; SystemDeliveredNotificationClearer backed by UNUserNotificationCenter; durable PendingNotificationDismissQueue persisted in UserDefaults; MobileShellComposite handlers for dismiss/reconcile and tests validating behavior.
Tests and localization strings for metadata, authorization, version warning
Packages/CMUXMobileCore/Tests/*, Packages/CmuxMobileRPC/Tests/*, Packages/CmuxMobileShell/Tests/*, Packages/CmuxMobileWorkspace/Tests/*, cmuxTests/*, ios/cmuxPackage/Tests/*, ios/cmux/Resources/Localizable.xcstrings
Tests extended to cover compact payload keys, QR encode/decode round trips that avoid exposing email, host RPC decoding, email-based authorization failures, pairing-version-warning acceptance flow and analytics timing; localization entries for email-mismatch and version-warning strings were added/updated (English + Japanese).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#5872: Related v2 CmxPairingQRCode minimal grammar and loopback/route parsing work that this PR extends with optional metadata query items.
  • manaflow-ai/cmux#5727: Overlaps on compact attach-ticket/compact-key grammar extensions; earlier PR introduced the compact short-key grammar.
  • manaflow-ai/cmux#5506: Related localization/authorization-failure wording changes affecting pairing messages.

"🐰
I nibble keys and stitch a ticket tight,
Metadata tucked to keep the pairings right.
A gentle warning hops into view,
Approve, continue — the link renews.
Hooray — two devices meet in light!"

✨ 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-pairing-email-version-warning

Comment thread Sources/Mobile/MobileHostService.swift Outdated
Comment thread Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift Outdated

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

ℹ️ 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 +3155 to +3156
guard let phoneVersion = Self.normalizedNonEmpty(phoneStamp.appVersion),
phoneVersion != macVersion else {

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 Avoid blocking pairing on unrelated app versions

This check compares the iPhone bundle marketing version directly with the Mac bundle marketing version, but those are currently different version streams in this repo (iOS ios/Config/Shared.xcconfig has MARKETING_VERSION = 1.0.0, while the Mac project has MARKETING_VERSION = 0.64.15). With matching current builds, every v2 QR therefore returns .needsUserApproval before dialing and shows a destructive “Version mismatch” warning even when there is no real compatibility skew; consider comparing a shared protocol/compatibility version or otherwise accounting for the expected Mac/iOS version scheme.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds account and build-identity metadata to pairing tickets and QR codes, and enforces a pre-dial email/user-ID match on iOS before the phone dials the Mac. App-version skew now surfaces a user-approvable warning instead of a hard block.

  • Ticket/QR wire format: CmxAttachTicket gains macUserEmail, macUserID, macPairingCompatibilityVersion, macAppVersion, and macAppBuild; the v2 QR URL gains ub/pc/av/ab query params; the compact ticket gains u/pc/av/ab.
  • Pre-dial validation: MobileShellComposite.connectPairingURLResult now runs emailFailure (blocks on account mismatch) and versionWarning (returns .needsUserApproval for compatibility skew) before starting the network dial, with a new connectionAttemptGeneration UUID to guard stale async paths.
  • UX routing: the pairing sheet is hoisted to root, attach-URL consumption is deferred until session restoration finishes, and the host picker gains a version-warning alert with "Continue anyway" / "Cancel" actions.

Confidence Score: 4/5

Safe to merge after addressing the nil-identity silent pass in emailFailure; the version-warning and attach-URL deferral logic are sound.

The pre-dial user-ID check in emailFailure returns nil (no failure) when ticket.macUserID is set but the phone's identity provider returns nil, silently skipping the check that is meant to block wrong-account pairing. A phone mid-sign-in or with a temporarily broken identity provider can scan a user-ID-scoped QR and proceed to dial. The Mac-side token auth is still a backstop, but the phone-side guard should not fail open. Everything else — wire-format additions, version-warning flow, session-restore deferral, localization — looks correct.

MobileShellComposite.swift (emailFailure nil-identity path) and CompactAttachTicket.swift (@ heuristic for u field disambiguation)

Important Files Changed

Filename Overview
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Core pairing logic overhauled: splits validation from dialing, adds emailFailure/versionWarning pre-dial checks, introduces connectionAttemptGeneration to guard stale async paths. The nil-identity silent-skip in emailFailure and a dead clearPairingVersionWarning() call need attention.
Packages/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift Adds u/pc/av/ab fields to the compact ticket; decodes u as email when it contains '@' and as user ID otherwise — a fragile heuristic that misclassifies email-format user IDs.
Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift Encodes ub/pc/av/ab into the v2 QR URL and decodes them on input; uses separate fields for user ID and email in the URL grammar, unlike the compact ticket.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift Hoists the pairing sheet to root, defers attach-URL consumption until session restoration completes, and routes needsUserApproval results to open the sheet. Logic looks correct.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileHostPickerView.swift Adds version-warning alert; the no-op isPresented setter leaves pairingVersionWarning set if the host-picker sheet is dismissed without a button tap.
Sources/Mobile/MobileHostService.swift Renames authorizeStackUser → authorizeStackUserID and makes remoteUserID optional; calls MobileHostBuildIdentity.current() twice when minting a ticket.
ios/cmux/Resources/Localizable.xcstrings Adds en+ja translations for all seven new pairing-related string keys; existing authFailed string updated in both locales. i18n coverage looks complete.

Sequence Diagram

sequenceDiagram
    participant UI as iOS UI (QR / attach URL)
    participant MSC as MobileShellComposite
    participant ID as IdentityProvider
    participant Mac as Mac (dial)

    UI->>MSC: connectPairingURLResult(rawURL)
    MSC->>MSC: beginPairingValidationAttempt()
    MSC->>MSC: CmxAttachTicketInput.decode(rawURL)
    alt decode error
        MSC-->>UI: .failed
    end
    MSC->>ID: currentUserID / currentUserEmail
    MSC->>MSC: emailFailure(ticket, actualUserID, actualEmail)
    alt userID or email mismatch
        MSC-->>UI: .failed
    end
    MSC->>MSC: versionWarning(ticket)
    alt compatibility version differs
        MSC->>MSC: store pendingPairingVersionWarningURL
        MSC-->>UI: .needsUserApproval
        UI->>UI: show version-warning alert
        alt user accepts
            UI->>MSC: acceptPairingVersionWarning()
            MSC->>MSC: "connectPairingURLResult(stored URL, accepted=true)"
        else user cancels
            UI->>MSC: cancelPairing()
            MSC-->>UI: clears warning
        end
    end
    MSC->>MSC: beginPairingAttempt()
    MSC->>Mac: dial routes
    Mac-->>MSC: connected / failed
    MSC-->>UI: .connected / .failed
Loading

Reviews (12): Last reviewed commit: "Restore pairing validation analytics" | Re-trigger Greptile

Comment thread Sources/Mobile/MobileHostService.swift Outdated
Comment on lines 1678 to 1685
static func authorizeStackEmail(localEmail: String?, remoteEmail: String?) throws {
guard let localEmail = normalizedEmail(localEmail) else {
throw MobileHostAuthorizationError.missingLocalUser
}
guard localUserID == remoteUserID else {
guard normalizedEmail(remoteEmail) == localEmail else {
throw MobileHostAuthorizationError.accountMismatch
}
}

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 Nil remote email silently blocks legitimate users

authorizeStackEmail accepts remoteEmail: String? and treats a nil value identically to a mismatched email: normalizedEmail(nil) returns nil, nil != localEmail (which is non-nil after the guard), so the function throws accountMismatch. The old authorizeStackUser took remoteUserID: String (non-optional) because user.id was always present. The new path fetches user.primaryEmail — which the Swift type marks as optional — caches it (including nil), and passes it here without any nil guard. Any Stack account whose primaryEmail returns nil would be permanently locked out of the Mac host with a misleading "different cmux account" error, despite potentially being the legitimate owner.

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

🤖 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 `@ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift`:
- Around line 1437-1488: Add a sibling test to cover the cancel/dismiss flow:
duplicate the setup from
minimalPairingCodeVersionMismatchWarnsAndContinuesAfterAcceptance(), call
store.cancelPairing() after verifying the initial result is .needsUserApproval
(from store.connectPairingURLResult), then assert that
store.pairingVersionWarning is nil, store.connectionState remains .disconnected,
and that no workspace RPCs were sent (use try await responses.sentRequests()).
Name the test something like
minimalPairingCodeVersionMismatchClearsWarningOnCancel and use the same
ScriptedTransportResponses, testRuntime, store.signIn(), and
connectPairingURLResult invocation so it mirrors the acceptance test.
- Around line 1407-1435: In the test
minimalPairingCodeRequiresMatchingEmailBeforeDialing add an explicit assertion
that store.pairingVersionWarning is nil to ensure the failure is due to email
mismatch rather than a version warning; locate the test method and append a
check like XCTAssertNil(store.pairingVersionWarning) (or your test-framework
equivalent) alongside the existing expectations so the distinction between
email-mismatch errors and version-skew warnings is enforced.

In `@Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift`:
- Around line 270-274: The percentEncodeQueryValue(_:) currently falls back to
returning the raw unencoded value on failure which can inject reserved
characters into query strings; change it to return a safe fallback (e.g., empty
string or a fixed placeholder like "__ENCODING_FAILED__") instead of the raw
value, and adjust callers that build the query (where percentEncodeQueryValue is
used) to filter out or skip parameters whose encoded value is empty or the
placeholder so malformed values are not inserted into the URL; update references
to percentEncodeQueryValue and the query-building logic to implement this
defensive behavior.

In `@Packages/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift`:
- Around line 20-29: The CompactAttachTicket.normalizedNonEmpty function
currently checks !value.isEmpty without trimming, causing whitespace-only
strings to be kept; update normalizedNonEmpty to trim whitespace (e.g., call
.trimmingCharacters(in: .whitespacesAndNewlines)) and return nil if the trimmed
result is empty so its behavior matches CmxPairingQRCode.normalizedNonEmpty and
MobileHostBuildIdentity.normalized; ensure this change applies to all callers in
CompactAttachTicket.init (workspaceID, terminalID, macUserEmail, macAppVersion,
macAppBuild) and verify trimming is acceptable for those fields.
🪄 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: 619e33bd-d3bc-44c7-90e9-4c5779d4321d

📥 Commits

Reviewing files that changed from the base of the PR and between af32009 and ddaaa86.

📒 Files selected for processing (23)
  • Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxAttachTicketCompactCoder.swift
  • Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift
  • Packages/CMUXMobileCore/Sources/CMUXMobileCore/CmxTransport.swift
  • Packages/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift
  • Packages/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxAttachTicketCompactCoderTests.swift
  • Packages/CMUXMobileCore/Tests/CMUXMobileCoreTests/CmxPairingQRCodeTests.swift
  • Packages/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileHostStatusResponse.swift
  • Packages/CmuxMobileRPC/Tests/CmuxMobileRPCTests/CmxAttachTicketInputTests.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePairingFailureTests.swift
  • Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobilePairingURLConnectionResult.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/PairingView.swift
  • Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileRootAuthGate.swift
  • Packages/CmuxMobileWorkspace/Tests/CmuxMobileWorkspaceTests/MobileRootAuthGateTests.swift
  • Sources/Mobile/MobileAttachTicketStore.swift
  • Sources/Mobile/MobileHostBuildIdentity.swift
  • Sources/Mobile/MobileHostService.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MobileHostAuthorizationTests.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment on lines +1407 to +1435
@MainActor
@Test func minimalPairingCodeRequiresMatchingEmailBeforeDialing() async throws {
let responses = ScriptedTransportResponses([
try rpcWorkspaceListFrame(workspaceID: "qr-workspace", title: "QR Workspace"),
])
let runtime = testRuntime(
supportedRouteKinds: [.tailscale],
transportFactory: ScriptedTransportFactory(responses: responses)
)
let store = CMUXMobileShellStore(
runtime: runtime,
workspaces: PreviewMobileHost.workspaces,
identityProvider: TestIdentityProvider(
currentUserIDValue: "phone-user",
currentUserEmailValue: "phone@example.com"
)
)

store.signIn()
let result = await store.connectPairingURLResult(
"cmux-ios://attach?v=2&e=mac@example.com&r=100.71.210.41:\(CmxMobileDefaults.defaultHostPort)"
)

#expect(result == .failed)
#expect(store.connectionState == .disconnected)
#expect(store.connectionError?.contains("mac@example.com") == true)
#expect(store.connectionError?.contains("phone@example.com") == true)
#expect(try await responses.sentRequests().isEmpty)
}

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 | 💤 Low value

Consider verifying pairingVersionWarning is nil.

The test validates email mismatch rejection, but doesn't explicitly assert that store.pairingVersionWarning == nil. Adding this would strengthen the distinction between email-mismatch failures (which should not show a version warning) and version-skew scenarios.

Optional assertion to add
 `#expect`(store.connectionState == .disconnected)
 `#expect`(store.connectionError?.contains("mac@example.com") == true)
 `#expect`(store.connectionError?.contains("phone@example.com") == true)
+#expect(store.pairingVersionWarning == nil)
 `#expect`(try await responses.sentRequests().isEmpty)
🤖 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 `@ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift` around lines
1407 - 1435, In the test minimalPairingCodeRequiresMatchingEmailBeforeDialing
add an explicit assertion that store.pairingVersionWarning is nil to ensure the
failure is due to email mismatch rather than a version warning; locate the test
method and append a check like XCTAssertNil(store.pairingVersionWarning) (or
your test-framework equivalent) alongside the existing expectations so the
distinction between email-mismatch errors and version-skew warnings is enforced.

Comment on lines +1437 to +1488
@MainActor
@Test func minimalPairingCodeVersionMismatchWarnsAndContinuesAfterAcceptance() async throws {
let responses = ScriptedTransportResponses([
try rpcWorkspaceListFrame(workspaceID: "qr-workspace", title: "QR Workspace"),
try rpcHostStatusFrame(
renderGrid: false,
macDeviceID: "status-reported-mac",
macDisplayName: "Status Mac"
),
])
let runtime = testRuntime(
supportedRouteKinds: [.tailscale],
transportFactory: ScriptedTransportFactory(responses: responses),
supportsServerPushEvents: false
)
let store = CMUXMobileShellStore(
runtime: runtime,
workspaces: PreviewMobileHost.workspaces,
identityProvider: TestIdentityProvider(
currentUserIDValue: "phone-user",
currentUserEmailValue: "user@example.com"
),
feedbackStampProvider: {
MobileFeedbackStamp(
buildType: .dev,
appVersion: "0.65.0",
appBuild: "10",
bundleIdentifier: "dev.cmux.ios.test",
osVersion: "iOS test",
deviceModel: "test"
)
}
)

store.signIn()
let result = await store.connectPairingURLResult(
"cmux-ios://attach?v=2&e=user@example.com&av=0.64.0&ab=9&r=100.71.210.41:\(CmxMobileDefaults.defaultHostPort)"
)

#expect(result == .needsUserApproval)
#expect(store.connectionState == .disconnected)
#expect(store.pairingVersionWarning?.contains("0.65.0 (10)") == true)
#expect(store.pairingVersionWarning?.contains("0.64.0 (9)") == true)
#expect(try await responses.sentRequests().isEmpty)

await store.acceptPairingVersionWarning()

#expect(store.pairingVersionWarning == nil)
#expect(store.connectionState == .connected)
#expect(store.selectedWorkspace?.id.rawValue == "qr-workspace")
#expect(try await responses.sentRequests().contains { $0.method == "workspace.list" })
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add test coverage for version warning rejection/cancellation.

This test validates the acceptance path (acceptPairingVersionWarning()), but per the review stack layer 7, the feature also "clears warning on pairing-cancel." Consider adding a sibling test that validates the cancel/dismiss flow: when the version warning is shown (.needsUserApproval) and the user cancels pairing (e.g., store.cancelPairing()), verify that pairingVersionWarning is cleared and the connection remains disconnected.

🤖 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 `@ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift` around lines
1437 - 1488, Add a sibling test to cover the cancel/dismiss flow: duplicate the
setup from minimalPairingCodeVersionMismatchWarnsAndContinuesAfterAcceptance(),
call store.cancelPairing() after verifying the initial result is
.needsUserApproval (from store.connectPairingURLResult), then assert that
store.pairingVersionWarning is nil, store.connectionState remains .disconnected,
and that no workspace RPCs were sent (use try await responses.sentRequests()).
Name the test something like
minimalPairingCodeVersionMismatchClearsWarningOnCancel and use the same
ScriptedTransportResponses, testRuntime, store.signIn(), and
connectPairingURLResult invocation so it mirrors the acceptance test.

Comment on lines +270 to +274
func percentEncodeQueryValue(_ value: String) -> String {
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: "&=+")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
}

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 | 💤 Low value

Consider explicit handling when percent-encoding fails.

The fallback ?? value returns the original unencoded value if addingPercentEncoding fails. For email addresses with + (e.g., user+tag@example.com) or versions with special characters, this could inject reserved characters into the URL query, potentially breaking parsing.

While encoding failure is rare for typical email/version strings, explicit handling would be more defensive.

🛡️ Safer fallback options

Option 1: Return empty string and skip the parameter:

 func percentEncodeQueryValue(_ value: String) -> String {
     var allowed = CharacterSet.urlQueryAllowed
     allowed.remove(charactersIn: "&=+")
-    return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
+    return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
 }

Then filter empty results:

 if let email = normalizedNonEmpty(ticket.macUserEmail) {
-    items.append("e=\(percentEncodeQueryValue(email))")
+    let encoded = percentEncodeQueryValue(email)
+    if !encoded.isEmpty {
+        items.append("e=\(encoded)")
+    }
 }

Option 2: Use a safe placeholder:

-    return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
+    return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? "ENCODING_FAILED"
📝 Committable suggestion

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

Suggested change
func percentEncodeQueryValue(_ value: String) -> String {
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: "&=+")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value
}
func percentEncodeQueryValue(_ value: String) -> String {
var allowed = CharacterSet.urlQueryAllowed
allowed.remove(charactersIn: "&=+")
return value.addingPercentEncoding(withAllowedCharacters: allowed) ?? "ENCODING_FAILED"
}
🤖 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/CMUXMobileCore/Sources/CMUXMobileCore/CmxPairingQRCode.swift` around
lines 270 - 274, The percentEncodeQueryValue(_:) currently falls back to
returning the raw unencoded value on failure which can inject reserved
characters into query strings; change it to return a safe fallback (e.g., empty
string or a fixed placeholder like "__ENCODING_FAILED__") instead of the raw
value, and adjust callers that build the query (where percentEncodeQueryValue is
used) to filter out or skip parameters whose encoded value is empty or the
placeholder so malformed values are not inserted into the URL; update references
to percentEncodeQueryValue and the query-building logic to implement this
defensive behavior.

Comment on lines 20 to 29
init(_ ticket: CmxAttachTicket) {
v = ticket.version
w = Self.normalizedNonEmpty(ticket.workspaceID)
t = Self.normalizedNonEmpty(ticket.terminalID)
d = ticket.macDeviceID
u = Self.normalizedNonEmpty(ticket.macUserEmail)
av = Self.normalizedNonEmpty(ticket.macAppVersion)
ab = Self.normalizedNonEmpty(ticket.macAppBuild)
r = Self.compactedRoutes(ticket.routes)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalization inconsistency across metadata fields.

CompactAttachTicket.normalizedNonEmpty (line 46) checks !value.isEmpty without trimming whitespace, while CmxPairingQRCode.normalizedNonEmpty (CmxPairingQRCode.swift:265) and MobileHostBuildIdentity.normalized (MobileHostBuildIdentity.swift:14) trim whitespace before checking. This means:

  • A whitespace-only macUserEmail like " " would be omitted from QR URLs (normalized to nil after trim) but included in compact payloads (kept as-is).
  • The same ticket could encode differently depending on which grammar is used.

Since these values come from controlled sources (authenticated email, Bundle info), the practical risk is low, but consistency would prevent subtle edge-case bugs.

🔧 Proposed fix to align normalization

Update normalizedNonEmpty to trim whitespace like the other normalizers:

 private static func normalizedNonEmpty(_ value: String?) -> String? {
-    guard let value, !value.isEmpty else {
-        return nil
-    }
-    return value
+    let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
+    return trimmed?.isEmpty == false ? trimmed : nil
 }

Note: This also affects workspaceID and terminalID (lines 22-23), so verify that trimming is appropriate for those fields as well.

🤖 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/CMUXMobileCore/Sources/CMUXMobileCore/CompactAttachTicket.swift`
around lines 20 - 29, The CompactAttachTicket.normalizedNonEmpty function
currently checks !value.isEmpty without trimming, causing whitespace-only
strings to be kept; update normalizedNonEmpty to trim whitespace (e.g., call
.trimmingCharacters(in: .whitespacesAndNewlines)) and return nil if the trimmed
result is empty so its behavior matches CmxPairingQRCode.normalizedNonEmpty and
MobileHostBuildIdentity.normalized; ensure this change applies to all callers in
CompactAttachTicket.init (workspaceID, terminalID, macUserEmail, macAppVersion,
macAppBuild) and verify trimming is acceptable for those fields.

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

ℹ️ 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 +75 to +77
.sheet(isPresented: addDeviceSheetBinding) {
pairingSheet
}

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 Dismiss the pairing sheet when auth drops

With the sheet now attached to rootContent, it is no longer scoped to the authenticated disconnected branch. If Stack revalidation or sign-out flips isAuthenticated to false while Add device is presented, the false branch in onChange(of: isAuthenticated) only returns, so isShowingAddDeviceSheet stays true and the pairing UI remains over SignInView instead of being torn down as before; clear/dismiss the sheet on auth loss.

Useful? React with 👍 / 👎.

@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: 6cb16477fe

ℹ️ 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 +3104 to +3106
guard let macCompatibilityVersion = ticket.macPairingCompatibilityVersion,
macCompatibilityVersion != CmxMobileDefaults.pairingCompatibilityVersion else {
return nil

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 Treat missing compact compatibility as a warning

When a legacy/full or compact payload attach URL lacks macPairingCompatibilityVersion, the decoded ticket carries nil and this guard returns no warning, so the phone dials immediately. That bypasses the new approval gate for exactly the pre-compatibility payloads still accepted by CmxAttachTicketInput (for example workspace-scoped/custom-route fallback URLs), while the v2 decoder maps a missing pc to unknown compatibility and warns; treat nil as unknown here instead of as compatible.

Useful? React with 👍 / 👎.

@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: 173742f517

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

macDisplayName: macDisplayName ?? fallbackDisplayName,
macUserEmail: macUserEmail,
macUserID: macUserID,
macPairingCompatibilityVersion: macPairingCompatibilityVersion,

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 Gate manual attach tickets on compatibility warnings

When Add device is used with a typed host, connectManualHost requests mobile.attach_ticket.create, decodes MobileManualAttachTicketCreateResponse, then calls response.ticket.constrainingRoutes(...) and immediately connects that ticket; this newly preserved macPairingCompatibilityVersion is never checked with versionWarning(for:). A Mac returning a compatibility level different from the iOS CmxMobileDefaults.pairingCompatibilityVersion therefore bypasses the explicit approval warning enforced for scanned/pasted URLs and can pair incompatible builds without user consent.

Useful? React with 👍 / 👎.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 005cedc. Configure here.

Comment thread Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Outdated

@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: 454c29f11f

ℹ️ 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 isAuthenticated else { return false }
pendingAttachURL = nil
Task {
await store.connectPairingURL(rawURL)

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 Handle approval for queued legacy pairing URLs

When a queued non-attach pairing URL is consumed after authentication (for example an older cmux-ios://pair link saved while the user was signed out/restoring), this still calls the Boolean connectPairingURL wrapper. This commit now normalizes those legacy payloads to compatibility 0, so connectPairingURLResult can return .needsUserApproval; the wrapper collapses that to false and no code opens the add-device sheet, leaving store.pairingVersionWarning hidden and the pairing flow stalled. Use the result-returning API here and surface the sheet/approval UI as connectAttachURL does.

Useful? React with 👍 / 👎.

@lawrencecchen lawrencecchen merged commit 38bcb99 into main Jun 14, 2026
25 of 28 checks passed
@lawrencecchen lawrencecchen deleted the feat-ios-pairing-email-version-warning branch June 14, 2026 05:19
lawrencecchen added a commit that referenced this pull request Jun 14, 2026
…enable presence-online-preferred via live presence (#5792)

Two post-rebase changes:

1. Manual pairing must supersede an in-flight auto-attach at the TOP of
   connectPairingURLResult, before validation, so even an invalid code
   parks the background attempt (the documented invariant the
   manualPairingSupersedes* / invalidManualHost* tests assert). Main's
   #6028 refactor split the post-validation beginPairingAttempt(method:
   "qr") out of the early path, so the supersede no longer rode on it.
   Call supersedeInFlightAutoAttach() explicitly; it is guarded on
   autoAttachInFlight, so it is a no-op when nothing is parked.

2. Presence #5792 has landed, so wire its live signal into auto-attach
   target selection (deferred when the PR was opened). The shell already
   owns a live PresenceMap from the device-tree subscription; derive
   online device ids from it (new PresenceMap.onlineDeviceIDs, same
   rollup rule as deviceSummary) when no explicit MobileAutoAttachPresence
   provider is injected and presence data exists. The external provider
   seam still wins if set, and an empty map (no presence yet) keeps the
   recency-only path unchanged, so existing behavior and tests are
   preserved. This stays behind the auto-attach flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 14, 2026
…enable presence-online-preferred via live presence (#5792)

Two post-rebase changes:

1. Manual pairing must supersede an in-flight auto-attach at the TOP of
   connectPairingURLResult, before validation, so even an invalid code
   parks the background attempt (the documented invariant the
   manualPairingSupersedes* / invalidManualHost* tests assert). Main's
   #6028 refactor split the post-validation beginPairingAttempt(method:
   "qr") out of the early path, so the supersede no longer rode on it.
   Call supersedeInFlightAutoAttach() explicitly; it is guarded on
   autoAttachInFlight, so it is a no-op when nothing is parked.

2. Presence #5792 has landed, so wire its live signal into auto-attach
   target selection (deferred when the PR was opened). The shell already
   owns a live PresenceMap from the device-tree subscription; derive
   online device ids from it (new PresenceMap.onlineDeviceIDs, same
   rollup rule as deviceSummary) when no explicit MobileAutoAttachPresence
   provider is injected and presence data exists. The external provider
   seam still wins if set, and an empty map (no presence yet) keeps the
   recency-only path unchanged, so existing behavior and tests are
   preserved. This stays behind the auto-attach flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 14, 2026
…enable presence-online-preferred via live presence (#5792)

Two post-rebase changes:

1. Manual pairing must supersede an in-flight auto-attach at the TOP of
   connectPairingURLResult, before validation, so even an invalid code
   parks the background attempt (the documented invariant the
   manualPairingSupersedes* / invalidManualHost* tests assert). Main's
   #6028 refactor split the post-validation beginPairingAttempt(method:
   "qr") out of the early path, so the supersede no longer rode on it.
   Call supersedeInFlightAutoAttach() explicitly; it is guarded on
   autoAttachInFlight, so it is a no-op when nothing is parked.

2. Presence #5792 has landed, so wire its live signal into auto-attach
   target selection (deferred when the PR was opened). The shell already
   owns a live PresenceMap from the device-tree subscription; derive
   online device ids from it (new PresenceMap.onlineDeviceIDs, same
   rollup rule as deviceSummary) when no explicit MobileAutoAttachPresence
   provider is injected and presence data exists. The external provider
   seam still wins if set, and an empty map (no presence yet) keeps the
   recency-only path unchanged, so existing behavior and tests are
   preserved. This stays behind the auto-attach flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Jun 14, 2026
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>
lawrencecchen added a commit that referenced this pull request Jun 14, 2026
…enable presence-online-preferred via live presence (#5792)

Two post-rebase changes:

1. Manual pairing must supersede an in-flight auto-attach at the TOP of
   connectPairingURLResult, before validation, so even an invalid code
   parks the background attempt (the documented invariant the
   manualPairingSupersedes* / invalidManualHost* tests assert). Main's
   #6028 refactor split the post-validation beginPairingAttempt(method:
   "qr") out of the early path, so the supersede no longer rode on it.
   Call supersedeInFlightAutoAttach() explicitly; it is guarded on
   autoAttachInFlight, so it is a no-op when nothing is parked.

2. Presence #5792 has landed, so wire its live signal into auto-attach
   target selection (deferred when the PR was opened). The shell already
   owns a live PresenceMap from the device-tree subscription; derive
   online device ids from it (new PresenceMap.onlineDeviceIDs, same
   rollup rule as deviceSummary) when no explicit MobileAutoAttachPresence
   provider is injected and presence data exists. The external provider
   seam still wins if set, and an empty map (no presence yet) keeps the
   recency-only path unchanged, so existing behavior and tests are
   preserved. This stays behind the auto-attach flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Jun 14, 2026
…enable presence-online-preferred via live presence (#5792)

Two post-rebase changes:

1. Manual pairing must supersede an in-flight auto-attach at the TOP of
   connectPairingURLResult, before validation, so even an invalid code
   parks the background attempt (the documented invariant the
   manualPairingSupersedes* / invalidManualHost* tests assert). Main's
   #6028 refactor split the post-validation beginPairingAttempt(method:
   "qr") out of the early path, so the supersede no longer rode on it.
   Call supersedeInFlightAutoAttach() explicitly; it is guarded on
   autoAttachInFlight, so it is a no-op when nothing is parked.

2. Presence #5792 has landed, so wire its live signal into auto-attach
   target selection (deferred when the PR was opened). The shell already
   owns a live PresenceMap from the device-tree subscription; derive
   online device ids from it (new PresenceMap.onlineDeviceIDs, same
   rollup rule as deviceSummary) when no explicit MobileAutoAttachPresence
   provider is injected and presence data exists. The external provider
   seam still wins if set, and an empty map (no presence yet) keeps the
   recency-only path unchanged, so existing behavior and tests are
   preserved. This stays behind the auto-attach flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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