Skip to content

Open iOS Tailscale pairing on strict method selection - #9720

Closed
azooz2003-bit wants to merge 6 commits into
mainfrom
fix-ios-tailscale-disconnect
Closed

azooz2003-bit wants to merge 6 commits into
mainfrom
fix-ios-tailscale-disconnect

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

Cause: selecting Tailscale Only without an exact device-local route grant made the strict choice active, but did not lead the user into the required pairing flow.

Fix: persist Tailscale Only immediately, disconnect non-Tailscale transports as requested, and open the QR scanner when the active Mac lacks a Tailscale grant. Cancelling or failing pairing leaves Tailscale Only selected so the failure stays visible. Existing authorized routes switch without reopening pairing.

Verification:

  • swift test --package-path Packages/iOS/CmuxMobileShellModel --filter MobileConnectionMethodStoreTests
  • isolated simulator behavior check
  • tagged macOS and iPhone builds

Note

Medium Risk
Changes connection routing UX and persists Tailscale Only before pairing completes, which can trigger existing connection-method recovery and leave the app disconnected until pairing succeeds; scope is mobile settings/onboarding, not auth core.

Overview
Selecting Tailscale Only now persists the choice immediately and, when the active Mac has no exact device-local Tailscale grant, opens the QR pairing scanner from onboarding, Settings, and onboarding replay. If the user cancels pairing, Tailscale Only stays selected so the missing authorization remains visible instead of silently reverting.

MobileConnectionMethodStore.request(_:hasAuthorizedTailscaleRoute:) centralizes that behavior: it saves the method and returns whether the UI must present pairing. Settings and root onboarding wire the connection-method picker through a custom binding that calls request and launches the scanner on true.

The shell exposes activeMacHasAuthorizedTailscaleRoute, derived from stored reconnect routes and Tailscale grant metadata for the active paired Mac, so UI layers do not duplicate route logic. When a grant already exists, selecting Tailscale Only commits without reopening pairing.

Unit tests cover unauthorized vs authorized Tailscale requests and switching back to Auto-Connect; a UI test expects the scanner after Tailscale selection and confirms selection after cancel.

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

Summary by CodeRabbit

  • New Features
    • Improved Tailscale connection selection during onboarding and Settings.
    • Automatically starts pairing when no authorized Tailscale route is available.
    • Preserves Tailscale selection when pairing is cancelled or pending.
    • Uses existing authorized routes for immediate Tailscale connections.
  • Bug Fixes
    • Connection-method choices now consistently reflect the active Mac’s available routes.
  • Tests
    • Expanded coverage for Tailscale pairing, cancellation, and automatic connection behavior.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The iOS connection flow checks active Mac Tailscale route authorization before committing a connection method. Unauthorized Tailscale selections remain selected while pairing starts. Authorized selections commit immediately.

Changes

Tailscale method flow

Layer / File(s) Summary
Pending method request handling
Packages/iOS/CmuxMobileShellModel/Sources/.../MobileConnectionMethodStore.swift, Packages/iOS/CmuxMobileShellModel/Tests/.../MobileConnectionMethodStoreTests.swift
request persists the selected method and reports when unauthorized Tailscale pairing is required. Tests cover unauthorized, automatic, and authorized requests.
Active Mac route authorization
Packages/iOS/CmuxMobileShell/Sources/.../MobileShellComposite.swift
The active Mac reports whether it has an authorized device-local Tailscale route.
Authorization-aware selection flow
Packages/iOS/CmuxMobileShellUI/Sources/.../CMUXMobileRootView.swift, MobileSettingsView.swift, MobileConnectionMethodSection.swift, ios/cmuxUITests/cmuxUITests.swift
Onboarding, settings, and picker selections use the request flow. Pairing starts when required. UI tests verify scanner presentation, cancellation, and retained Tailscale selection.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MobileShellComposite
  participant MobileConnectionMethodSection
  participant MobileConnectionMethodStore
  participant PairingScanner
  MobileShellComposite->>MobileConnectionMethodSection: provide authorized Tailscale route status
  MobileConnectionMethodSection->>MobileConnectionMethodStore: request selected connection method
  MobileConnectionMethodStore-->>MobileConnectionMethodSection: return pairing required
  MobileConnectionMethodSection->>PairingScanner: start Tailscale pairing
Loading

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux No Ambient Global State ❓ Inconclusive Investigation not complete. Inspect the production Swift changes and compare them with the no-ambient-global-state rule.
✅ Passed checks (23 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed The diff adds no new Sendable models, protocols, or shared Sendable references; MobileConnectionMethodStore and MobileShellComposite remain @MainActor, and all new store calls are UI-bound.
Cmux Swift Blocking Runtime ✅ Passed The feature-series production diff adds no semaphores, waits, sleeps, delayed dispatch, polling, main-queue sync, or locks; the only new wait is deterministic UI-test scaffolding.
Cmux Browser Automation Off-Main ✅ Passed The PR changes only iOS files; Sources/TerminalController.swift, ControlCommandExecutionPolicy.swift, and its tests are unchanged, with no browser/WebKit routing changes.
Cmux Expensive Synchronous Load ✅ Passed The final PR diff adds no agent-history, file, JSON, directory, or syscall loader. The new main-actor property only filters cached pairedMac routes in memory.
Cmux Cache Substitution Correctness ✅ Passed No fresh read was replaced; cached pairedMacs only gates the transient pairing scanner, while request persists the selected method. Cold state conservatively returns false.
Cmux No Hacky Sleeps ✅ Passed The merge-base diff changes only Swift files; it introduces no TypeScript, JavaScript, shell, or build/runtime-script sleeps or fixed waits.
Cmux Algorithmic Complexity ✅ Passed The PR scans one active Mac and its per-instance routes; it adds no batch or nested workspace/session scan, and reuses the existing route-selection helper.
Cmux Swift Concurrency ✅ Passed The aggregate Swift diff adds no DispatchQueue, Task, Combine, completion-handler, or fire-and-forget pattern; it adds synchronous request, binding, and route-property logic.
Cmux Swift @Concurrent ✅ Passed The PR adds only synchronous, @MainActor-isolated request and route-check APIs; the diff adds no async/nonisolated/@Concurrent work or heavy async UI call site.
Cmux Swift Package Boundaries ✅ Passed All new production logic is in existing CmuxMobileShellModel, CmuxMobileShell, and CmuxMobileShellUI SwiftPM targets; app-target changes are absent, and model logic has package tests.
Cmux Swiftpm Lockfiles ✅ Passed The feature-range diff contains no Package.swift, Package.resolved, .gitignore, workflow, or Xcode package-reference changes, so the SwiftPM lockfile requirements are not triggered.
Cmux Swift Logging ✅ Passed The PR diff adds no print, debugPrint, dump, NSLog, ad hoc logging, Logger declarations, or sensitive-data logs; the existing mobileShellLog is unchanged.
Cmux User-Facing Error Privacy ✅ Passed The combined diff adds no user-facing error, alert, API body, or recovery copy; it only routes Tailscale selections to the existing scanner, and Tailscale is explicitly user-configured.
Cmux Full Internationalization ✅ Passed The PR adds no production user-facing text or catalog/message changes; added Swift lines are logic/comments, and existing UI strings remain through L10n.string.
Cmux Swiftui State Layout ✅ Passed The diff adds no prohibited state wrappers, GeometryReader, lazy/list store subtree, or render-time writes; existing @Observable/@bindable usage remains unchanged and mutations run from event callb...
Cmux Architecture Rethink ✅ Passed The diff adds no timing or blocking repair path, mutable side channel, or new observer; the store owns the persisted method and UI passes route snapshots through request with explicit scanner closu...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR changes iOS connection-method state and existing sheet callbacks only; no changed file adds or materially changes NSWindow, NSPanel, Window, WindowGroup, or close-shortcut code.
Cmux Source Artifacts ✅ Passed The PR diff contains only seven hand-written Swift source/UI/test files; it adds no logs, screenshots, recordings, temp/cache/build directories, or dependency checkouts.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The new production members have real callers in CMUXMobileRootView, MobileSettingsView, and MobileConnectionMethodSection; no test-only guard or seam name was added, and debug additions are isolate...
Title check ✅ Passed The title clearly identifies the iOS Tailscale pairing behavior changed by this pull request.
Description check ✅ Passed The description explains the cause, fix, scope, and testing, but omits the template’s demo video, review trigger, and checklist sections.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-ios-tailscale-disconnect

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.

@azooz2003-bit
azooz2003-bit force-pushed the fix-ios-tailscale-disconnect branch from 1363a66 to d704a12 Compare August 6, 2026 21:30

@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 (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift (1)

294-306: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Non-Tailscale connect while a Tailscale selection is pending leaves pendingMethod stuck.

If connectionState becomes .connected through a route other than Tailscale while pendingMethod == .tailscale (for example, Auto-Connect succeeds in the background while the pairing sheet is open), this handler dismisses the add-device sheet but never calls cancelPendingMethod(). store.method stays .automatic, but presentedMethod keeps returning .tailscale, so Settings keeps showing "Tailscale Only" selected even though the app connected through a different method. The pending state stays stuck until the user manually re-selects a method.

Cancel the pending selection on the same transition when the active route is not Tailscale.

🛡️ Proposed fix to keep pending state in sync with the sheet dismissal
         .onChange(of: store.connectionState) { _, connectionState in
             if connectionState == .connected {
                 if store.activeRoute?.kind == .tailscale {
                     connectionMethodStore?.commitPendingTailscaleMethod()
+                } else {
+                    connectionMethodStore?.cancelPendingMethod()
                 }
                 isShowingAddDeviceSheet = false
             } else {
                 clearAttachTicketAuthenticationIfNeeded()
             }

Add a test covering: stage an unauthorized Tailscale selection, then transition connectionState to .connected via a non-Tailscale route, and assert presentedMethod returns to .automatic.

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

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift`
around lines 294 - 306, Update the connected branch of the connectionState
change handler to cancel the pending method when store.activeRoute?.kind is not
.tailscale, while retaining commitPendingTailscaleMethod() for Tailscale
connections. Add a test that stages an unauthorized Tailscale selection,
connects through a non-Tailscale route, and verifies presentedMethod returns to
.automatic.

Source: Path instructions

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileConnectionMethodSection.swift`:
- Around line 55-71: The methodSelection setter must always call
store.request(method, hasAuthorizedTailscaleRoute:) regardless of whether
startPairingScanner exists. Remove the direct store.method assignment, pass the
optional scanner into the request result flow, and invoke startPairingScanner
only when the request requires pairing.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift`:
- Around line 294-306: Update the connected branch of the connectionState change
handler to cancel the pending method when store.activeRoute?.kind is not
.tailscale, while retaining commitPendingTailscaleMethod() for Tailscale
connections. Add a test that stages an unauthorized Tailscale selection,
connects through a non-Tailscale route, and verifies presentedMethod returns to
.automatic.
🪄 Autofix

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 Plus

Run ID: b06b9672-6a56-4a79-b273-f0a7414f2a32

📥 Commits

Reviewing files that changed from the base of the PR and between 2c3f2fb and 1363a66.

📒 Files selected for processing (8)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionMethod.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileConnectionMethodStore.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileConnectionMethodStoreTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileConnectionMethodSection.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • ios/cmuxUITests/cmuxUITests.swift

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@azooz2003-bit azooz2003-bit changed the title Fix iOS Tailscale selection without a local grant Open iOS Tailscale pairing on strict method selection Aug 7, 2026
…connect

# Conflicts:
#	ios/cmuxUITests/cmuxUITests.swift
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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.

3 participants