Skip to content

iOS: re-implement still-needed terminal/notification fixes from #5259 - #5519

Merged
lawrencecchen merged 9 commits into
mainfrom
feat-ios-terminal-notify-fixes
Jun 6, 2026
Merged

lawrencecchen merged 9 commits into
mainfrom
feat-ios-terminal-notify-fixes

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

What

Re-implements the still-relevant fixes from #5259, which was closed because it was 569 commits behind main and edited files the iOS refactor deleted (ios/cmuxPackage/.../WorkspaceViews.swift, CmuxMobileAuth/AuthManager.swift) — a rebase would have been a bug-prone rewrite. I audited all 16 commits against current main and re-did the 5 still-needed fixes against the new architecture.

Fixes

  • Sign-out push-token teardown ordering (security/correctness). AuthCoordinator.signOut now runs the onSignedOut hook (the APNs device-token DELETE) before revoking the Stack session, so the DELETE still has a valid token. Previously it ran after revocation and was silently skipped, leaving the device receiving pushes for a signed-out account. (+unit test asserting the hook sees a valid token.)
  • Push notification toggle localization. Added mobile.notifications.enable / mobile.notifications.disable (en + ja) to ios/cmux/Resources/Localizable.xcstrings (the call site already referenced them; they were falling back to English with no JA).
  • Notification toggle state. MobileSettingsView mirrors MobilePushCoordinator.isEnabled into @State so the label/icon refresh after the async enable/disable (isEnabled is a non-observable UserDefaults read).
  • createTerminal(in:). Targets an explicit workspace so an in-flight create can't land in a drifted selection. (+unit test.)
  • Terminal focus-suppression + detached-surface guards. Suppress autofocus after chrome actions (create-workspace / create-terminal / switch-terminal), dismiss the hidden text input before SwiftUI chrome so the grid recomputes full-height, and stop a SwiftUI-dismantled surface from doing render/output/accessibility work. Ported across GhosttySurfaceView, GhosttySurfaceRepresentable, WorkspaceShellView, WorkspaceDetailContainer, WorkspaceDetailView.

Skipped (already on main / obsolete)

  • nonisolated isolation fixes and .id(terminalID) remount + notification-delegate isolation are already on main.
  • CMUX_PUSH_REDACTED_* dropped — the redacted push body is generated server-side (web/services/apns).

🤖 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
Touches auth sign-out ordering (push teardown), libghostty surface free queue ordering, and terminal input/focus paths—important for correctness and native crashes, but scoped with regression tests and bounded teardown.

Overview
Ports still-needed iOS fixes onto the refactored shell/auth stack: sign-out, mobile terminal UX, notifications settings, and Ghostty surface lifecycle.

Sign-out: AuthCoordinator.signOut now runs the composition-root onSignedOut hook (e.g. authenticated APNs device-token DELETE) before client.signOut() revokes tokens, with a 5s bounded task group that joins and cancels slow teardown so sign-out cannot hang and teardown cannot outlive the session. Tests cover token visibility during the hook and deadline cancellation.

Mobile shell / terminal: MobileShellComposite adds one-shot autofocus suppression for chrome paths (create workspace/terminal, terminal picker via selectTerminalFromChrome); push deep links still use selectTerminal and may autofocus. createTerminal(in:) pins the target workspace for async remote creates. UI wires suppression through GhosttySurfaceRepresentable, resigns input before chrome (GhosttySurfaceView.resignActiveInput()), and passes explicit workspace id from WorkspaceDetailContainer.

Ghostty: Surfaces honor autoFocusOnWindowAttach, prepareForDismantle stops render/output/a11y after SwiftUI removal, and libghostty ghostty_surface_free runs on the shared outputQueue (replacing a separate disposer queue) to avoid use-after-free with queued render/output work.

Settings / l10n: MobileSettingsView mirrors push enable state in @State (seeded on appear) so the toggle label updates after async enable/disable; adds en/ja strings for notification enable/disable labels.

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


Summary by cubic

Re-implements iOS fixes for secure sign-out, scoped terminal creates, predictable keyboard behavior, and safer terminal teardown; merged with main’s new host picker/toolbar. Adds localized notification labels and keeps the settings toggle in sync.

  • Bug Fixes
    • Sign-out: run the push teardown hook before token revoke; bounded by a 5s deadline and cancelled/joined before return; tests cover “hook sees valid token” and deadline cancel.
    • Notifications: add mobile.notifications.enable / mobile.notifications.disable (en + ja); mirror push state into @State (seeded on appear) so the label/icon update after enable/disable.
    • Terminal targeting + focus: createTerminal(in:) pins creates to a specific workspace; store tracks one‑shot autofocus suppression per terminal id so creates and picker switches don’t pop the keyboard while push deep‑links still do; suppression is passed via autoFocusOnWindowAttach and consumed on appear; GhosttySurfaceView.resignActiveInput() is called before overlays with keyboardWillHide owning cleanup.
    • Terminal lifecycle + disposal: dismantled/detached surfaces stop render/output/a11y work; free libghostty surfaces on the shared output queue (FIFO with process_output/render_now) to avoid use‑after‑free; removed the separate disposer queue; integrates cleanly with main’s host switcher and toolbar.

Written for commit 3b7fc5b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Fixed notifications toggle state not syncing properly with the coordinator.
  • New Features

    • Added autofocus suppression for terminal surfaces to prevent unwanted keyboard activation in specific workflows.
    • Improved terminal creation to respect explicit workspace context, preventing stale workspace selection issues.
  • Improvements

    • Enhanced keyboard input management for terminal surfaces with better lifecycle handling.
    • Added localization strings for notification control actions.

#5259 was 569 commits behind and edited files the iOS refactor deleted, so it
was closed and the still-relevant fixes re-done fresh against current main:

- Auth: run the sign-out teardown hook (push-token DELETE) BEFORE revoking the
  Stack session, so the server-side device-token delete can still authenticate;
  otherwise the device keeps receiving pushes for a signed-out account. (+test)
- iOS push notification toggle strings localized (en + ja).
- Notifications toggle: mirror MobilePushCoordinator.isEnabled into @State so the
  label/icon refresh after the async enable/disable (isEnabled is a non-observable
  UserDefaults read).
- createTerminal(in:): target an explicit workspace so an in-flight create can't
  land in a drifted selection. (+test)
- Terminal focus-suppression + detached-surface guards: suppress autofocus after
  chrome actions, dismiss the hidden input before chrome so the grid recomputes
  full-height, and stop a SwiftUI-dismantled surface from doing render/output/a11y
  work. Ported across GhosttySurfaceView / GhosttySurfaceRepresentable /
  WorkspaceShellView / WorkspaceDetailContainer / WorkspaceDetailView.

Skipped (already on main): nonisolated isolation fixes; .id(terminalID) remount +
delegate isolation. Dropped CMUX_PUSH_REDACTED_* (server-side, obsolete).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 6, 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 6, 2026 9:37pm
cmux-staging Building Building Preview, Comment Jun 6, 2026 9:37pm

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f12d83f7-013c-4aa7-89da-adb9fe2ef48b

📥 Commits

Reviewing files that changed from the base of the PR and between ceb5f45 and 3b7fc5b.

📒 Files selected for processing (5)
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • ios/cmux/Resources/Localizable.xcstrings

📝 Walkthrough

Walkthrough

This PR enhances sign-out teardown ordering, hardens terminal surface lifecycle management, adds workspace-targeted terminal creation with autofocus suppression, refactors mobile view selection routing, and updates notifications settings with observable state and localization.

Changes

Sign-out, Terminal Lifecycle, Autofocus, and Mobile Views

Layer / File(s) Summary
Sign-out hook execution order and cleanup
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift, Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthCoordinatorTests.swift, Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/Fakes.swift
AuthCoordinator.signOut(onSignedOut:teardownTimeout:) runs the hook concurrently with a cancellable timeout before client.signOut(), preserving token validity for teardown. Tests verify tokens are accessible in the hook and timeout-bounded cancellation works via TokenProbe and TeardownOutcomeProbe fakes.
GhosttySurfaceView lifecycle quiesce and input APIs
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
Adds isDismantled flag to quiesce render/output/accessibility after SwiftUI dismantle, static weak activeInputSurface to track input first-responder, public APIs (resignActiveInput(), resignInput(), prepareForDismantle()), guards render/output paths when dismantled, and moves libghostty surface free onto the serial outputQueue for FIFO ordering.
GhosttySurfaceRepresentable autofocus and dismantle wiring
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
Adds autoFocusOnWindowAttach boolean to control keyboard focus on window attach. Sets it in makeUIView, propagates updates in updateUIView, and calls prepareForDismantle() during dismantleUIView.
Workspace-targeted terminal creation and autofocus suppression
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
createTerminal(in:) accepts optional workspace ID to pin selection during remote RPCs and suppress autofocus for newly created terminals. Exposes public APIs for chrome-focused selection (selectTerminalFromChrome) and autofocus control (shouldAutoFocusTerminalSurface, consumeTerminalAutoFocusSuppression).
WorkspaceDetailView and container autofocus and selection routing
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift, Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
WorkspaceDetailContainer scopes createTerminal closure to workspace. WorkspaceDetailView derives selected terminal from store, computes autoFocusOnWindowAttach per surface, routes picker selection through selectTerminalFromChrome(), and resigns active input before keyboard dismissal.
Workspace-targeted creation preview test
Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift
Test verifies createTerminal(in:) targets explicit workspace even when selectedWorkspaceID drifts.
Notifications settings observable state and localization
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift, ios/cmux/Resources/Localizable.xcstrings
MobileSettingsView adds observable notificationsEnabled state for proper SwiftUI re-rendering. Notifications button drives async enable/disable via coordinator. Added localized strings for enable/disable in English and Japanese.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops through the shell where surfaces dance,
Sign-out hooks valid, in structured stance,
Workspace-scoped terms with focus refined,
Autofocus suppressed, just one at a time!
From lifecycle quiesce to chrome-tinted views,
We've tamed the terminal blues! ✨


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error AuthCoordinator.signOut introduces Task.sleep(for: teardownTimeout) in production code for timeout synchronization. Rule prohibits Task.sleep in shipped code; needs real cancellation signals instead. Replace Task.sleep deadline with cancellation-based timeout or async sequence; avoid sleep-based deadline racing in production code for teardown coordination.
Cmux Algorithmic Complexity ❌ Error Line 96-97 of WorkspaceShellView.swift contains nested scan: removeAll loop calls contains on workspaceIDs Array, O(m*n) complexity on hot path. Convert workspaceIDs Array to Set before the removeAll loop to make contains O(1) instead of O(n).
Cmux Architecture Rethink ❌ Error Mutable state terminalAutoFocusSuppressedSurfaceIDs not cleared in signOut/clearRemoteConnectionContext, leaving stale autofocus suppression representable across connection cycles. Clear terminalAutoFocusSuppressedSurfaceIDs in signOut() and clearRemoteConnectionContext() to prevent stale suppression leaking to subsequent connections.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: re-implementing iOS fixes from a prior PR, with focus on terminal/notification features.
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 PR properly uses @Sendable for async callbacks, @MainActor for store access, and correctly isolates new public methods. No new isolation violations introduced.
Cmux No Hacky Sleeps ✅ Passed Rule applies only to TypeScript, JavaScript, shell, and build/runtime scripts. PR contains only Swift code; Swift timing is covered by separate swift-blocking-runtime rule.
Cmux Swift Concurrency ✅ Passed Modern async/await with structured task groups, proper actor isolation in tests, stored/cancelled Tasks, and no Combine or fire-and-forget patterns outside allowed boundaries.
Cmux Swift @Concurrent ✅ Passed PR complies with @concurrent annotation rules: proper @escaping @Sendable on async closures in signOut, all new methods are synchronous, @MainActor async methods appropriately stay on main thread.
Cmux Swift File And Package Boundaries ✅ Passed Additions to oversized files (81 and 87 lines) are below 250-line threshold; responsibilities remain cohesive; all changes in SwiftPM packages.
Cmux Swift Logging ✅ Passed PR has no Swift logging violations. The only NSLog found is DEBUG-guarded in WorkspaceDetailView.swift. Pre-existing loggers used in new code follow proper redaction practices.
Cmux User-Facing Error Privacy ✅ Passed All user-facing changes use generic safe terms with no vendor names, credentials, or sensitive implementation details; all error logs properly use privacy annotations.
Cmux Full Internationalization ✅ Passed PR adds user-facing strings via L10n.string() API with complete locale coverage (en, ja) in Localizable.xcstrings; all modified production files comply with localization requirements.
Cmux Swiftui State Layout ✅ Passed All SwiftUI changes follow state layout rules: @State initialized in lifecycle, no @Published/@observableobject, no GeometryReader, List rows pass value snapshots and closures, not store refs.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains only iOS terminal/auth UI refactoring with no NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup additions; the rule does not apply to iOS-only, non-window changes.
Cmux Source Artifacts ✅ Passed All files are legitimate source/test/config files (.swift, .xcstrings, .yaml, .yml) with no local tool output, generated logs, artifacts, caches, or build output.
Description check ✅ Passed The PR description is comprehensive and covers all major changes (sign-out teardown, notifications, terminal targeting, focus suppression, surface lifecycle), with clear sections explaining what was changed, why, and references to fixes from a prior PR.
✨ 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-terminal-notify-fixes

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 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR re-implements five targeted iOS fixes from a stale predecessor branch against the current architecture: sign-out push-teardown ordering, push-notification toggle localization and UI state, workspace-scoped terminal creation, and terminal autofocus/keyboard suppression with Ghostty surface lifecycle guards.

  • Auth teardown: signOut now runs the onSignedOut hook (APNs device-token DELETE) before client.signOut() revokes tokens, bounded by a structured 5-second task group that cancels and joins slow teardown so sign-out cannot hang indefinitely; two regression tests cover the token-visibility and deadline-cancel paths.
  • Localization and settings state: mobile.notifications.enable / mobile.notifications.disable are added to Localizable.xcstrings with both en and ja entries; MobileSettingsView mirrors MobilePushCoordinator.isEnabled into @State (seeded on .onAppear) so the toggle label and icon update after the async enable/disable.
  • Terminal focus and lifecycle: createTerminal(in:) pins creates to an explicit workspace ID; the store tracks one-shot autofocus suppression per surface ID so chrome actions don't pop the keyboard while push deep-links still may; GhosttySurfaceView gains prepareForDismantle() / resignActiveInput() guards and frees libghostty surfaces on the shared serial outputQueue to prevent use-after-free on fast terminal switches.

Confidence Score: 5/5

Safe to merge. All five fixes are well-scoped, tested, and consistent with the current architecture.

The auth teardown change is the most sensitive path: it moves the push-unregistration hook before token revocation and bounds it with a structured task group that joins slow teardown before returning, which is both correct and tested with two regression assertions. The terminal autofocus suppression is a store-owned Set with one-shot semantics; the PR adds unit tests for all entry points (create, chrome switch, push deep-link). Localization additions include both supported locales. The Ghostty surface lifecycle changes (dismantle guard, queue-ordered free) are narrow and target a concrete use-after-free risk on fast terminal switches. No dead-state, no timing-based synchronization, and no new actor isolation mistakes were found.

No files require special attention.

Important Files Changed

Filename Overview
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift Restructures signOut to run the onSignedOut hook before token revocation, bounded by a structured task group with a configurable teardown timeout; the logic is correct and the previous-comment concern about an unbound hook is now addressed.
Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthCoordinatorTests.swift Adds two regression tests: hook sees a valid token (ordering assertion) and slow teardown is cancelled and joined before sign-out returns (structured-task-group assertion).
Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/Fakes.swift Adds TokenProbe and TeardownOutcomeProbe actor types to support the new sign-out ordering and deadline-cancel assertions.
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds terminalAutoFocusSuppressedSurfaceIDs Set with one-shot suppression API; createTerminal(in:) now accepts an explicit workspace ID to prevent drift; implementation is correct and well-encapsulated in the store.
Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift Adds tests for explicit workspace targeting, one-shot autofocus suppression lifecycle, and push-navigation exemption; all tests are correct and cover the new store behaviors.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift Adds autoFocusOnWindowAttach prop threaded from the store suppression set; dismantleUIView now calls prepareForDismantle() before coordinator.detach().
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift Adds @State private var notificationsEnabled seeded in .onAppear; correctly mirrors the non-observable UserDefaults-backed isEnabled property so the toggle label and icon refresh after async enable/disable.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift Passes an explicit workspace.id to createTerminal(in:) so an in-flight create cannot land in a drifted workspace.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Threads autoFocusOnWindowAttach from the store into GhosttySurfaceRepresentable and calls consumeTerminalAutoFocusSuppression on .onAppear; adds selectTerminalFromChrome for keyboard-safe picker navigation.
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift Adds isDismantled flag with prepareForDismantle() guard; frees libghostty surfaces on the shared serial outputQueue via Unmanaged.passRetained/release to prevent use-after-free; adds static resignActiveInput() for chrome keyboard dismissal.
ios/cmux/Resources/Localizable.xcstrings Adds mobile.notifications.enable and mobile.notifications.disable with both en and ja translations; both keys are already referenced at the call site.

Sequence Diagram

sequenceDiagram
    participant UI as iOS UI
    participant AC as AuthCoordinator
    participant Hook as onSignedOut hook (APNs DELETE)
    participant Client as StackClient

    UI->>AC: signOut()
    AC->>+Hook: "addTask { await onSignedOut() }"
    AC->>AC: "addTask { sleep(teardownTimeout) }"
    Note over AC: withTaskGroup races hook vs timer
    alt hook finishes first (normal path)
        Hook-->>-AC: completed
        AC->>AC: cancelAll() cancels timer
    else timer fires first (slow network)
        AC->>Hook: cancelAll() sends CancellationError
        Hook-->>AC: catches error, exits (joined)
    end
    AC->>Client: client.signOut() — tokens still valid above
    AC->>AC: clearAuthState()
    AC-->>UI: sign-out complete
Loading

Reviews (9): Last reviewed commit: "Merge origin/main into feat-ios-terminal..." | Re-trigger Greptile

Comment on lines 443 to 453
public func signOut(onSignedOut: @Sendable () async -> Void = {}) async {
// Run the teardown hook first, while tokens are still valid (see note).
await onSignedOut()
do {
try await client.signOut()
} catch {
authLog.error("Sign-out failed: \(error.localizedDescription, privacy: .private)")
}
if launch.includesDevAuth { debugCredentials = nil }
clearAuthState()
await onSignedOut()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Sign-out blocked if hook stalls

Before this change, client.signOut() and clearAuthState() ran unconditionally; the hook was a best-effort cleanup. Now the hook runs first with no timeout at this layer, so if the APNs DELETE hangs (server unreachable, network error, or an indefinite retry in the caller), client.signOut() and clearAuthState() never execute. The user's sign-out request appears to freeze, and the Stack session stays live — exactly the state the PR is trying to prevent. Consider wrapping the hook with a withTimeout or Task.detached so sign-out always completes regardless of hook outcome, while still preserving the token for the DELETE attempt.

Comment on lines 252 to 261
private func selectTerminalFromPicker(_ terminalID: MobileTerminalPreview.ID) {
dismissTerminalKeyboardForChrome()
isTerminalPickerPresented = false
// Switching to a different terminal is chrome, not a typing intent, so
// the newly-selected surface must not grab the keyboard on attach.
if selectedTerminal?.id != terminalID {
terminalAutoFocusSuppressedSurfaceIDs.insert(terminalID.rawValue)
}
suppressNextTerminalAutoFocus = false
selectedTerminalID = terminalID

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 One-shot suppression cleared without a per-surface substitute

selectTerminalFromPicker always sets suppressNextTerminalAutoFocus = false, but the preceding guard only inserts into terminalAutoFocusSuppressedSurfaceIDs when the selection changes (selectedTerminal?.id != terminalID). If the same terminal is re-confirmed from the picker, the per-surface entry is never added. This means any one-shot suppression set by a concurrent createWorkspaceFromToolbar call (which does not dismiss the picker) is silently cleared with no substitute — the next surface that appears will then autofocus and pop the keyboard. On iPad where the picker popover and the toolbar are both reachable simultaneously, this race is user-triggerable.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 6, 2026

@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/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 1506-1515: resignInput() currently zeroes keyboardHeight before
keyboardWillHide can run, causing the guard in keyboardWillHide (guard
keyboardHeight != 0) to bail out and skip inputProxy.setKeyboardShown(false) and
toolbar animations; update resignInput() (and the analogous block around
keyboardWillHide handling) to perform the keyboard-hide cleanup before resetting
keyboardHeight: first call inputProxy.setKeyboardShown(false) (or invoke the
same keyboardWillHide cleanup path), then set keyboardHeight = 0 and call
setNeedsGeometrySync(); ensure Self.activeInputSurface handling remains
unchanged and that keyboardWillHide will see a non-zero keyboardHeight when
invoked.
🪄 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: 2f0d3239-f5f7-4eb6-838e-34c6e559b215

📥 Commits

Reviewing files that changed from the base of the PR and between 97cf213 and c619c10.

📒 Files selected for processing (12)
  • Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
  • Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthCoordinatorTests.swift
  • Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/Fakes.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/GhosttySurfaceRepresentable.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileSettingsView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
  • ios/cmux/Resources/Localizable.xcstrings

Autoreview: the workspace-list '+' (split + compact) created a workspace and
mounted its terminal with autoFocusOnWindowAttach still true, popping the
keyboard. Route both list-create paths through the same suppression the detail
toolbar uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Outdated
…cleanup

Autoreview round 2:
- AuthCoordinator.signOut: bound the onSignedOut teardown (push-token DELETE) in
  a task group with a 5s deadline so local sign-out is never gated on a slow/
  stuck network call, while still giving the DELETE a chance with valid tokens.
- GhosttySurfaceView.resignInput(): stop pre-zeroing keyboardHeight; let
  resignFirstResponder's keyboardWillHide run the full hide cleanup (toolbar
  animation, proxy state) instead of short-circuiting it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 6, 2026 03:06
Autoreview round 3: the round-2 withTaskGroup bound still structurally awaited
the teardown child, so a teardown that ignores cancellation (a wedged network
DELETE) would keep withTaskGroup from returning and block local sign-out past
the deadline. Run the teardown unstructured and only await a deadline it cancels
on completion: sign-out proceeds at min(teardown, 5s), and a stuck teardown keeps
running detached without holding up the local clear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rate one

Autoreview round 4: disposeSurface() freed the surface on GhosttySurfaceDisposer's
own serial queue while process_output / render_now / binding_action run on the
shared Self.outputQueue with a captured surface pointer. Two different queues let
the free race a queued ghostty_* call on the same pointer (use-after-free during
fast terminal switches / removals).

Free on Self.outputQueue instead. It's serial and FIFO, so the free is ordered
after every already-enqueued block that captured the pointer, and never runs
concurrently with one. processOutput's main-actor guard stops new work once
surface is nil, so only the bounded backlog drains before the free. Removes the
separate-queue disposer (a namespace-enum carve-out) entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 6, 2026

@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 (2)
Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift (2)

1506-1508: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Block geometry scheduling once the surface is dismantled.

After Line 1507, a last layoutSubviews() / applyViewSize() can still reach Line 1909’s direct syncSurfaceGeometry() path because prepareForReuseAfterDetach() has already nilled the display link while the view may still have a window. That schedules ghostty_surface_set_size work on the shared outputQueue, so a SwiftUI-removed surface can still consume the same serial queue that live terminals use for process_output and render_now. Guard the geometry path on isDismantled and clear any pending geometry state in prepareForDismantle().

Suggested fix
 public func prepareForDismantle() {
     isDismantled = true
+    needsGeometrySync = false
+    pendingGeometryReassert = false
+    pendingViewportReport = nil
+    needsDraw = false
     prepareForReuseAfterDetach()
 }

 private func setNeedsGeometrySync(reassertNaturalSize: Bool = true) {
+    guard !isDismantled else { return }
     needsGeometrySync = true
     if reassertNaturalSize { pendingGeometryReassert = true }
     needsDraw = true
@@
 }

 private func syncSurfaceGeometry(shouldReassertNaturalSize: Bool = true) {
-    guard let surface else { return }
+    guard !isDismantled, let surface else { return }
@@
             let result = GeometryResult(cellPixelSize: cell, naturalSize: natural, pinnedSize: pinnedSize)
             DispatchQueue.main.async {
-                self?.applyGeometryResult(
-                    result,
-                    scale: scale,
-                    containerW: containerW,
-                    containerH: containerH,
-                    shouldReassertNaturalSize: shouldReassertNaturalSize
-                )
+                guard let self, !self.isDismantled else { return }
+                self.applyGeometryResult(
+                    result,
+                    scale: scale,
+                    containerW: containerW,
+                    containerH: containerH,
+                    shouldReassertNaturalSize: shouldReassertNaturalSize
+                )
             }
         }
     }
🤖 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/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`
around lines 1506 - 1508, Set isDismantled and clear any pending geometry state
in prepareForDismantle(): after setting isDismantled = true call code to nil or
reset any stored geometry variables and cancel/clear any scheduled geometry work
so no ghostty_surface_set_size remains queued on the shared outputQueue; then
modify the direct geometry path (the fast path reached from
layoutSubviews()/applyViewSize() into syncSurfaceGeometry()) to early-return if
isDismantled is true so a dismantled view never schedules
ghostty_surface_set_size or touches the outputQueue after
prepareForReuseAfterDetach()/prepareForDismantle().

1492-1509: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Stop growing GhosttySurfaceView inside this already-oversized file.

This PR adds more lifecycle/input-teardown/disposal logic to a production Swift file that already mixes rendering, input, gestures, snapshots, accessibility, registry state, and libghostty lifecycle. Please extract this surface-lifecycle slice into its own type/file instead of extending GhosttySurfaceView further.

As per coding guidelines, "Flag Swift production files that exceed 400 lines without a clear single responsibility, or exceed 800 lines even with mostly coherent responsibility" and "One major type per file."

Also applies to: 1515-1523, 1643-1663

🤖 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/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`
around lines 1492 - 1509, This file is growing and the
lifecycle/input-teardown/disposal logic should be moved out of
GhosttySurfaceView: create a new type (e.g., GhosttySurfaceLifecycle or
GhosttySurfaceController) in its own file and move the lifecycle methods and
related state — resignInput(), prepareForDismantle(),
prepareForReuseAfterDetach(), isDismandled flag, any references to inputProxy,
Self.activeInputSurface, and keyboard-related state/guards — into that new type;
update GhosttySurfaceView to hold/forward to the new lifecycle object
(delegation/composition) so all callers use the same API but the large lifecycle
responsibilities are removed from GhosttySurfaceView. Ensure ownership of
activeInputSurface semantics and keyboard hide/cleanup behavior are preserved
and referenced symbols remain unchanged so external callers compile.

Source: Coding guidelines

🤖 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/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift`:
- Around line 450-459: The sign-out hook Task is inheriting MainActor from the
surrounding `@MainActor` method so a long-running synchronous onSignedOut() can
block MainActor and bypass the 5s deadline; change the child Task that runs the
hook to a non-inheriting task (use Task.detached) so onSignedOut() starts off
the MainActor and deadline.cancel() is still called when it finishes, i.e.
replace the Task { await onSignedOut(); deadline.cancel() } with a Task.detached
{ await onSignedOut(); deadline.cancel() } (referencing the onSignedOut() call
and the deadline Task variable).

---

Outside diff comments:
In
`@Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift`:
- Around line 1506-1508: Set isDismantled and clear any pending geometry state
in prepareForDismantle(): after setting isDismantled = true call code to nil or
reset any stored geometry variables and cancel/clear any scheduled geometry work
so no ghostty_surface_set_size remains queued on the shared outputQueue; then
modify the direct geometry path (the fast path reached from
layoutSubviews()/applyViewSize() into syncSurfaceGeometry()) to early-return if
isDismantled is true so a dismantled view never schedules
ghostty_surface_set_size or touches the outputQueue after
prepareForReuseAfterDetach()/prepareForDismantle().
- Around line 1492-1509: This file is growing and the
lifecycle/input-teardown/disposal logic should be moved out of
GhosttySurfaceView: create a new type (e.g., GhosttySurfaceLifecycle or
GhosttySurfaceController) in its own file and move the lifecycle methods and
related state — resignInput(), prepareForDismantle(),
prepareForReuseAfterDetach(), isDismandled flag, any references to inputProxy,
Self.activeInputSurface, and keyboard-related state/guards — into that new type;
update GhosttySurfaceView to hold/forward to the new lifecycle object
(delegation/composition) so all callers use the same API but the large lifecycle
responsibilities are removed from GhosttySurfaceView. Ensure ownership of
activeInputSurface semantics and keyboard hide/cleanup behavior are preserved
and referenced symbols remain unchanged so external callers compile.
🪄 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: dd04db22-6835-4bd1-8608-8fa946484d23

📥 Commits

Reviewing files that changed from the base of the PR and between c619c10 and bfdf259.

📒 Files selected for processing (3)
  • Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift

Autoreview round 5: the round-4 unstructured teardown could outlive signOut().
unregisterFromServer() builds its DELETE from the LIVE AuthCoordinator's tokens
(PushRegistrationService.makeRequest reads tokenProvider at request-build time),
so a delayed stale task that ran after a subsequent sign-in would authenticate
the device-token DELETE with the NEW account's credentials and break push for
that session.

Make the teardown structured: a task group runs the hook + a bounded deadline,
then cancelAll() once either finishes; the group joins the (cancelled) hook
before returning, so no teardown ever outlives signOut. The push DELETE runs on
URLSession (cancellation-aware), so cancelAll() unblocks the join promptly, and
awaiting inline guarantees the hook reads this account's tokens since no new
sign-in can interleave before signOut returns. (Reverses the round-3 detached
approach, which traded round-5's severe cross-account corruption for a
hypothetical block by a hook that ignores cancellation; the real hook cancels.)

The deadline is an injected Duration (teardownTimeout, default 5s) per the
bounded-delay carve-out, so the new signOutJoinsAndCancelsSlowTeardownAtDeadline
test exercises the cancel-and-join path in 50ms with no real waiting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Outdated
@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] June 6, 2026 11:31

Dismissed: CodeRabbit now posts non-blocking comment reviews (request_changes_workflow=false, #5538).

lawrencecchen and others added 2 commits June 6, 2026 05:02
Moves the "don't autofocus a freshly-created terminal" suppression out of
WorkspaceShellView's @State (a free-floating `suppressNextTerminalAutoFocus`
boolean plus a convert-on-next-selection dance) and into MobileShellComposite,
keyed by terminal id. The boolean could not tell a chrome create from a
push-notification deep link (both arrive as the same selection change), so any
"suppress whatever comes next" scheme sometimes suppressed the wrong surface;
it also leaked when a create failed and the selection never changed.

This commit lands the store API (`shouldAutoFocusTerminalSurface`,
`consumeTerminalAutoFocusSuppression`, `selectTerminalFromChrome`), migrates
the three views to read it, and adds behavior tests, but does NOT yet wire the
create paths to suppress the new terminal id. The "created terminal is
suppressed" tests therefore fail here (red); the next commit adds the wiring.

Also reorders createTerminal(in:) so a remote create that is going to abort
(another create already in flight) bails before pinning selectedWorkspaceID,
so a second "+" can't strand the UI on a workspace with no new terminal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every create path (local + remote createWorkspace/createTerminal) now marks the
freshly-created terminal id in the store the instant it becomes the selection,
so its surface mounts with autofocus disabled. This makes the previous commit's
tests pass (green) and dissolves the bot-flagged edge cases of the old boolean:

- A create that fails (remote RPC error) never produces a terminal id, so there
  is nothing to leak and the current terminal stays autofocusable.
- A push-notification deep link goes through selectTerminal, which is left out
  of the suppression set, so it autofocuses even mid-create.
- Re-confirming the already-selected terminal in the picker is a no-op
  suppression (selectTerminalFromChrome only suppresses an actual switch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen force-pushed the feat-ios-terminal-notify-fixes branch from 5da5350 to ceb5f45 Compare June 6, 2026 12:13

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Addressed the remaining open review findings (the AuthCoordinator sign-out and resignInput keyboard findings were already handled by earlier commits on this branch).

Findings 1-3 (autofocus suppression) were one root cause. The suppressNextTerminalAutoFocus boolean lived in WorkspaceShellView @State and could not tell a chrome create from a push-notification selectTerminal (both reach the view as the same selectedTerminal?.id change), so any "suppress whatever selection comes next" scheme sometimes suppressed the wrong surface and leaked when a create failed. Moved suppression into MobileShellComposite, keyed by the actual created terminal id:

  • create paths mark the new id, so a failed create marks nothing (stuck-after-failure dissolves);
  • selectTerminal (the push-notification path) is never added to the set, so deep links still autofocus;
  • selectTerminalFromChrome no-ops when re-confirming the already-selected terminal.

Finding 4: createTerminal(in:) now bails before pinning selectedWorkspaceID when a create is already in flight, so a second "+" can't strand the UI on a workspace with no new terminal.

Suppression is now store state, so it is unit-tested (4 behavior tests in MobileShellCompositePreviewTests); it was untestable as view @State. Two-commit red/green: the failing tests land first, the create-path wiring that makes them pass lands second.

@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 (2)
Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift (1)

778-783: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add full DocC callouts for the new public APIs.

These package-level public functions have summaries, but the package docs policy also requires - Parameter / - Returns: callouts on funcs that take parameters or return values. Please fill those in for createTerminal(in:), selectTerminalFromChrome(_:), shouldAutoFocusTerminalSurface(_:), and consumeTerminalAutoFocusSuppression(for:).

As per coding guidelines, "Every public symbol in any new Swift package under Packages/ is documented..." and "Use - Parameter name: / - Returns: / - Throws: callouts on init and func symbols that take parameters or throw."

Also applies to: 820-845

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 778 - 783, Add DocC callouts for the public functions in
MobileShellComposite: update the documentation for createTerminal(in:),
selectTerminalFromChrome(_:), shouldAutoFocusTerminalSurface(_:), and
consumeTerminalAutoFocusSuppression(for:) to include - Parameter entries for
each parameter and - Returns: where the function returns a value, and - Throws:
if any throw; ensure the callouts follow the existing summary style and use the
exact symbol names (createTerminal(in:), selectTerminalFromChrome(_:),
shouldAutoFocusTerminalSurface(_:), consumeTerminalAutoFocusSuppression(for:))
so the package-level public API has complete DocC parameter/return documentation
per the package docs policy.

Source: Coding guidelines

Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift (1)

44-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume the suppression token after the surface actually attaches, not in onAppear.

The autofocus gate lives in GhosttySurfaceView.didMoveToWindow(), but this clears the token from SwiftUI onAppear. That makes the feature timing-dependent: if onAppear fires, triggers a rerender, and updateUIView flips autoFocusOnWindowAttach back to true before the UIView reaches didMoveToWindow(), the keyboard still pops on the very mount this PR is trying to suppress. Move the consume step into the hosted-view/coordinator attach path that runs after the surface is in a window.

🤖 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/WorkspaceDetailView.swift`
around lines 44 - 59, The current call to
store.consumeTerminalAutoFocusSuppression(for: terminalID) in
WorkspaceDetailView's onAppear is too early; move the consume step into the
GhosttySurfaceRepresentable's hosted view attach path so it runs after the
UIView is in a window. Specifically, remove the
consumeTerminalAutoFocusSuppression call from the .onAppear block in
WorkspaceDetailView and instead invoke
store.consumeTerminalAutoFocusSuppression(for:) from the
GhosttySurfaceView/Coordinator attachment flow (e.g., in
GhosttySurfaceView.didMoveToWindow() or the coordinator's view attach handler
that runs after makeUIView/dismantleUIView) so the suppression token is cleared
only after the surface is attached and the didMoveToWindow-based autofocus gate
can operate correctly.
🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 90-100: terminalAutoFocusSuppressedSurfaceIDs persists across
session resets and can suppress autofocus in a new connection; update signOut()
and clearRemoteConnectionContext() to clear
terminalAutoFocusSuppressedSurfaceIDs when rebuilding session state OR modify
logic that uses terminalAutoFocusSuppressedSurfaceIDs to include the current
connectionGeneration so suppressed IDs are only valid for that generation (e.g.,
pair the set with connectionGeneration or check connectionGeneration when
consuming/consulting the set from consumeTerminalAutoFocusSuppression(for:)).
Ensure you reference and clear/update terminalAutoFocusSuppressedSurfaceIDs
inside the same reset code paths (signOut() and clearRemoteConnectionContext())
so stale IDs cannot carry over.

---

Outside diff comments:
In `@Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`:
- Around line 778-783: Add DocC callouts for the public functions in
MobileShellComposite: update the documentation for createTerminal(in:),
selectTerminalFromChrome(_:), shouldAutoFocusTerminalSurface(_:), and
consumeTerminalAutoFocusSuppression(for:) to include - Parameter entries for
each parameter and - Returns: where the function returns a value, and - Throws:
if any throw; ensure the callouts follow the existing summary style and use the
exact symbol names (createTerminal(in:), selectTerminalFromChrome(_:),
shouldAutoFocusTerminalSurface(_:), consumeTerminalAutoFocusSuppression(for:))
so the package-level public API has complete DocC parameter/return documentation
per the package docs policy.

In
`@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift`:
- Around line 44-59: The current call to
store.consumeTerminalAutoFocusSuppression(for: terminalID) in
WorkspaceDetailView's onAppear is too early; move the consume step into the
GhosttySurfaceRepresentable's hosted view attach path so it runs after the
UIView is in a window. Specifically, remove the
consumeTerminalAutoFocusSuppression call from the .onAppear block in
WorkspaceDetailView and instead invoke
store.consumeTerminalAutoFocusSuppression(for:) from the
GhosttySurfaceView/Coordinator attachment flow (e.g., in
GhosttySurfaceView.didMoveToWindow() or the coordinator's view attach handler
that runs after makeUIView/dismantleUIView) so the suppression token is cleared
only after the surface is attached and the didMoveToWindow-based autofocus gate
can operate correctly.
🪄 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: e3471501-50c9-4815-a781-6b1bbe48645b

📥 Commits

Reviewing files that changed from the base of the PR and between bfdf259 and ceb5f45.

📒 Files selected for processing (8)
  • Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
  • Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthCoordinatorTests.swift
  • Packages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/Fakes.swift
  • Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellCompositePreviewTests.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailContainer.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift

Comment on lines +90 to +100
/// Surface IDs whose next window attach must NOT grab the keyboard.
///
/// A surface in this set mounts with autofocus disabled; the entry is
/// cleared once that surface has appeared and consumed the suppression
/// (``consumeTerminalAutoFocusSuppression(for:)``). Ownership lives here,
/// next to selection and terminal creation, rather than in the view, so the
/// create path can mark the *exact* new terminal id the instant it becomes
/// the selection. A freshly created terminal therefore never steals the
/// keyboard, while push-notification navigation (``selectTerminal(_:)``) is
/// intentionally left out of the set and allowed to autofocus.
private var terminalAutoFocusSuppressedSurfaceIDs: Set<String> = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clear pending autofocus suppression when shell state is reset.

terminalAutoFocusSuppressedSurfaceIDs is now long-lived state, but signOut() and clearRemoteConnectionContext() below rebuild the session without emptying it. If a chrome-driven selection is queued and the surface never reaches consumeTerminalAutoFocusSuppression(for:) before teardown, that stale surface ID survives into the next connection and suppresses autofocus for a later mount too. Reset this set alongside the rest of the selection/connection state, or scope it to the current connectionGeneration.

🤖 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/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift`
around lines 90 - 100, terminalAutoFocusSuppressedSurfaceIDs persists across
session resets and can suppress autofocus in a new connection; update signOut()
and clearRemoteConnectionContext() to clear
terminalAutoFocusSuppressedSurfaceIDs when rebuilding session state OR modify
logic that uses terminalAutoFocusSuppressedSurfaceIDs to include the current
connectionGeneration so suppressed IDs are only valid for that generation (e.g.,
pair the set with connectionGeneration or check connectionGeneration when
consuming/consulting the set from consumeTerminalAutoFocusSuppression(for:)).
Ensure you reference and clear/update terminalAutoFocusSuppressedSurfaceIDs
inside the same reset code paths (signOut() and clearRemoteConnectionContext())
so stale IDs cannot carry over.

main evolved the iOS shell heavily (multi-Mac host switcher, terminal toolbar
redesign, rename/pin workspaces); none of it had the autofocus-suppression
feature this branch introduces. Resolved:
- WorkspaceShellView.swift: took main's version (rename/pin closures, toolbar);
  the branch's only net change here was an inert createWorkspaceFromSplitList
  wrapper, now unneeded since suppression lives in the store.
- MobileSettingsView.swift: kept both new @State (branch's notificationsEnabled
  toggle mirror + main's showingHostPicker).
WorkspaceDetailView/Container, GhosttySurfaceRepresentable, and the store
auto-merged: the store-based suppression consumption sits alongside main's
toolbar changes with no overlap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen merged commit b7e9d67 into main Jun 6, 2026
19 of 25 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 3b7fc5bf Deployed Jun 6, 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