Skip to content

Notify iOS while using the app - #6654

Closed
austinywang wants to merge 30 commits into
mainfrom
issue-6636-ios-notify-while-using-app
Closed

austinywang wants to merge 30 commits into
mainfrom
issue-6636-ios-notify-while-using-app

Conversation

@austinywang

@austinywang austinywang commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add iOS notification settings for enable/disable, Always vs Only When Away from Mac, and Hide Notification Content.
  • Sync explicit iOS notification setting changes to the Mac over new notification.settings.get/set mobile RPCs backed by the same Mac PhonePushSettings defaults that power the Mac Notifications page.
  • When notifications are enabled from iOS, write the forwarding mode as Always so Mac presence no longer suppresses phone notifications for the phone-active case. Existing Mac-only opt-ins with no stored mode keep their legacy away-only fallback.

Behavior

When the user enables agent notifications on iOS, the phone persists the local APNs opt-in and syncs the forwarding preference to the paired Mac. The iOS default mode is Always, so completed-workspace notifications are forwarded to the phone even if the Mac was awake or recently used. Users can still choose Only When Away from Mac from the iOS settings, and the settings view explains that this mode can be suppressed by Mac presence.

The Mac Notifications page and iOS settings share the Mac forwarding mode and hide-content preference. The iOS local APNs opt-in is stored separately from the Mac forwarding-enabled mirror: passive iOS reconciliation is read-only toward the Mac, so a later Mac-side forwarding opt-out or privacy change is not overwritten by an old phone opt-in, and it does not silently unregister the phone. Explicit iOS enable/mode/privacy actions remain the write path, and failed explicit writes are marked pending so reconnect can retry. Turning notifications off on one phone remains device-scoped and does not disable Mac forwarding or another device.

Foreground presentation remains intentionally narrow: iOS suppresses a banner only for the exact workspace/terminal currently on screen, and still presents foreground banners for other completed workspaces/terminals. Background delivery continues through APNs; real device/TestFlight validation is required for end-to-end phone-push delivery.

Tests

  • Added Mac forwarding/presence regression tests in a first failing-test commit.
  • Added iOS notification preference persistence tests.
  • Added iOS shell RPC sync tests for notification.settings.get/set, capability gating, passive read-only reconcile, Mac-side opt-out handling, and pending explicit-sync retry.
  • Added foreground-presentation coverage for exact-terminal suppression only.
  • Ran non-build checks: git diff --check, jq empty ios/cmux/Resources/Localizable.xcstrings, and localization key audit for English/Japanese notification strings.

Closes #6636

@vercel

vercel Bot commented Jun 23, 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 Jul 4, 2026 11:50am
cmux-staging Building Building Preview, Comment Jul 4, 2026 11:50am

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end iOS notification settings synchronization with the Mac. New MobileNotificationForwardingMode and MobileNotificationPreferences model types are introduced. Mac-side RPC handlers for notification.settings.get/set read and write UserDefaults. iOS gains a response model, RPC auth bypass, MobileShellComposite sync APIs, updated MobilePushCoordinator preference management, and an expanded MobileSettingsView UI with localization. PhoneForwardingMode.defaultMode is changed from .onlyWhenAway to .always.

Changes

iOS Notification Settings Sync

Layer / File(s) Summary
Shared notification preference models and forwarding mode defaults
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileNotificationForwardingMode.swift, Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileNotificationPreferences.swift, Sources/Cloud/PhoneForwardingMode.swift, Sources/Cloud/PhonePushClient.swift
Adds MobileNotificationForwardingMode String-backed enum with .always/.onlyWhenAway cases and defaultMode = .always. Adds MobileNotificationPreferences struct with isEnabled, forwardingMode, hidesContent stored properties, UserDefaults key constants, two initializers (memberwise and from-defaults with fallback to .always), and persist(to:) method. Changes PhoneForwardingMode.defaultMode from .onlyWhenAway to .always and updates documentation.
Mac RPC handlers for notification settings
Sources/Mobile/MobileHostService+Capabilities.swift, Sources/Mobile/MobileHostService.swift, Sources/TerminalController+MobileNotificationSync.swift, Sources/TerminalController.swift
Advertises "notification.settings.v1" capability. Exempts notification.settings.get/set from ticket workspace scoping. Implements v2MobileNotificationSettingsGet (returns current settings from UserDefaults) and v2MobileNotificationSettingsSet (parses/validates optional enabled/mode/hide_content, persists valid updates, returns updated payload). Routes all notification.* methods through unified dispatch. Helper v2MobileNotificationSettingsPayload constructs response from UserDefaults.
iOS RPC client response model and auth bypass
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileNotificationSettingsResponse.swift, Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
Introduces MobileNotificationSettingsResponse decodable model with isEnabled, forwardingMode, hidesContent fields, custom CodingKeys, and missing-field defaults. Adds static decode(_:) helper for JSON decoding. Marks notification.settings.get/set as not needing Stack Auth fallback.
MobileShellComposite RPC sync and fetch APIs
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationSettingsSync.swift, Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift, Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/NotificationSettingsCapabilityTests.swift
Adds syncNotificationPreferencesToMac(_:) and fetchNotificationPreferencesFromMac() async methods that send RPC calls with capability probing and error handling. Introduces notificationSettingsLog logger and supportsNotificationSettings computed property. Broadens supportedHostCapabilities setter visibility to internal. Includes capability-gating test verifying RPCs are not issued without host support.
MobilePushCoordinator preference management
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift
Removes enabledKey constant, replaces direct UserDefaults bool read with notificationPreferences accessor. Changes isEnabled to compute from preferences. Updates enable() signature to accept optional forwardingMode/hidesContent. Updates enable/disable to persist and sync preferences to Mac. Adds setForwardingMode and setHidesContent async APIs. Updates reconcileNotificationPreferencesWithMac to branch on stored preference presence. Adds helper methods detecting stored preference fields and syncNotificationPreferencesToMac helper.
MobileSettingsView UI and localization
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift, ios/cmux/Resources/Localizable.xcstrings
Adds @State vars for notification forwarding mode, hide-content, and syncing flag. Replaces single inline toggle with status row, toggle button, forwarding-mode picker, and hide-content toggle (disabled/enabled/unsupported conditionally). Updates onAppear to load full preferences and trigger async Mac reconciliation. Adds helpers for loading/refreshing/updating preferences. Introduces all notification settings localization strings (disabled/unsupported hints, content toggle, mode options, status labels).
Model tests, Mac RPC tests, iOS sync tests, and reconciliation tests
Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileNotificationPreferencesTests.swift, cmuxTests/PhonePushPresenceGateTests.swift, ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift
Adds MobileNotificationPreferencesTests covering defaults, invalid mode fallback, and persistence round-trip. Updates PhoneForwardingMode.defaultMode expectation to .always. Adds Mac capability, ticket auth, and notification.settings.set RPC integration tests with invalid-request validation. Adds two @MainActor async iOS sync tests for set/get RPC calls and test infrastructure for recording notification preference params. Adds notification preference reconciliation (non-adoption of Mac defaults, legacy opt-in persistence, opt-out behavior) and foreground-presentation suppression tests.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant MobileSettingsView
  participant MobilePushCoordinator
  participant MobileShellComposite
  participant MobileCoreRPCClient
  participant TerminalController as TerminalController (Mac)

  rect rgba(100, 149, 237, 0.5)
    Note over MobileSettingsView,TerminalController: Enable notifications + initial sync
    User->>MobileSettingsView: toggle notifications on
    MobileSettingsView->>MobilePushCoordinator: enable()
    MobilePushCoordinator->>MobilePushCoordinator: persist MobileNotificationPreferences to UserDefaults
    MobilePushCoordinator->>MobileShellComposite: syncNotificationPreferencesToMac(preferences)
    MobileShellComposite->>MobileCoreRPCClient: notification.settings.set {enabled, mode, hide_content, client_id}
    MobileCoreRPCClient->>TerminalController: notification.settings.set RPC
    TerminalController->>TerminalController: write enabled/mode/hide_content to UserDefaults
    TerminalController-->>MobileCoreRPCClient: {enabled, mode, hide_content} response
    MobileCoreRPCClient-->>MobileShellComposite: MobileNotificationSettingsResponse
    MobileShellComposite-->>MobilePushCoordinator: MobileNotificationPreferences (Mac-echoed)
    MobilePushCoordinator->>MobilePushCoordinator: persist Mac-returned preferences to UserDefaults
    MobilePushCoordinator-->>MobileSettingsView: updated MobileNotificationPreferences
    MobileSettingsView->>MobileSettingsView: loadNotificationPreferences(prefs)
  end

  rect rgba(144, 238, 144, 0.5)
    Note over MobileSettingsView,TerminalController: Change forwarding mode
    User->>MobileSettingsView: pick forwarding mode
    MobileSettingsView->>MobilePushCoordinator: setForwardingMode(mode)
    MobilePushCoordinator->>MobileShellComposite: syncNotificationPreferencesToMac(preferences)
    MobileShellComposite->>MobileCoreRPCClient: notification.settings.set {mode, ...}
    MobileCoreRPCClient->>TerminalController: notification.settings.set RPC
    TerminalController-->>MobileCoreRPCClient: updated payload
    MobileCoreRPCClient-->>MobileShellComposite: MobileNotificationSettingsResponse
    MobileShellComposite-->>MobilePushCoordinator: MobileNotificationPreferences
    MobilePushCoordinator-->>MobileSettingsView: updated preferences
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#5519: Modifies MobileSettingsView's notifications toggle state handling, which this PR extends to include forwarding mode and hide-content sync.
  • manaflow-ai/cmux#6022: Updates MobileCoreRPCClient.requestNeedsStackAuthFallback to adjust auth-fallback routing for specific RPC methods, which this PR also modifies for notification settings methods.

Suggested reviewers

  • lawrencecchen

Poem

🐇 Hop hop, a new setting's here,
Notifications flow from phone to Mac, so clear!
.always the default, no hiding away,
Mode pickers and toggles light up the display.
🔔 The rabbit rejoices — alerts every day! 🎉


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error PR introduces split UI lifecycle ownership with concurrent reconciliation entry points: reconcileTask from bind() runs in background while MobileSettingsView.onAppear independently triggers `... Serialize notification settings reconciliation: either prevent concurrent calls via a guard on refreshNotificationPreferencesFromMac() using an existing flag (e.g., extend notificationSettingsSyncGeneration as coordinator-level lock)...
Cmux Source Artifacts ❌ Error The commit adds ~26 files in .claude/ and .agents/ directories (commands, skills, and lock files) that are local tool artifacts, violating source-control-artifacts.md by adding "local tool outp... Remove .claude/commands/, .claude/skills/, .claude/scheduled_tasks.lock, and .agents/skills files from the commit, or add .claude/ and .agents/ to .gitignore.
Docstring Coverage ⚠️ Warning Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Notify iOS while using the app' clearly and concisely summarizes the main change: enabling phone notifications to be delivered while actively using the iOS application.
Linked Issues check ✅ Passed The PR directly addresses #6636 by implementing all desired outcomes: defaulting forwarding mode to 'Always' to enable notifications during active iOS usage, adding discoverable iOS settings UI for enable/disable/mode/content-hiding, and synchronizing iOS-Mac settings via new RPC endpoints.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue objectives: iOS notification settings UI, Mac-iOS sync infrastructure, notification preferences persistence, and foreground presentation refinement; no unrelated modifications detected.
Cmux Swift Actor Isolation ✅ Passed All new and modified types correctly implement actor isolation per Swift 6 rules: value types (MobileNotificationPreferences, MobileNotificationSettingsResponse, MobileNotificationForwardingMode) m...
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing-based synchronization patterns (semaphores, locks, sleeps, polling) introduced in new notification settings code. All RPC handlers, iOS UI, and sync logic use proper async/await...
Cmux Expensive Synchronous Load ✅ Passed PR adds no expensive synchronous loads (RestorableAgentSessionIndex, agent stores, transcripts, large JSON parsing, directory scans, or file I/O) to main thread or interactive paths. JSON parsing o...
Cmux Cache Substitution Correctness ✅ Passed Fresh Mac RPC preferences are always fetched when reconciling; when persisted (local opt-in case), fresh values are merged and stored; when not persisted (no opt-in case), returned values are trans...
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift code (18 files) and localization strings; the runtime-no-hacky-sleeps rule explicitly excludes Swift code, covering only TypeScript/JavaScript/shell/build scripts.
Cmux Algorithmic Complexity ✅ Passed PR introduces notification settings sync with no algorithmic complexity violations: single-pass O(n) scans on infrequent paths (dismiss RPC, tap), capped loops (maxDismissIDs=256), Set lookups O(1)...
Cmux Swift Concurrency ✅ Passed The diff uses only modern Swift concurrency patterns: @Observable (not Combine), proper async/await throughout, stored Task with lifecycle management in MobilePushCoordinator (cancelled in bind and...
Cmux Swift @Concurrent ✅ Passed PR contains no invalid @concurrent annotations on synchronous, MainActor-isolated, or state-accessing functions. Network-heavy async methods follow existing codebase pattern of MainActor-bound op...
Cmux Swift File And Package Boundaries ✅ Passed All new production files are under 400-line threshold; modified files (442, 499 lines) remain below thresholds and maintain clear cohesive responsibilities. Feature logic appropriately extracted to...
Cmux Swiftpm Lockfiles ✅ Passed PR includes all package-local Package.resolved files with corresponding Package.swift changes, root Xcode Package.resolved, and cmux-owned package .gitignore files do not ignore Package.resolved.
Cmux Swift Logging ✅ Passed All logging in the PR complies with swift-logging.md: uses Apple's unified logging system, declares Logger as nonisolated private let in MainActor context, redacts errors with privacy: .private, an...
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages and strings comply with privacy rules. Server-side errors (enabled/mode/hide_content validation) are generic and don't expose sensitive data. iOS logging uses privacy...
Cmux Full Internationalization ✅ Passed All user-facing notification settings text uses L10n.string() with matching Localizable.xcstrings entries translated for both English and Japanese locales; Mac-side changes are implementation-only...
Cmux Swiftui State Layout ✅ Passed PR complies with swiftui-state-layout.md: uses @Observable for MobilePushCoordinator, no @ObservableObject/@published, no render-time state mutations (Tasks only in event handlers/private functions...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup additions; MobileSettingsView is an existing View struct update with notification UI components.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No test/debug seams violating the rule were added. Existing #if DEBUG blocks gate demo screens (acceptable). Visibility change to supportedHostCapabilities is justified for production RPC handling.
Description check ✅ Passed The PR description comprehensively addresses all required template sections with detailed information about changes, behavior, and testing.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6636-ios-notify-while-using-app

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.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds iOS notification settings (enable/disable, Always vs Only When Away, Hide Content) synced to the Mac over new notification.settings.get/set mobile RPCs backed by the existing PhonePushSettings UserDefaults keys. The generation-based concurrency model (notificationSettingsSyncGeneration) serializes concurrent reconcile and explicit-write paths, and the pending-sync mechanism retries failed Mac writes on reconnect.

  • Mac-side: Adds notification.settings.v1 capability, two new RPC handlers (v2MobileNotificationSettingsGet/Set), and routes all notification.* RPCs through a single dispatch; the debugTicketAuthorizationError test seam is removed and replaced by directly calling the now-internal ticketAuthorizationError, which is the pattern recommended by the no-test-seam rule.
  • iOS-side: New MobileNotificationPreferences model, MobileNotificationSettingsSection SwiftUI view, MobilePushCoordinator+NotificationSettingsPendingSync extension for durable retry state, and MobileShellComposite+NotificationSettingsSync for Mac RPC round-trips; all new user-facing strings are present in both English and Japanese in Localizable.xcstrings.
  • Behavioral contract: iOS disable is device-scoped (does not alter Mac forwarding or another device's APNs); passive reconcile on connect/bind is read-only toward the Mac; only explicit iOS enable/mode/privacy writes push to the Mac.

Confidence Score: 5/5

Safe to merge; the generation-based concurrency model and pending-sync retry mechanism are sound, @mainactor isolation is correct throughout, and all new user-facing strings are localized in both English and Japanese.

The core write path (enable → write defaults → Mac RPC set → echo back → persist) is correctly guarded by the generation counter, always mutated synchronously before the first await on the @MainActor-isolated coordinator. The pending-sync mechanism survives disconnects, reconnects to different Macs, and Mac-side opt-outs. The two issues found are edge-case gaps: reconcileTask?.cancel() not stopping an in-flight reconcile (the generation mechanism is the real guard), and a pending-sync flag that can linger when the phone is locally disabled via a public API path that bypasses disable(). Both are bounded to settings sync state and self-correct on the next explicit user action.

MobilePushCoordinator.swift — the reconcileTask cancellation semantics and the pending-sync-not-cleared-when-disabled branch.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift Significantly expanded with enable/disable Mac sync, generation-based concurrency control, pending-sync retry, and new reconcile/setForwardingMode/setHidesContent paths; defaults widened from private to internal for extension access; reconcileTask cancellation semantics are weaker than they appear (only guards task start, not mid-flight execution, which is handled by the generation mechanism).
Sources/Mobile/MobileHostService.swift Adds notification.settings.get/set to the auth bypass list, removes the debugTicketAuthorizationError test seam, widens ticketAuthorizationError to internal — all correct per the no-test-seam and nonisolated-static rules.
Sources/TerminalController+MobileNotificationSync.swift Adds v2MobileNotificationSettingsGet/Set handlers and a unified v2MobileNotificationDispatch router; reads/writes UserDefaults.standard synchronously on @mainactor, which is safe; echoes the written state back to the caller.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationSettingsSync.swift New file providing syncNotificationPreferencesToMac and fetchNotificationPreferencesFromMac, with remoteClient identity checks before and after each RPC await; capability probe uses a 750ms timeout; file-scope Logger is correctly nonisolated.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileNotificationSettingsSection.swift 231-line SwiftUI section with enable/disable/mode/hide-content controls; uses notificationSettingsSyncing and notificationSettingsRefreshing flags to guard concurrent writes; the two flags are not always mutually exclusive, which the generation mechanism in MobilePushCoordinator correctly handles.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator+NotificationSettingsPendingSync.swift Provides durable pending-sync state via UserDefaults; mac device ID matching correctly handles nil (matches any Mac) and different-Mac cases; clean separation of pending sync metadata from the preference keys.
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileNotificationPreferences.swift Clean value type with isEnabled/isForwardingEnabled separation; forwardingModeForPhoneOptIn correctly distinguishes already-enabled Mac (preserve mode) from disabled Mac (default to .always).
ios/cmux/Resources/Localizable.xcstrings 14 new mobile.notifications.* keys and several auth error renames/additions, all with en+ja translations; ordering changes are alphabetical resorting with no content regressions.

Reviews (13): Last reviewed commit: "Scope pending iOS notification settings ..." | Re-trigger Greptile

Comment on lines +249 to +265
.onChange(of: notificationMode) { mode in
guard !notificationSettingsSyncing else { return }
updateNotificationMode(mode)
}
.accessibilityIdentifier("MobileSettingsNotificationsMode")
.disabled(notificationSettingsSyncing)

Toggle(isOn: $hideNotificationContent) {
Text(L10n.string(
"mobile.notifications.hideContent",
defaultValue: "Hide Notification Content"
))
}
.onChange(of: hideNotificationContent) { hidesContent in
guard !notificationSettingsSyncing else { return }
updateNotificationHideContent(hidesContent)
}

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 Both .onChange(of:) callbacks use the single-argument closure form that was deprecated in iOS 17. Since MobilePushCoordinator is @Observable (iOS 17+), the minimum deployment target makes these deprecated API calls, and Xcode will emit warnings. The fix is the two-argument { _, new in } form.

Suggested change
.onChange(of: notificationMode) { mode in
guard !notificationSettingsSyncing else { return }
updateNotificationMode(mode)
}
.accessibilityIdentifier("MobileSettingsNotificationsMode")
.disabled(notificationSettingsSyncing)
Toggle(isOn: $hideNotificationContent) {
Text(L10n.string(
"mobile.notifications.hideContent",
defaultValue: "Hide Notification Content"
))
}
.onChange(of: hideNotificationContent) { hidesContent in
guard !notificationSettingsSyncing else { return }
updateNotificationHideContent(hidesContent)
}
.onChange(of: notificationMode) { _, mode in
guard !notificationSettingsSyncing else { return }
updateNotificationMode(mode)
}
.accessibilityIdentifier("MobileSettingsNotificationsMode")
.disabled(notificationSettingsSyncing)
Toggle(isOn: $hideNotificationContent) {
Text(L10n.string(
"mobile.notifications.hideContent",
defaultValue: "Hide Notification Content"
))
}
.onChange(of: hideNotificationContent) { _, hidesContent in
guard !notificationSettingsSyncing else { return }
updateNotificationHideContent(hidesContent)
}

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!

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

🤖 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 `@cmuxTests/PhonePushPresenceGateTests.swift`:
- Around line 325-335: The PhonePushPresenceGateTests struct modifies shared
global state (UserDefaults.standard and TerminalController.shared) without
serialization, which can cause race conditions when tests run in parallel. Add
the .serialized trait to the PhonePushPresenceGateTests struct declaration to
ensure tests in this suite execute serially and prevent interleaved mutations of
shared state.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+NotificationSettingsSync.swift:
- Line 43: The notificationSettingsLog.error calls at lines 43 and 67 are
logging error descriptions with `.public` privacy redaction, which can expose
sensitive authentication or runtime details in production logs. Change the
privacy parameter from `.public` to `.private` for the String(describing: error)
portion in both notificationSettingsLog.error calls to properly redact sensitive
error information according to coding guidelines.
- Around line 6-9: The file-scoped logger instance notificationSettingsLog needs
to be declared as nonisolated to avoid unnecessary MainActor coupling in Swift 6
isolation mode. Add the nonisolated keyword before the private let declaration
of notificationSettingsLog so it reads nonisolated private let
notificationSettingsLog instead of just private let notificationSettingsLog.
This explicitly marks the logger as not being actor-isolated, allowing it to be
safely accessed from both actor and non-actor contexts.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift`:
- Around line 214-217: The issue is that the
reconcileNotificationPreferencesWithMac() method uses a single
hasStoredNotificationPreference boolean flag to determine if local preferences
should be synced to Mac, but this doesn't account for partial preference data
where some fields may be missing or only have default values. On upgrade paths
with incomplete stored data, missing mode/hide fields get defaulted and pushed
to Mac, overwriting its authoritative settings. Instead of relying solely on the
hasStoredNotificationPreference flag, implement explicit validation to check
that all required notification preference fields (enabled, mode, hide_content,
forwarding behavior) are fully present and valid before syncing to Mac. If any
required fields are missing or only have defaults, treat the local cache as
stale and skip the sync, allowing the Mac settings to remain authoritative. This
aligns with the cache-substitution-correctness requirements for explicit
cold/stale cache handling.
- Around line 109-114: The Task created in the bind method for
reconcileNotificationPreferencesWithMac is a fire-and-forget task that is not
tracked or managed, which can cause overlapping task execution if bind is called
multiple times before the previous task completes. Store the reconciliation task
as a property of the MobilePushCoordinator class, and in the bind method, cancel
any existing stored task before starting the new reconciliation task. This
ensures that only one reconciliation task runs at a time and prevents stale
preferences from being persisted due to out-of-order task completion.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift`:
- Around line 405-447: The notificationSettingsSyncing flag is being set to true
inside the Task block rather than before it, which creates a race condition
where rapid events can queue multiple async tasks before the gate is
established. Move the notificationSettingsSyncing = true statement outside and
before each Task block initialization in the four methods:
refreshNotificationPreferencesFromMac, toggleNotifications,
updateNotificationMode, and updateNotificationHideContent. This ensures the
synchronization flag is set immediately when the method is called, preventing
overlapping writes and out-of-order preference application.

In `@Sources/TerminalController`+MobileNotificationSync.swift:
- Around line 20-43: The code is persisting values to UserDefaults as each
parameter is validated individually, which means if an early parameter is valid
but a later one is invalid, the earlier value is already persisted when the
error is returned. Refactor the logic to first validate all parameters (enabled
via v2Bool, mode via v2OptionalTrimmedRawString and PhoneForwardingMode
initialization, and hide_content via v2Bool) and collect the valid values, then
only after all validations pass, persist all values to UserDefaults using the
keys PhonePushSettings.forwardEnabledKey, PhonePushSettings.forwardModeKey, and
PhonePushSettings.hideContentKey. This ensures atomicity—either all changes are
persisted or none are.
🪄 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: 87b5d38d-36c4-4015-82c9-73acd912b936

📥 Commits

Reviewing files that changed from the base of the PR and between f2dd188 and cbb26c9.

📒 Files selected for processing (17)
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileNotificationSettingsResponse.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationSettingsSync.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileNotificationForwardingMode.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileNotificationPreferences.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileNotificationPreferencesTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Sources/Cloud/PhoneForwardingMode.swift
  • Sources/Cloud/PhonePushClient.swift
  • Sources/Mobile/MobileHostService+Capabilities.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/TerminalController+MobileNotificationSync.swift
  • Sources/TerminalController.swift
  • cmuxTests/PhonePushPresenceGateTests.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment thread cmuxTests/PhonePushPresenceGateTests.swift
Comment thread Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift Outdated
Comment thread Sources/TerminalController+MobileNotificationSync.swift Outdated

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

♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift (1)

230-233: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist reconciled Mac values in the cold-cache branch.

This branch returns Mac-derived forwardingMode/hidesContent but does not persist them, so subsequent reads from notificationPreferences can still serve stale defaults after reconciliation. Persist the reconciled non-opt-in fields before returning to make cold-start reconciliation durable.

As per path instructions, cache substitution paths must explicitly handle cold/stale cache behavior rather than leaving authoritative reads ephemeral.

🤖 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/MobilePushCoordinator.swift`
around lines 230 - 233, The localPreferences variable is modified with
Mac-derived values for forwardingMode and hidesContent but is not persisted
before being returned. This causes subsequent reads from notificationPreferences
to serve stale defaults. After assigning the reconciled Mac values to
localPreferences (both forwardingMode and hidesContent), persist these changes
to the underlying cache or storage mechanism before returning localPreferences
so that cold-start reconciliation is durable and future reads reflect the
reconciled state.

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.

Duplicate comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift`:
- Around line 230-233: The localPreferences variable is modified with
Mac-derived values for forwardingMode and hidesContent but is not persisted
before being returned. This causes subsequent reads from notificationPreferences
to serve stale defaults. After assigning the reconciled Mac values to
localPreferences (both forwardingMode and hidesContent), persist these changes
to the underlying cache or storage mechanism before returning localPreferences
so that cold-start reconciliation is durable and future reads reflect the
reconciled state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 47782889-0144-4ef2-a9e4-b923a5f4c1da

📥 Commits

Reviewing files that changed from the base of the PR and between cbb26c9 and a58dd2d.

📒 Files selected for processing (7)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/TerminalController+MobileNotificationSync.swift
  • Sources/TerminalController.swift
  • cmuxTests/PhonePushPresenceGateTests.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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/MobilePushCoordinator.swift (1)

240-246: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist the Mac settings in the cold-cache reconciliation path.

When no local opt-in key exists, this fetches Mac settings but leaves isEnabled as the local default and never writes the fetched values to defaults; the bind(store:) caller ignores the return value, so first-run devices can drop the authoritative Mac settings and later overwrite them from local defaults.

💡 Suggested fix
         guard let macPreferences = await store?.fetchNotificationPreferencesFromMac() else {
             return notificationPreferences
         }
-        var localPreferences = notificationPreferences
-        localPreferences.forwardingMode = macPreferences.forwardingMode
-        localPreferences.hidesContent = macPreferences.hidesContent
-        return localPreferences
+        macPreferences.persist(to: defaults)
+        return macPreferences

As per path instructions, .github/review-bot-rules/cache-substitution-correctness.md requires explicit cold-cache handling when substituting cached/local values for authoritative settings.

🤖 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/MobilePushCoordinator.swift`
around lines 240 - 246, In the cold-cache reconciliation path where
fetchNotificationPreferencesFromMac() is called, the code currently only copies
forwardingMode and hidesContent from macPreferences to localPreferences while
leaving isEnabled as the local default, and never persists the merged values
back to defaults. This causes first-run devices to lose the authoritative Mac
settings. Fix this by ensuring all properties from macPreferences (including the
missing isEnabled property) are copied to localPreferences, and then persist the
complete merged localPreferences back to the defaults cache before returning, so
the authoritative Mac settings are preserved and not overwritten by local
defaults on subsequent app launches.

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/MobilePushCoordinator.swift`:
- Around line 387-397: The three helper properties hasStoredNotificationOptIn,
hasStoredForwardingModePreference, and hasStoredHideContentPreference only check
for key presence in defaults without validating the actual stored values. This
allows stale or invalid values to be treated as authoritative, causing invalid
fallback values in notificationPreferences to potentially sync back to the Mac
and overwrite valid Mac settings. Modify each helper to validate not only that
the key exists but also that the stored value is of the correct type and
represents a valid state before returning true, ensuring stale or malformed
cached values are rejected.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift`:
- Around line 240-246: In the cold-cache reconciliation path where
fetchNotificationPreferencesFromMac() is called, the code currently only copies
forwardingMode and hidesContent from macPreferences to localPreferences while
leaving isEnabled as the local default, and never persists the merged values
back to defaults. This causes first-run devices to lose the authoritative Mac
settings. Fix this by ensuring all properties from macPreferences (including the
missing isEnabled property) are copied to localPreferences, and then persist the
complete merged localPreferences back to the defaults cache before returning, so
the authoritative Mac settings are preserved and not overwritten by local
defaults on subsequent app launches.
🪄 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: 249fdb23-008f-450d-879b-9358e2bdeecf

📥 Commits

Reviewing files that changed from the base of the PR and between a58dd2d and c0b0a5b.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobilePushCoordinator.swift
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift`:
- Line 235: The toggle at line 235 is being disabled whenever
supportsNotificationSettings is false, and the handler at line 432 returns early
in the same condition, which prevents users from disabling notifications on
older Macs. The restriction should only apply to enabling or syncing settings,
not disabling. Modify the disabled condition at line 235 to only restrict when
the notification toggle would be switching from off to on (or when sync is
needed), and update the early return at line 432 to only apply when the user is
trying to enable notifications with pushCoordinator.enable(), allowing the
pushCoordinator.disable() path to proceed unconditionally since it only updates
local state and does not require Mac settings sync support.
🪄 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: 86d21070-3aa8-4131-9f21-b2906623b119

📥 Commits

Reviewing files that changed from the base of the PR and between 7739dfb and e9e84d4.

📒 Files selected for processing (6)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+NotificationSettingsSync.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/NotificationSettingsCapabilityTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • ios/cmux/Resources/Localizable.xcstrings
  • ios/cmuxPackage/Tests/cmuxFeatureTests/cmuxFeatureTests.swift

Comment thread Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift Outdated
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: notify the phone while using the app (foreground/background) + add user-configurable notification settings

3 participants