Skip to content

Surface the Simulator feature at moments of intent on Mac and iOS - #9918

Open
azooz2003-bit wants to merge 2 commits into
mainfrom
feat-simulator-onboarding
Open

Surface the Simulator feature at moments of intent on Mac and iOS#9918
azooz2003-bit wants to merge 2 commits into
mainfrom
feat-simulator-onboarding

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Simulator visibility was low on both platforms: the Mac pane hid behind config customization and the palette, and the phone showed nothing until a user already knew to open the surfaces menu inside a workspace. This PR adds moment-of-intent discovery on both ends, all copy EN+JA.

Mac

  • Contextual tab-bar button: New Simulator appears while any Simulator is booted and disappears otherwise. Presence is sampled at launch and app activation through a throttled, single-flight SimulatorBootPresence (no timers); workspaces re-apply their tab-bar buttons on the flip, reusing the feature-flag re-apply path. A button already present (default or user-configured) wins.
  • Teaching idle state: the "Select a device to start" pane now explains the next step and the phone-control payoff, offers a one-click Start boot, and embeds the same device picker menu as the toolbar.
  • Cross-device teaser: a one-time dismissable chip over a live stage — "Control this Simulator from your iPhone" — opens Tailscale Pairing via the shared performMobileConnectWorkspaceAction path, gated on the mobile-connect flag so it can never advertise a dead path.

iOS

  • Workspace rows show a Simulator chip (green dot when a device is booted), fed by the simulators payload already in the workspace list — no protocol change. Renders in both the SwiftUI and UIKit list pipelines via the shared WorkspaceRow.
  • A one-time dismissable banner inside the workspace ("This workspace has a Mac Simulator") opens the stream directly and names the durable path (the Terminals menu). It retires permanently on dismissal or on opening a simulator stream any other way; the changes hint keeps priority so at most one banner shows.
  • The surfaces menu keeps a "Mac Simulators" section when the capability exists but the workspace has no pane, teaching the Mac-side step instead of hiding the feature.
  • Onboarding gains a simulator card (stage between agents and notifications); screenshots land with the dogfood build.

Everything gates on the existing simulator-enabled-release flag (and the teaser additionally on mobile-connect-button-enabled-release), so no surface can advertise a disabled feature.

No analytics events: every existing emit path attaches a persistent per-install id (iOS client_id, Mac PostHog anonymous distinct id), which fails the completely-anonymous requirement, so the funnel instrumentation was deliberately skipped.

Tests: booted-detection policy on MobileWorkspacePreview, hint-dismissal persistence and composite write-through, plus the existing simulator stream suites (15 green); CmuxSimulator package builds clean.

🤖 Generated with Claude Code


Note

Low Risk
Mostly UI/onboarding and UserDefaults persistence; Mac boot sampling is throttled and flag-gated with no auth or protocol changes.

Overview
Adds moment-of-intent discovery for Mac Simulator streaming on both Mac and iOS, with EN+JA strings and feature-flag gating (simulator-enabled-release; Mac teaser also needs mobile-connect-button-enabled-release).

iOS adds workspace-list Simulator chips (green dot when hasBootedSimulator), a one-time workspace banner that opens the stream and names the Terminals menu, global dismissal via MobileSimulatorStreamHintDismissalStore / dismissSimulatorStreamHint() (also when opening any simulator stream), and an empty-state in the Mac Simulators menu when capability exists but no pane is attached. Onboarding inserts a simulator stage between agents and notifications.

Mac introduces throttled SimulatorBootPresence (launch + activation) to inject a contextual New Simulator tab-bar button while any device is booted, improves the idle Simulator pane with help text, quick-start, and embedded picker, and shows a one-time phone-control teaser on live stages that opens pairing through performMobileConnectWorkspaceAction.

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


Summary by cubic

Improves Simulator discovery on Mac and iOS with contextual UI, clear teaching moments, and one‑tap paths. Adds fresh boot sampling and a resilient pairing teaser so the feature shows up exactly when you’re doing iOS work.

  • New Features
    • Mac: Adds a contextual New Simulator tab-bar button that appears when any Simulator is booted (sampled at launch, app activation, and on remote flag changes via SimulatorBootPresence) and hides otherwise. Refreshes are single‑flight and always fresh (no time‑based cache; one discovery service reused). The idle pane teaches the next step, offers one‑click Start , embeds the device picker, and shows a tooltip when booted. A one‑time “Control this Simulator from your iPhone” chip over a live stage opens pairing via performMobileConnectWorkspaceAction, stays retryable on failure, and can be dismissed.
    • iOS: Workspace rows show a Simulator chip (green dot when a device is booted) using the existing simulators payload. A one‑time banner in the workspace opens the stream and points to the durable path (Terminals menu); it retires globally on dismissal or when any simulator stream is opened (dismissSimulatorStreamHint write‑through). The surfaces menu keeps a “Mac Simulators” section with a teaching empty state when no pane exists. Onboarding adds a Simulator card. Includes case‑insensitive hasBootedSimulator detection and unit tests.
    • Flags & localization: All surfaces gated by simulator-enabled-release; the phone‑control teaser also by mobile-connect-button-enabled-release. Boot‑presence re‑samples when flags change. All copy localized in EN and JA. No analytics added.

Written for commit 3192c42. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added iPhone and iPad Simulator onboarding for controlling a Mac’s Simulator from mobile.
    • Added Simulator workspace indicators, booted-status detection, quick-start actions, and improved empty states.
    • Added Simulator discovery banners and menu guidance with persistent dismissal options.
    • Added an optional phone-control pairing teaser in the Mac Simulator interface.
  • Improvements
    • Automatically surfaces Simulator actions when supported and a device is booted.
    • Added English and Japanese localization for Simulator features and guidance.

Visibility was low on both platforms: the Mac pane hid behind config
customization and the palette, and the phone showed nothing until a
user already knew to open the surfaces menu inside a workspace.

Mac: a contextual New Simulator tab-bar button appears while any
Simulator is booted (sampled at launch and app activation, throttled
and single-flight, never on a timer) and disappears otherwise; the
idle pane state teaches the next step with a one-click boot of the
most recent device plus the embedded device picker; a one-time
dismissable chip over a live stage offers phone control and opens
Tailscale Pairing.

iOS: workspace rows show a Simulator chip (green dot when booted)
fed by the existing list payload; a one-time dismissable banner in
the workspace opens the stream directly and names the durable path
(the Terminals menu), retiring itself when a stream is opened any
other way; the surfaces menu teaches the Mac-side step when the
capability exists but no pane does; onboarding gains a simulator
card (screenshots follow with the dogfood build).

All copy localized EN and JA. No analytics: every existing emit path
attaches a persistent per-install id, which fails the completely-
anonymous requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds simulator boot tracking, macOS simulator controls, iOS simulator onboarding, workspace indicators, persistent simulator-stream hints, and English and Japanese localization.

Changes

Simulator experience

Layer / File(s) Summary
Boot presence and workspace actions
Sources/SimulatorBootPresence.swift, Sources/AppDelegate.swift, Sources/Workspace.swift, Packages/iOS/CmuxMobileShellModel/..., cmux.xcodeproj/project.pbxproj
The app tracks booted simulators and updates workspace surface buttons when boot presence changes.
macOS simulator controls and quick start
Packages/macOS/CmuxSimulator/..., Sources/Panels/SimulatorPanelView.swift, Resources/Localizable.xcstrings
The simulator pane renders phone-control teaser actions, idle guidance, and quick-start actions for available devices.
iOS simulator stream hint and persistence
Packages/iOS/CmuxMobileShell/..., Packages/iOS/CmuxMobileShellUI/..., ios/cmux/Resources/Localizable.xcstrings
The iOS shell persists hint dismissal, shows conditional simulator guidance, and supports simulator-panel selection or empty-state content.
iOS simulator indicators and onboarding
Packages/iOS/CmuxMobileShellUI/...
Workspace rows show simulator status chips. Onboarding includes a simulator stage with navigation, screenshots, accessibility metadata, and localized text.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppDelegate
  participant SimulatorBootPresence
  participant Workspace
  participant SimulatorPanelView
  participant SimulatorDeviceStage
  AppDelegate->>SimulatorBootPresence: refresh simulator boot presence
  SimulatorBootPresence->>Workspace: notify when boot state changes
  Workspace->>Workspace: update New Simulator tab-bar action
  SimulatorPanelView->>SimulatorDeviceStage: provide eligible phone-control teaser
  SimulatorDeviceStage->>SimulatorPanelView: invoke pairing or dismissal action
Loading

Possibly related PRs


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Package Boundaries ❌ Error Sources/SimulatorBootPresence.swift keeps independently testable simulator discovery, boot-state derivation, single-flight refresh, and state transitions in the app root without AppKit or SwiftUI d... Move the tracker into the CmuxSimulator SwiftPM target and expose a small public SimulatorBootPresence or protocol; keep feature-flag, NotificationCenter, AppDelegate, and Workspace wiring in Sources/.
Cmux Full Internationalization ❌ Error Resources/Localizable.xcstrings adds six user-facing Simulator keys with only en and ja, while the touched catalog supports 20 locales; 18 locale entries are missing. Add real translated values for ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant for all six keys.
Cmux Architecture Rethink ❌ Error SimulatorBootPresence adds a NotificationCenter side channel to every Workspace, while Workspace reads AppDelegate.shared state; this splits boot-state propagation and tab-bar ownership. Inject boot presence into the tab-bar coordinator and expose one typed state-change path; remove the global lookup and notification observers, then test the booted-state invariant.
Cmux No Ambient Global State ❌ Error AppDelegate.swift:543 adds runtime state to AppDelegate, and Workspace.swift:3583 reads it through AppDelegate.shared; this is new ambient app-delegate state. Inject SimulatorBootPresence through the application composition root into Workspace or its tab-bar configuration path, instead of accessing it through AppDelegate.shared.
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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
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 New mutable state is @MainActor or actor-isolated; MobileWorkspacePreview remains Sendable; SwiftUI types are UI-bound; the defaults store is used only by the @MainActor composite.
Cmux Swift Blocking Runtime ✅ Passed The PR adds event-triggered, @MainActor single-flight async discovery and notifications; the added Swift lines contain no sleeps, timers, polling loops, blocking waits, main-queue sync, or manual l...
Cmux Browser Automation Off-Main ✅ Passed The PR diff from base ada6abc contains no changes to TerminalController, the execution policy, or policy tests, and adds no browser automation routing or waits.
Cmux Expensive Synchronous Load ✅ Passed The PR adds no agent-history, transcript, trajectory, JSONL, or RestorableAgentSessionIndex load. New simulator discovery is async through actor-backed SimulatorControlService.
Cmux Cache Substitution Correctness ✅ Passed SimulatorBootPresence only drives transient UI; the final diff removes time-based caching and samples simctl freshly, while hint dismissal reads and writes UserDefaults directly.
Cmux No Hacky Sleeps ✅ Passed The cumulative PR diff contains only Swift sources/tests, .xcstrings resources, and an Xcode project file; it introduces no TypeScript, JavaScript, shell, or covered runtime-script changes.
Cmux Algorithmic Complexity ✅ Passed The added collection work is linear: per-workspace simulator detection, device selection, and boot discovery use single-pass scans; panel lookup uses a workspace-keyed dictionary, with no nested ba...
Cmux Swift Concurrency ✅ Passed The diff adds no prohibited Dispatch, Combine, or completion-handler patterns; discovery uses async/await with a stored single-flight Task, and callback Tasks only hop to @MainActor.
Cmux Swift @Concurrent ✅ Passed The new boot sampling awaits actor-isolated SimulatorControlService.discoverDevices(), so process I/O and JSON parsing leave @MainActor; UI-only notification tasks remain intentionally @MainActor.
Cmux Swiftpm Lockfiles ✅ Passed The PR adds only SimulatorBootPresence.swift to cmux.xcodeproj; it changes no SwiftPM package references, Package.swift, .gitignore, or lockfile, so no lockfile diff is required.
Cmux Swift Logging ✅ Passed The PR diff adds no print, debugPrint, dump, NSLog, Logger, or ad hoc stdout/stderr logging; existing logging statements remain unchanged.
Cmux User-Facing Error Privacy ✅ Passed The diff adds generic Simulator guidance and actions only; no user-facing errors, alerts, raw messages, credentials, IDs, or provider/internal details appear in new localized values.
Cmux Swiftui State Layout ✅ Passed Diff adds @Observable state only; no new ObservableObject/@published, GeometryReader, or store-backed lazy rows. State writes occur in lifecycle or user-action callbacks, not body.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR adds no standalone NSWindow, NSPanel, NSWindowController, Window, or WindowGroup; Simulator views remain workspace-pane content, and the auxiliary-window lint passes.
Cmux Source Artifacts ✅ Passed All 29 changed paths are Swift source, tests, localization catalogs, or the Xcode project file; no logs, media, caches, build output, temp folders, or artifact directories were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No added test-build guards or seam-named members exist; the widened simulator selector and dismissal state have production callers in the UI.
Title check ✅ Passed The title clearly summarizes the main change: intent-based Simulator discovery across Mac and iOS.
Description check ✅ Passed The description clearly explains the changes, rationale, feature gating, localization, and testing, but omits several template 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 feat-simulator-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.

@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 using default effort 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 93668ff. Configure here.

struct OnboardingScreenshot: View {
enum Content: String, CaseIterable {
case workspaces
case simulator

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing onboarding simulator screenshots

High Severity

OnboardingScreenshot.Content gains a .simulator case and OnboardingSimulatorView renders it, but Resources/OnboardingScreenshots has no Onboarding-simulator-*.png assets. Missing loads become an empty UIImage, so the new page shows a blank phone frame and everyLocalizedOnboardingScreenshotLoads fails its non-zero size checks.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 93668ff. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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

10-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the simulator onboarding assets. Add the four missing files under Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/OnboardingScreenshots: Onboarding-simulator-en.png, Onboarding-simulator-en-dark.png, Onboarding-simulator-ja.png, and Onboarding-simulator-ja-dark.png. Without them, release builds display a blank screenshot.

🤖 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/OnboardingScreenshot.swift`
around lines 10 - 18, Add the four simulator onboarding PNG
assets—Onboarding-simulator-en.png, Onboarding-simulator-en-dark.png,
Onboarding-simulator-ja.png, and Onboarding-simulator-ja-dark.png—to the
OnboardingScreenshots resources directory so the simulator case in Content loads
correctly in release builds.
🤖 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 `@Sources/AppDelegate.swift`:
- Around line 1375-1379: The feature-flag change handling must also refresh
simulator boot presence when the simulator feature becomes enabled. Update the
existing observer or flag-change handler near `CmuxFeatureFlags.shared.start()`
to detect the transition to enabled and call `simulatorBootPresence.refresh()`,
while preserving the current button reapplication behavior.

In `@Sources/Panels/SimulatorPanelView.swift`:
- Around line 62-66: Update the openPairing closure in SimulatorPanelView so it
calls performMobileConnectWorkspaceAction before setting
phoneControlTeaserDismissed. Only mark the teaser dismissed when that method
returns a non-nil workspace, preserving the dismissed state when pairing cannot
be opened.

In `@Sources/SimulatorBootPresence.swift`:
- Around line 49-51: Remove the time-based minimumRefreshInterval early return
from the boot-presence refresh logic so activation always refreshes from the
authoritative service. Preserve the refreshTask single-flight guard and existing
authoritative update behavior, ensuring hasBootedDevice reflects newly booted
Simulators without throttling.
- Around line 54-55: Remove the 30-second lastRefreshAt suppression from the
hasBootedDevice refresh flow around refreshTask and discoverBootedDevice. Keep
single-flight protection, but allow a fresh
SimulatorControlService.discoverDevices() result whenever the UI state may be
stale, using freshness or event-driven invalidation so Workspace reflects newly
booted devices without a visible stale window.

---

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingScreenshot.swift`:
- Around line 10-18: Add the four simulator onboarding PNG
assets—Onboarding-simulator-en.png, Onboarding-simulator-en-dark.png,
Onboarding-simulator-ja.png, and Onboarding-simulator-ja-dark.png—to the
OnboardingScreenshots resources directory so the simulator case in Content loads
correctly in release builds.
🪄 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: fd7f39d6-63f3-4695-8534-2e7c302f51a2

📥 Commits

Reviewing files that changed from the base of the PR and between ada6abc and 93668ff.

📒 Files selected for processing (29)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+SimulatorStream.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileSimulatorStreamHintDismissalStore.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileSimulatorStreamHintTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspacePreview.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspacePreviewSimulatorTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingFlowView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingSceneChrome.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingScreenshot.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingSimulatorView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/OnboardingStage.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/Resources/Localizable.xcstrings
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalPickerMenu.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView+Surfaces.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceSimulatorChipLabel.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceSimulatorHintBanner.swift
  • Packages/macOS/CmuxSimulator/Sources/CmuxSimulatorUI/Views/SimulatorDeviceStage.swift
  • Packages/macOS/CmuxSimulator/Sources/CmuxSimulatorUI/Views/SimulatorPaneView.swift
  • Packages/macOS/CmuxSimulator/Sources/CmuxSimulatorUI/Views/SimulatorPhoneControlTeaser.swift
  • Packages/macOS/CmuxSimulator/Sources/CmuxSimulatorUI/Views/SimulatorStrings.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/Panels/SimulatorPanelView.swift
  • Sources/SimulatorBootPresence.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • ios/cmux/Resources/Localizable.xcstrings

Comment thread Sources/AppDelegate.swift
Comment thread Sources/Panels/SimulatorPanelView.swift Outdated
Comment thread Sources/SimulatorBootPresence.swift Outdated
Comment thread Sources/SimulatorBootPresence.swift
…fresh

Boot presence now always samples fresh on refresh (single-flight only,
no time-based cache: UI-enabling state must not go stale right after a
boot), reuses one discovery service across samples, and re-samples when
the remote feature flags change so a late flag enable does not wait for
the next app activation. The phone-control teaser stays available when
pairing fails to open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant