Skip to content

iOS: first-run onboarding before pairing - #5655

Merged
lawrencecchen merged 4 commits into
mainfrom
feat-ios-onboarding
Jun 10, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
feat-ios-onboarding

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

First-run onboarding for the cmux iOS companion app: a one-time, skippable, three-page explainer shown before the user pairs a Mac.

Pages: what cmux iOS is (your Mac's terminals and AI coding agents on your phone), what it needs (the cmux Mac app running, plus Tailscale on both devices, with a "Set up Tailscale" link to https://tailscale.com/download and a short note on why the private tailnet path is used instead of a cloud relay), and pair (hand off to the existing QR/manual pairing flow). All copy is short and concrete, no marketing fluff, no em dashes.

No new pairing code. On complete the root view marks the seen flag and falls through to DisconnectedWorkspaceShellView, which already auto-presents PairingView.

Placement (and why post-auth): onboarding gates after authentication, in front of the never-paired add-device state, so store.hasKnownPairedMac is authoritative when the gate reads it. This is what satisfies the never-onboarded-vs-paired-but-offline requirement: a returning, previously-paired user whose Stack session expired (and whose seen flag is still false because they updated across the flag's introduction) presents an ambiguous pairedMacHintUndetermined hint pre-auth, but post-auth the reconnect-determining branch resolves the real paired-Mac record, so the gate sees hasKnownPairedMac == true and skips onboarding instead of interrupting them. The gate is a pure function next to MobileRootAuthGate: shouldShowOnboarding(hasSeenOnboarding:hasKnownPairedMac:) = !seen && !paired.

Seen flag: MobileOnboardingStore (value type, injected UserDefaults), mirroring MobileDisplaySettings; built once at the composition root and threaded down. markSeen() is called in the button action, not a view-lifecycle callback. A forceSeen bypass is set for the UI-test mock harness and dogfood auto-pair attach URLs so neither path is wedged behind onboarding; it never writes the real install's flag.

Settings gains a "How Pairing Works" row that replays the explainer without touching the seen flag.

Localization: 11 new keys, en + ja, in ios/cmux/Resources/Localizable.xcstrings.

Tests (Swift Testing): the gate truth table (the paired-but-offline exclusion and the never-onboarded show) in CmuxMobileWorkspace, and the store round-trip / forceSeen no-op in CmuxMobileShellModel.

Verification: iOS simulator Debug and generic-device Release both compile (Release build catches DEBUG-gating leaks; none). autoreview (codex) clean; cmux Aziz policy clean. Not yet dogfooded on device (folds into a dog round).

Design doc: plans/feat-ios-onboarding/DESIGN.md in cmuxterm-hq.

🤖 Generated with Claude Code


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


Note

Medium Risk
Reorders the mobile root scene after authentication; mitigated by pure gating, paired-Mac exclusion, and test/UI bypasses, with no pairing or auth protocol changes.

Overview
Adds iOS first-run onboarding: a skippable three-page explainer (what cmux is, Tailscale/private link, how to pair) shown after sign-in and before the never-paired add-device flow, without changing pairing logic.

Persistence and gating: MobileOnboardingStore saves a one-time “seen” flag in injected UserDefaults, with forceSeen for UI-test mock data and dogfood/attach URLs so those launches skip onboarding without writing the real flag. MobileOnboardingGate.shouldShowOnboarding is !seen && !hasKnownPairedMac, evaluated only after the stored-Mac reconnect/determining branch so returning paired-but-offline users are not blocked. The root view seeds hasSeenOnboarding at init to avoid flashing the wrong screen.

UI and wiring: New OnboardingFlowView (paged flow, analytics, Tailscale download link) is injected from AppCompositionRoot → CMUXMobileRootScene → CMUXMobileAppView. Completing onboarding calls markSeen() and falls through to the existing disconnected shell / auto-presented PairingView. Settings adds How Pairing Works to replay the flow in a sheet without updating the seen flag.

Tests and strings: Unit tests cover the gate truth table and store persistence/forceSeen; new en/ja localization keys for onboarding and settings.

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


Summary by cubic

Adds a one-time, skippable onboarding flow on iOS shown after sign-in and before pairing, with a persisted seen flag and a paired-state check to avoid interrupting returning users. On completion it falls through to the existing pairing flow; no pairing logic changes.

  • New Features
    • Three-page explainer: overview, Tailscale setup (with link), and pair now; supports Skip/Next/Get started.
    • Gate via MobileOnboardingGate.shouldShowOnboarding(!seen && !paired), placed after the reconnect-determining branch; CMUXMobileRootView seeds the seen state at init to avoid flashes.
    • Persisted seen flag via MobileOnboardingStore (UserDefaults) with a forceSeen bypass for UI tests and dogfood/attach URLs; bypass never writes the real flag.
    • Settings adds “How Pairing Works” to replay the explainer without changing the flag.
    • Analytics events for viewed/skip/complete, en/ja strings, unit tests for the gate and store; wiring injected through AppCompositionRoot into CMUXMobileRootScene → CMUXMobileAppView → CMUXMobileRootView.

Written for commit 408ea6b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • iOS first-run onboarding flow: paged walkthrough with Skip/Get started, inline link, and a settings “How Pairing Works” explainer sheet
    • Persistent “seen” flag with ability to force-skip onboarding for tests/special launches; onboarding routed deterministically for eligible users
  • Tests

    • Added tests for onboarding persistence and the gating logic across pairing/onboarded states
  • Localization

    • Added onboarding and pairing strings (EN/JA)

Add a one-time, skippable onboarding flow shown post-auth, in front of the
never-paired add-device state. Three pages: what cmux iOS is (your Mac's
terminals + agents on your phone), what it needs (the cmux Mac app running and
Tailscale on both devices, with a "Set up Tailscale" link and why the private
tailnet path is used), and pair (hand off to the existing QR/manual pairing
flow). No new pairing code: on complete the root view marks the flag seen and
falls through to DisconnectedWorkspaceShellView, which already auto-presents
PairingView.

Gate (pure, in CmuxMobileWorkspace next to MobileRootAuthGate):
shouldShowOnboarding(hasSeenOnboarding:hasKnownPairedMac:) =
!seen && !paired. Placed after the stored-Mac reconnect-determining branch so
hasKnownPairedMac is authoritative, so a returning paired-but-offline user
(reachable after a failed reconnect with seen still false) is excluded.

Seen flag: MobileOnboardingStore (value type, injected UserDefaults), mirroring
MobileDisplaySettings; built once at the composition root and threaded down.
markSeen() is called in the button action, not a view-lifecycle callback.
forceSeen bypass is set for the UI-test mock harness and dogfood auto-pair
attach URLs so neither path is wedged behind onboarding; it never writes the
real install's flag.

Settings gains a "How Pairing Works" row that replays the explainer without
touching the seen flag. All copy localized (en + ja). Swift Testing unit tests
for the gate truth table and the store round-trip / forceSeen no-op.

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

vercel Bot commented Jun 8, 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 10, 2026 5:35pm
cmux-staging Building Building Preview, Comment Jun 10, 2026 5:35pm

@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements an iOS first-run onboarding flow: a persisted "seen" store with test bypass, a pure gating function, paged onboarding UI and pages, root-level routing and app wiring, settings entry point, and localized strings.

Changes

iOS First-Run Onboarding Flow

Layer / File(s) Summary
Onboarding Persistence Model
Packages/CmuxMobileShellModel/Sources/.../MobileOnboardingStore.swift, Packages/CmuxMobileShellModel/Tests/.../MobileOnboardingStoreTests.swift
MobileOnboardingStore struct persists the "seen onboarding" flag to UserDefaults and exposes it synchronously via hasSeenOnboarding. The forceSeen bypass flag allows test modes to skip persistence. Three tests validate initial state, prior-persistence reads, and bypass behavior.
Onboarding Gating Policy
Packages/CmuxMobileWorkspace/Sources/.../MobileOnboardingGate.swift, Packages/CmuxMobileWorkspace/Tests/.../MobileOnboardingGateTests.swift
MobileOnboardingGate provides a pure shouldShowOnboarding(hasSeenOnboarding:hasKnownPairedMac:) decision function that returns true only when onboarding is unseen and no paired Mac exists. Four tests cover all combinations.
Onboarding UI Components
Packages/CmuxMobileShellUI/Sources/.../OnboardingPage.swift, OnboardingPageLink.swift, OnboardingPageView.swift, OnboardingFlowView.swift
iOS-only SwiftUI views render the onboarding flow: OnboardingPage and OnboardingPageLink define three pages (what it is, how it connects with a Tailscale link, pair now); OnboardingPageView renders pages; OnboardingFlowView orchestrates a TabView-based paged flow with skip/next CTAs and analytics capture.
Settings Reference Point
Packages/CmuxMobileShellUI/Sources/.../MobileSettingsView.swift
Adds a "How Pairing Works" button in the Connection settings section that presents OnboardingFlowView via a sheet, allowing users to view the onboarding flow outside first-run.
Root View Onboarding Integration
Packages/CmuxMobileShellUI/Sources/.../CMUXMobileRootView.swift, CMUXMobileAppView.swift
CMUXMobileRootView seeds UI state from onboardingStore, gates onboarding using MobileOnboardingGate, inserts the onboarding branch before pairing UI, and completes onboarding by marking the flag and updating state. CMUXMobileAppView conditionally threads onboardingStore on iOS.
App-Level Dependency Wiring
ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift, ios/cmux/AppCompositionRoot.swift, ios/cmux/cmuxApp.swift
CMUXMobileRootScene accepts onboardingStore and passes it to CMUXMobileAppView. AppCompositionRoot creates and stores the MobileOnboardingStore with UserDefaults.standard and computes forceSeen from UITest/dogfood/attach-URL conditions. cmuxApp wires the root's store into both DEBUG and production builds.
Localization
ios/cmux/Resources/Localizable.xcstrings
Adds localized strings for onboarding navigation and content plus the settings "How Pairing Works" label (English and Japanese).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#5543: Changes to CMUXMobileRootView branching for stored-Mac restore that may interact with the new onboarding routing.

Poem

🐰 A rabbit hops through screens so bright,

first-run pages guide the curious sight,
a tiny flag remembers when they've seen,
settings show the flow again between,
hop — onboarding done, the build runs light.

🚥 Pre-merge checks | ✅ 21
✅ Passed checks (21 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main addition: a first-run onboarding flow before the pairing step on iOS.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
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 MobileOnboardingStore correctly avoids MainActor as a Sendable struct; uses nonisolated(unsafe) UserDefaults with documentation; accessed only from MainActor Views. No isolation violations.
Cmux Swift Blocking Runtime ✅ Passed PR contains no blocking/timing primitives. Uses SwiftUI @State, thread-safe UserDefaults with nonisolated(unsafe), Sendable conformance, and completion closures instead of sleep/semaphores/locks.
Cmux Expensive Synchronous Load ✅ Passed Synchronous UserDefaults.bool() in View initializer is lightweight key-value lookup, not the expensive per-record syscalls described in the rule. Mirrors existing codebase patterns.
Cmux Cache Substitution Correctness ✅ Passed Fresh read at init time seeding @State from onboardingStore, synchronized cache updates on completion; cold and stale cache properly handled per rules.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift and localization changes; check scope is TypeScript, JavaScript, shell, or non-Swift build/runtime scripts, and Swift is covered separately by swift-blocking-runtime.md.
Cmux Algorithmic Complexity ✅ Passed PR uses only fixed-size collections (3 onboarding pages). MobileOnboardingStore does O(1) UserDefaults reads/writes. No collection rescans or inefficient algorithms on hot paths.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced: no DispatchQueue, Combine, completion handlers, or fire-and-forget Tasks. New code uses modern SwiftUI and proper async/await.
Cmux Swift @Concurrent ✅ Passed No concurrent annotation violations found. All async work properly isolated with Task blocks; @Sendable closure correctly defined without needing @concurrent.
Cmux Swift File And Package Boundaries ✅ Passed New files under 400 lines with clear single responsibilities. Core logic behind package boundaries. No mixed responsibilities; proper separation of persistence, logic, and UI across packages.
Cmux Swift Logging ✅ Passed PR adds no print/debugPrint/dump/NSLog in app code, no improper Logger declarations, no ad hoc file logging, and analytics calls expose no secrets.
Cmux User-Facing Error Privacy ✅ Passed No privacy violations found. Onboarding adds only informational UI with no exposed credentials, internal details, or inappropriate vendor references. Tailscale mention allowed as user-configured.
Cmux Full Internationalization ✅ Passed All user-facing Swift text uses L10n.string() with proper localization keys; all 11 new keys have complete en+ja translations in Localizable.xcstrings catalog.
Cmux Swiftui State Layout ✅ Passed PR complies: MobileOnboardingStore is a Sendable struct, OnboardingFlowView uses @State for local UI, pages passed as immutable snapshots, state mutations only in button action closures.
Cmux Architecture Rethink ✅ Passed PR passes swift-architectural-rethink.md check: no timing/blocking repairs, locks, or polling; single state ownership hierarchy; pure gate function with clear invariants; synchronized completion path.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds only SwiftUI Views and sheets, no NSWindow/NSPanel/NSWindowController or Window/WindowGroup declarations. All onboarding UI is sheet presentation within existing iOS app structure.
Cmux Source Artifacts ✅ Passed All 15 changed files are hand-written Swift source, tests, or localization configs—no artifacts like logs, caches, build output, or scratch directories detected.
Description check ✅ Passed The PR description comprehensively covers the onboarding feature with technical details, design decisions, and verification steps, but lacks formal Testing and Demo Video sections from the template.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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-onboarding

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.

@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a one-time, skippable, three-page first-run onboarding flow to the iOS companion app. It gates after authentication and before the never-paired add-device state, using a pure shouldShowOnboarding(!seen && !paired) function to skip the flow for returning paired-but-offline users.

  • MobileOnboardingStore: a UserDefaults-backed Sendable struct with a forceSeen bypass (for UI tests and dogfood auto-pair URLs) that never writes to the real install's flag; seeded at init into @State to avoid flash-of-wrong-screen on first render.
  • MobileOnboardingGate: a namespace-only struct whose single static function is the tested truth table; wired through AppCompositionRoot → CMUXMobileRootScene → CMUXMobileAppView → CMUXMobileRootView.
  • OnboardingFlowView: three-page TabView with Skip/Next/Get Started, interactiveDismissDisabled() for the first-run full-screen path, and a Settings "How Pairing Works" sheet re-entry that calls onComplete without touching the seen flag; 11 new en + ja localization keys cover all user-facing strings.

Confidence Score: 5/5

Safe to merge. All changes are additive iOS-only UI; no pairing, auth, or persistence paths are modified.

The gate logic is a pure two-input boolean covered by an exhaustive truth-table test. The seen flag is seeded from UserDefaults at init time (avoiding flash-of-wrong-screen), written only in the button action, and never touched by the forceSeen bypass path. Returning paired-but-offline users are correctly excluded by the paired check. Localization covers the only two locales in the catalog. No blocking primitives, actor isolation mistakes, or production logging issues were found.

No files require special attention.

Important Files Changed

Filename Overview
Packages/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileOnboardingStore.swift New value-type store with injected UserDefaults, forceSeen bypass, and nonisolated(unsafe) let — all correct; no actor isolation or blocking issues.
Packages/CmuxMobileWorkspace/Sources/CmuxMobileWorkspace/MobileOnboardingGate.swift Pure static gate function with correct truth table (!seen && !paired); well-tested.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift Adds onboarding branch after reconnect-determining branch; @State seeded from store at init to avoid flash; markSeen() called in button action, not lifecycle. Clean.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingFlowView.swift Three-page TabView pager; interactiveDismissDisabled() applied unconditionally (addressed in prior review threads); analytics events fire on both first-run and Settings paths (also addressed in prior threads).
ios/cmux/AppCompositionRoot.swift Builds onboardingStore once with forceSeen derived from UITestConfig flags; bypass logic is clear and correctly never writes the real flag.
ios/cmux/Resources/Localizable.xcstrings 11 new keys, all with en and ja translations matching the existing catalog's two supported locales.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingPage.swift Pure data model; allPages is a static var (correct for runtime localization lookups); no issues.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift Adds How Pairing Works button and sheet; onComplete closure only dismisses without touching the seen flag — intentional and correct.
ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift Threads onboardingStore through; canImport(UIKit) && DEBUG replaced with os(iOS) + nested #if DEBUG — semantically correct for this target.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App Launch] --> B{isAuthenticated?}
    B -- No --> C[AuthView]
    B -- Yes --> D{pairedMacHint?}
    D -- Undetermined --> E[MobilePairedMacDeterminingView\nresolves hasKnownPairedMac]
    E --> F{shouldShowOnboarding?\n!seen AND !paired}
    D -- Known --> F
    F -- true --> G[OnboardingFlowView\nSkip / Next / Get Started]
    G -- markSeen + complete --> H[DisconnectedWorkspaceShellView\nauto-presents PairingView]
    F -- false --> I{connectionState == .connected?}
    I -- No --> H
    I -- Yes --> J[Connected workspace UI]
    K[Settings] --> L[How Pairing Works sheet]
    L --> G2[OnboardingFlowView\nonComplete = dismiss only]
    G2 -- dismiss --> K
Loading

Reviews (4): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

footer
}
.background(PlatformPalette.systemBackground.ignoresSafeArea())
.interactiveDismissDisabled()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 interactiveDismissDisabled blocks swipe-to-dismiss in the Settings re-entry sheet

.interactiveDismissDisabled() is unconditional, so it applies to both the first-run full-screen presentation and the Settings "How Pairing Works" sheet in MobileSettingsView. A user who opens the sheet from Settings cannot swipe it away — they must tap Skip or march through all three pages, which is an unusual constraint for an informational replay. For the first-run case blocking dismiss makes sense; for Settings re-entry it is a UX regression over standard iOS sheet behavior. Consider passing a flag or checking the presentation context (e.g. an isDismissible parameter) so the first-run path keeps the guard while the Settings path allows a free swipe-down.

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

Comment on lines +49 to +53
.onAppear {
analytics.capture("ios_onboarding_viewed", ["page": .int(0)])
}
.onChange(of: pageIndex) { _, newValue in
analytics.capture("ios_onboarding_viewed", ["page": .int(newValue)])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Analytics events are context-blind between first-run and Settings re-entry

ios_onboarding_viewed, ios_onboarding_skipped, and ios_onboarding_completed fire identically whether the flow is the first-run gate or the "How Pairing Works" Settings re-entry. Any funnel analysis that tracks page views or completion rates will double-count against the first-run cohort for every Settings tap. Consider passing a source string (e.g. "first_run" vs "settings") and including it as an event property.

# Conflicts:
#	ios/cmux/AppCompositionRoot.swift
#	ios/cmux/cmuxApp.swift
#	ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5f7ccf3e9

ℹ️ 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 +208 to +210
return MobileOnboardingGate.shouldShowOnboarding(
hasSeenOnboarding: hasSeenOnboarding,
hasKnownPairedMac: store.hasKnownPairedMac

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't gate onboarding solely on the persisted paired hint

When a returning install has an active paired Mac record but none of its saved routes are supported by the current runtime, reconnectActiveMacIfAvailable finds the Mac and then deliberately writes hasKnownPairedMac = false before falling through (MobileShellComposite.swift:932-940). Because the new gate here treats that false hint as a genuine first run, those already-paired users see first-run onboarding instead of the disconnected/add-device recovery path, which breaks the intended paired-but-offline exclusion for this route-migration/unsupported-route case.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift (1)

102-110: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move "How Pairing Works" button inside the Connection section.

The button currently sits outside the Section (but inside the hasConnectionSection conditional), so it renders as a standalone row in the Form. All other buttons in this file are placed inside their respective sections (Account, Terminal, Notifications). Move the button inside the Connection section's closing brace (after line 101, before the current } on line 101) to group it visually with the other connection-related rows.

📐 Proposed fix: move button inside the Section
                     }
                     .accessibilityIdentifier("MobileSettingsRescanQR")
                 }
+                Button {
+                    showingOnboarding = true
+                } label: {
+                    Label(
+                        L10n.string("mobile.settings.howPairingWorks", defaultValue: "How Pairing Works"),
+                        systemImage: "questionmark.circle"
+                    )
+                }
+                .accessibilityIdentifier("MobileSettingsHowPairingWorks")
             }
-            Button {
-                showingOnboarding = true
-            } label: {
-                Label(
-                    L10n.string("mobile.settings.howPairingWorks", defaultValue: "How Pairing Works"),
-                    systemImage: "questionmark.circle"
-                )
-            }
-            .accessibilityIdentifier("MobileSettingsHowPairingWorks")
         }
🤖 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/MobileSettingsView.swift`
around lines 102 - 110, The "How Pairing Works" Button in MobileSettingsView is
currently outside the Connection Section (but still inside the
hasConnectionSection conditional), causing it to render as a standalone Form
row; move the Button (the block that sets showingOnboarding = true and uses
Label with L10n.string("mobile.settings.howPairingWorks")) so it is inside the
Connection Section's curly braces (i.e., place it before the Section's closing
brace within the hasConnectionSection branch) so it renders as a row grouped
with other connection-related items.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift`:
- Around line 102-110: The "How Pairing Works" Button in MobileSettingsView is
currently outside the Connection Section (but still inside the
hasConnectionSection conditional), causing it to render as a standalone Form
row; move the Button (the block that sets showingOnboarding = true and uses
Label with L10n.string("mobile.settings.howPairingWorks")) so it is inside the
Connection Section's curly braces (i.e., place it before the Section's closing
brace within the hasConnectionSection branch) so it renders as a row grouped
with other connection-related items.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0297cb73-aeee-4d68-9236-591aa94c8829

📥 Commits

Reviewing files that changed from the base of the PR and between d6cfcf3 and b5f7ccf.

📒 Files selected for processing (3)
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • ios/cmux/Resources/Localizable.xcstrings

# Conflicts:
#	ios/cmux/Resources/Localizable.xcstrings

@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 408ea6b. Configure here.

// sees the one-time explainer before the add-device flow; a returning
// paired-but-offline user (who can reach here after a failed
// reconnect) is excluded by the gate and falls through to pairing.
onboardingFlow

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Onboarding shows while connected

Medium Severity

The onboarding branch runs before the connected workspace branch and is not gated on connectionState. If the shell reaches .connected while hasKnownPairedMac is still false, first-run onboarding replaces the live workspace until the user skips or finishes it.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 408ea6b. Configure here.

This branch was successfully deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant