Skip to content

Fix Settings sign-in by using the default browser - #4974

Closed
austinywang wants to merge 14 commits into
mainfrom
issue-3617-signin-fix
Closed

austinywang wants to merge 14 commits into
mainfrom
issue-3617-signin-fix

Conversation

@austinywang

@austinywang austinywang commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Closes #3617.

Summary

This consolidates the durable pieces of the existing issue-3617 work and changes the Settings sign-in architecture from an embedded ASWebAuthenticationSession sheet to the system default browser plus cmux's existing native auth callback.

  • Settings > Account > Sign In now opens AuthEnvironment.signInURL() through NSWorkspace, so Arc/Chrome/Firefox/etc. are honored instead of always using Safari's AuthenticationServices web stack.
  • The native callback URL includes a per-attempt state; stale callbacks from an older or signed-out attempt are ignored instead of reauthenticating unexpectedly.
  • The app no longer registers itself as an http/https URL handler, matching the LaunchServices loop fix from Drop vestigial http/https URL handler so ASWebAuthenticationSession works against http://localhost #3650.
  • The after-sign-in handler now requires both a non-empty refresh token and a non-empty decoded access token before emitting a native handoff, matching the defensive web-side fix from Require valid accessToken for native app handoff to prevent auth crashes #3634.
  • The browser handoff page automatically attempts the cmux*://auth-callback return while keeping the manual Return to cmux link as a fallback.
  • Settings now surfaces localized sign-in errors instead of only writing auth failures to logs.

Why this architecture

I weighed the two options called out in the issue:

This PR chooses the system-browser architecture. It also keeps the LaunchServices and token-shape fixes because they are still valid defense-in-depth for callback reliability.

Related work credited

Paid-user reproduction folded in

A paid user reported Arc as their macOS default browser, but cmux Settings sign-in opened Safari and then auto-closed before sign-in. They completed sign-in only by capturing the auth URL from LaunchServices logs and pasting it into Arc manually. This PR makes that workaround the normal path: cmux opens the auth URL in the default browser directly, then completes via the native callback.

Regression coverage

The first commit adds a failing Swift Testing regression for the new invariant: AuthManager.beginSignIn() must hand the generated sign-in URL to the injected browser opener. That fails on the old embedded-session implementation and passes with this change.

This PR also adds Bun coverage for the web native-handoff guard: native schemes are recognized, callback targets are coerced to auth-callback, callback state is preserved, and empty access tokens do not emit native handoffs.

Verification

Local static checks only, per repo instruction not to run local tests or builds:

  • python3 scripts/normalize-pbxproj.py --check cmux.xcodeproj/project.pbxproj
  • python3 -m json.tool Resources/Localizable.xcstrings >/dev/null
  • git diff --check

I did not run ./scripts/reload.sh, xcodebuild, or local tests. I did not use cloud-mac because real sign-in completion needs live credentials; the fix is verified by code-path ownership and regression coverage rather than a credentialed recording.


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


Note

High Risk
Changes authentication UX and callback validation (token handoff, state matching, URL handler registration); mistakes could block sign-in or accept wrong callbacks.

Overview
Settings account sign-in no longer uses an embedded ASWebAuthenticationSession sheet. It opens the Stack sign-in URL in the system default browser (via NSWorkspace), then completes when the app receives a cmux*://auth-callback deep link.

Each attempt gets a unique state on the callback URL; AuthManager only accepts callbacks that match the active attempt, and ignores stale, sign-out-cancelled, or stateless callbacks. Sign-in can time out (default 5 minutes) with a new localized “Sign in timed out” message shown in Settings.

Plumbing: Info.plist drops http/https URL-handler registration so LaunchServices is less likely to route auth URLs back into cmux. The web after-sign-in handler only deep-links when both tokens are present, normalizes native return URLs to auth-callback, preserves state, and auto-redirects via OpenNativeClient.

Tests: Swift tests for external-browser sign-in and timeout/stale callbacks; Bun tests for native handoff helpers; existing sign-out race tests updated for state.

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


Summary by cubic

Settings sign-in now opens in the default browser and completes via cmux*://auth-callback. Each attempt uses a unique state, times out after 5 minutes with a localized error in Settings, and ignores stale, stateless, or cancelled callbacks (fixes #3617).

  • Bug Fixes
    • Use the system browser via NSWorkspace with a new DefaultBrowserAuthSessionFactory (wired in MacAuthComposition); build the URL with AuthEnvironment.signInURL(callbackState:); keep loading scoped to the active attempt; show lastSignInError on timeout/settle.
    • Validate callbacks strictly: require a matching active state; ignore stateless callbacks, stale callbacks (including after timeout), and callbacks from sign‑out–cancelled attempts; reject idle stateless callbacks.
    • Remove http/https URL handler from Info.plist and defensively avoid launching cmux as the browser to prevent LaunchServices loops.
    • Web after‑sign‑in only deep‑links when both tokens are present, coerces native return targets to .../auth-callback, preserves state, and auto‑redirects via OpenNativeClient; tests add DefaultBrowserAuthSessionFactoryTests (success/failure) and cover external‑browser opener, timeouts, and stale/stateless/cancelled callbacks.

Written for commit 844a117. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Sign-in now opens in the user’s default browser, matches callbacks to the active sign-in attempt, and times out with a user-facing "sign in timed out" message.
  • UI

    • Settings shows the last sign-in error beneath the account row when present.
  • Localization

    • Added localized "sign in timed out" translations for many locales.
  • New Features

    • Improved native-app handoff and callback handling for after-sign-in flows.
  • Tests

    • Added tests for external browser sign-in and native handoff logic.

Review Change Stack

@vercel

vercel Bot commented May 29, 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 23, 2026 3:41pm
cmux-staging Building Building Preview, Comment Jun 23, 2026 3:41pm

@coderabbitai

coderabbitai Bot commented May 29, 2026 •

Copy link
Copy Markdown

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

Browser sign-in now uses per-attempt state, surfaces localized sign-in errors, adds a sign-in timeout, centralizes Settings button logic and error display, removes web URL scheme registration, extracts native handoff helpers, refactors after-sign-in logic, adds client-side native redirect, and introduces tests and project updates.

Changes

Auth error surface and state tracking

Layer / File(s) Summary
Callback state & callback URL construction
Sources/Auth/AuthCallbackRouter.swift, Sources/Auth/AuthEnvironment.swift
Adds optional state to callback payload, extracts/trim state from callback URLs, and builds callback URLs with optional state query parameter.
AuthManager: attempt state, errors, and callback handling
Sources/Auth/AuthManager.swift
Tracks per-attempt browser sign-in state, adds lastSignInError and signInTimedOut, refactors beginSignIn/callback handling to validate state, and adds diagnostics/timestamp helpers.
Settings UI and localization
Sources/cmuxApp.swift, Resources/Localizable.xcstrings, Resources/Info.plist, CHANGELOG.md
Displays localized last-sign-in error under the Sign In button, centralizes button disabled logic, adds settings.account.error.signInTimedOut translations, removes the web URL scheme from Info.plist, and documents the fix in CHANGELOG.
Tests and project registration
cmuxTests/*, cmux.xcodeproj/project.pbxproj
Adds browser sign-in initiation and timeout/staleness tests, in-memory test token store and client stub, and registers the new test in the Xcode project.

Native handoff module and web refactor

Layer / File(s) Summary
Native handoff utilities
web/app/handler/after-sign-in/native-handoff.ts
New helpers to detect native return schemes, convert return-to URLs into auth-callback deep links (preserving state), and gate emission on token presence.
After-sign-in handler refactor
web/app/handler/after-sign-in/page.tsx
Replaces inline native-scheme logic with helpers, changes buildNativeHref to return null on missing/invalid inputs, rebuilds access cookie payload, and emits native handoff only when helpers agree.
OpenNativeClient auto-redirect
web/app/handler/after-sign-in/OpenNativeClient.tsx
Adds useEffect redirect (window.location.assign) to perform client-side deep-link navigation when href changes.
Native handoff tests
web/tests/native-handoff.test.ts
Adds Bun tests covering scheme detection, callback conversion (state preserved), and token-presence gating for handoff emission.

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

Poem:

"A rabbit tapped the code with care,
Hopped a state into the air,
Callbacks matched and errors shown,
Native links now find their home,
🍃 hop, sign-in — all is fair."


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces Task.sleep in production code (Sources/Auth/AuthManager.swift) for timeouts, violating swift-blocking-runtime.md rule which treats Task.sleep as default failure. Replace Task.sleep with proper async timing abstractions: use DispatchSourceTimer, Timer, or real callback signals instead of sleep in scheduleBrowserSignInTimeout and waitForSignInSettled.
Cmux Architecture Rethink ❌ Error PR introduces Task.sleep timeout + multiple mutable flags (activeBrowserSignInAttemptID/State) papering over shared-state race between external browser and callback handler from different contexts. Use unified state machine actor for browser signin lifecycle instead of Task.sleep delays and scattered mutable ID/state guard flags.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift @Concurrent ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: Settings sign-in now uses the default browser instead of ASWebAuthenticationSession.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #3617: surfaces user-visible sign-in errors via lastSignInError and userFacingSignInErrorMessage, implements per-attempt state matching to ignore stale callbacks, removes http/https URL handler registration, opens sign-in in default browser, adds auto-redirect to native deep link, and includes regression test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #3617 and its related issues (#3650, #3634, #3624, #1078): auth flow refactoring, error surfacing, state management, URL handler removal, web handoff logic, and regression tests. No unrelated changes detected.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation violations. @Published property correctly on @MainActor class; Sendable struct with proper types; service protocols properly scoped.
Cmux No Hacky Sleeps ✅ Passed PR introduces no sleep/timer/polling patterns in TypeScript/JavaScript production code; uses proper event-driven patterns (React useEffect, async/await, pure functions).
Cmux Algorithmic Complexity ✅ Passed All changes are O(1) state management or simple URL parsing with fixed-size components. No nested scans, per-target rescans, or unbounded collection rebuilds in production code.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns. New @Published property in AuthManager is for SwiftUI boundary (allowed). Tasks properly stored, not fire-and-forget. Tests clean.
Cmux Swift File And Package Boundaries ✅ Passed PR satisfies Swift file-boundary rules: no oversized new files, AuthManager +150 lines (below 250 threshold), no mixed responsibilities, focused bug-fix refactor appropriate for app target.
Cmux Swift Logging ✅ Passed Auth-related Swift files contain no NSLog, print, debugPrint, or dump statements added in the diff, complying with swift-logging.md rules.
Cmux User-Facing Error Privacy ✅ Passed All user-facing error messages comply with privacy rule: new timeout message is generic, and no raw errors, tokens, credentials, or sensitive details leak to users.
Cmux Full Internationalization ✅ Passed PR complies with i18n rules: Swift errors use String(localized:); new xcstrings entry has all 20 locales; web logic is non-UI; pre-existing web strings unchanged.
Cmux Swiftui State Layout ✅ Passed Touches existing legacy ObservableObject (AuthManager since #3027), adding @Published incidentally. No render-time mutations, no GeometryReader, no new SwiftUI state patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR removes ASWebAuthenticationSession sheet and adds error display as Text element in existing Settings view; no new NSWindow/NSPanel/NSWindowController or SwiftUI Window/WindowGroup created.
Description check ✅ Passed The pull request description comprehensively covers all required sections: a clear summary of what changed and why, detailed testing approach, architectural rationale, and a complete verification checklist.
✨ 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 issue-3617-signin-fix

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.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 29, 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: 6

Caution

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

⚠️ Outside diff range comments (1)
Sources/Auth/AuthManager.swift (1)

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

Swift 6 concurrency warning: main actor isolated property accessed from nonisolated context.

The pipeline flagged a Swift 6 concurrency warning: logTimestampFormatter is main actor-isolated but referenced from the nonisolated static func authLog. While not blocking, this may cause issues when strict concurrency checking is enabled.

🔧 Possible fix

Mark logTimestampFormatter as nonisolated:

-private static let logTimestampFormatter: ISO8601DateFormatter = {
+private nonisolated static let logTimestampFormatter: ISO8601DateFormatter = {
     let f = ISO8601DateFormatter()
     f.formatOptions = [.withInternetDateTime]
     return f
 }()

ISO8601DateFormatter is thread-safe, so this annotation is safe.

🤖 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 `@Sources/Auth/AuthManager.swift` at line 743, The static authLog function
references the main-actor isolated property logTimestampFormatter causing a
Swift 6 concurrency warning; mark the static property logTimestampFormatter as
nonisolated (or otherwise annotate it to be safely accessible from nonisolated
contexts) so authLog can call Self.logTimestampFormatter.string(from:) without
main-actor isolation conflicts; ensure the change is applied to the declaration
of logTimestampFormatter (not authLog) and keep the formatter as an
ISO8601DateFormatter since it is thread-safe.
🤖 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 `@CHANGELOG.md`:
- Around line 5-9: Update the Unreleased changelog entry under "Fixed" to remove
implementation details like "auth callback" and "native auth handoffs" and
instead describe user-visible behavior: state that Account sign-in now opens in
the user's default browser (replacing the embedded Safari sheet) and that
sign-in errors (e.g., timeouts) are now surfaced to users with localized error
messages; keep the entry concise and product-focused while referencing the
existing bullet "Open Settings > Account sign-in" to replace the current
technical phrasing.

In `@Sources/Auth/AuthManager.swift`:
- Around line 437-440: The defer block currently uses "guard let
browserSignInAttemptID else { return }" which is illegal because defer cannot
transfer control; change it to a conditional binding: inside the defer use "if
let browserSignInAttemptID = browserSignInAttemptID {
finishBrowserSignInAttempt(browserSignInAttemptID) }" (i.e., replace the guard
with an if-let and call finishBrowserSignInAttempt(browserSignInAttemptID) only
when non-nil).

In `@Sources/cmuxApp.swift`:
- Around line 8437-8439: The buttonIsDisabled computed property currently only
disables the button during session restore or when authenticated+loading,
leaving it enabled during sign-in loading and allowing duplicate beginSignIn()
calls; change the condition to disable whenever any auth operation is in-flight
by using authManager.isRestoringSession || authManager.isLoading (i.e., remove
the dependency on isAuthenticated) so the UI prevents additional beginSignIn()
invocations while loading.
- Around line 8388-8392: The code is rendering raw
lastSignInError?.localizedDescription which may expose internal/upstream
details; update the UI to use a sanitized, user-facing message provided by
AuthManager (e.g., expose a computed property like userFacingMessage or
userFacingErrorMessage on AuthManager) instead of localizedDescription; if such
a property doesn't exist, add a mapping in AuthManager that converts known error
types to localized, user-safe strings and returns a generic fallback for unknown
errors, then change the SwiftUI view to display authManager.userFacingMessage
(with the same font/color) rather than lastSignInError?.localizedDescription.

In `@web/app/handler/after-sign-in/page.tsx`:
- Around line 130-137: The isNativeReturnScheme(nativeReturnTo) check is
redundant because nativeCallbackHref is null for non-native schemes; remove that
guard from the if condition and simplify the block to rely on
shouldEmitNativeHandoff({ refreshToken, accessToken }) && nativeCallbackHref,
keeping the existing buildNativeHref(nativeCallbackHref, refreshToken,
accessCookie) call and the OpenNativeClient return path (functions/identifiers
to locate: shouldEmitNativeHandoff, isNativeReturnScheme, nativeCallbackHref,
buildNativeHref, OpenNativeClient).
- Around line 149-161: Remove the redundant fallback block that computes
fallbackReturnTo via nativeAuthCallbackForReturnTo and the subsequent if that
calls buildNativeHref and returns <OpenNativeClient ... />; the same
nativeAuthCallbackForReturnTo value is already used in the primary native
handoff path guarded by shouldEmitNativeHandoff and buildNativeHref, so delete
the fallbackReturnTo variable and the entire fallback if/return (references:
fallbackReturnTo, nativeAuthCallbackForReturnTo, shouldEmitNativeHandoff,
buildNativeHref, OpenNativeClient, accessCookie) to clarify control flow and
avoid dead code.

---

Outside diff comments:
In `@Sources/Auth/AuthManager.swift`:
- Line 743: The static authLog function references the main-actor isolated
property logTimestampFormatter causing a Swift 6 concurrency warning; mark the
static property logTimestampFormatter as nonisolated (or otherwise annotate it
to be safely accessible from nonisolated contexts) so authLog can call
Self.logTimestampFormatter.string(from:) without main-actor isolation conflicts;
ensure the change is applied to the declaration of logTimestampFormatter (not
authLog) and keep the formatter as an ISO8601DateFormatter since it is
thread-safe.
🪄 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: fcab30d0-21b1-478e-b0b3-183c4852a5a0

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa6aef and c92e4b9.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • Resources/Info.plist
  • Resources/Localizable.xcstrings
  • Sources/Auth/AuthCallbackRouter.swift
  • Sources/Auth/AuthEnvironment.swift
  • Sources/Auth/AuthManager.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AuthManagerExternalBrowserSignInTests.swift
  • web/app/handler/after-sign-in/OpenNativeClient.tsx
  • web/app/handler/after-sign-in/native-handoff.ts
  • web/app/handler/after-sign-in/page.tsx
  • web/tests/native-handoff.test.ts
💤 Files with no reviewable changes (1)
  • Resources/Info.plist

Comment thread CHANGELOG.md Outdated
Comment thread Sources/Auth/AuthManager.swift Outdated
Comment thread Sources/cmuxApp.swift Outdated
Comment thread Sources/cmuxApp.swift Outdated
Comment thread web/app/handler/after-sign-in/page.tsx Outdated
Comment thread web/app/handler/after-sign-in/page.tsx Outdated
@greptile-apps

greptile-apps Bot commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the embedded ASWebAuthenticationSession sheet with NSWorkspace.shared.open() for Settings sign-in, so Arc, Chrome, Firefox, and other user-chosen browsers are honored. Sign-in completes through the existing native cmux*://auth-callback URL scheme with a per-attempt state guard; the embedded sheet lifecycle is gone.

  • DefaultBrowserAuthSessionFactory implements HostBrowserAuthSessionFactory by opening the sign-in URL via NSWorkspace and intentionally dropping the completion parameter — the terminal result is delivered through handleCallbackURL(_:) or the existing timeout/cancel paths in HostBrowserSignInFlow, so the continuation is always resumed.
  • Info.plist removes the http/https CFBundleURLTypes entry, preventing cmux from being registered as a web browser and eliminating the LaunchServices routing loop that was the root cause of Settings → Sign In… opens auth sheet that closes immediately (no page rendered) #3617.
  • Two unit tests verify that the opener receives the correct URL, that completion is never invoked (the correct invariant for the external-callback architecture), and that a failed opener propagates false from start().

Confidence Score: 5/5

Safe to merge — the changed files are a clean, well-scoped replacement of ASWebAuthenticationSession with NSWorkspace.open, and the continuation lifecycle in HostBrowserSignInFlow correctly handles the dropped completion through external-callback, timeout, and cancel paths.

The new DefaultBrowserAuthSessionFactory is small and correctly satisfies the HostBrowserAuthSessionFactory contract for the external-callback architecture: start() propagates the opener's success/failure, cancel() is a documented no-op, and the dropped completion closure does not leak the withCheckedContinuation because HostBrowserSignInFlow's three other resumption paths (handleCallbackURL, timeout, cancelActiveAttempt) always cover it. Actor isolation is correct throughout. The Info.plist change is precise. No new issues were found in the diff; prior threads already captured the outstanding concerns in other parts of the PR.

No files in this diff require special attention. The HostBrowserSignInFlow (not in this diff) contains the state-matching and race-guard logic that was already flagged in earlier review threads.

Important Files Changed

Filename Overview
Sources/Auth/DefaultBrowserAuthSessionFactory.swift New factory implementing HostBrowserAuthSessionFactory via NSWorkspace.open; completion is intentionally dropped since the result is delivered through the app's native URL handler — continuation lifecycle is sound.
Sources/Auth/MacAuthComposition.swift Swaps ASWebBrowserAuthSessionFactory for DefaultBrowserAuthSessionFactory; anchor is still used by AuthCoordinator so the construction is clean.
Resources/Info.plist Removes the http/https CFBundleURLTypes entry so cmux stops declaring itself a web browser, eliminating the LaunchServices routing loop.
cmuxTests/DefaultBrowserAuthSessionFactoryTests.swift Two tests: one confirms the opener receives the sign-in URL and that completion is never invoked (correct for the external-callback architecture); the other confirms start() propagates opener failure. Coverage is adequate for the new type.
cmux.xcodeproj/project.pbxproj Adds DefaultBrowserAuthSessionFactory.swift to the app target and DefaultBrowserAuthSessionFactoryTests.swift to the test target; project structure is correct.
CHANGELOG.md Changelog entry for the default-browser sign-in change; no PR number included but prose is clear.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    actor User
    participant Settings
    participant HBSF as HostBrowserSignInFlow
    participant Factory as DefaultBrowserAuthSessionFactory
    participant Session as DefaultBrowserAuthSession
    participant NSW as NSWorkspace
    participant Browser as Default Browser
    participant AppDelegate

    User->>Settings: Sign In
    Settings->>HBSF: beginSignIn()
    HBSF->>Factory: makeSession(signInURL, completion_dropped)
    Factory->>Session: init(signInURL, opener)
    HBSF->>Session: start()
    Session->>NSW: open(signInURL)
    NSW->>Browser: launch sign-in URL
    Browser->>User: sign-in page
    User->>Browser: completes sign-in
    Browser->>AppDelegate: "cmux://auth-callback?cmux_auth_state=UUID"
    AppDelegate->>HBSF: handleCallbackURL
    HBSF->>HBSF: validate state equals activeCallbackState
    HBSF->>HBSF: seed tokens and completeExternalSignIn
    HBSF->>Settings: "isSigningIn = false"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    actor User
    participant Settings
    participant HBSF as HostBrowserSignInFlow
    participant Factory as DefaultBrowserAuthSessionFactory
    participant Session as DefaultBrowserAuthSession
    participant NSW as NSWorkspace
    participant Browser as Default Browser
    participant AppDelegate

    User->>Settings: Sign In
    Settings->>HBSF: beginSignIn()
    HBSF->>Factory: makeSession(signInURL, completion_dropped)
    Factory->>Session: init(signInURL, opener)
    HBSF->>Session: start()
    Session->>NSW: open(signInURL)
    NSW->>Browser: launch sign-in URL
    Browser->>User: sign-in page
    User->>Browser: completes sign-in
    Browser->>AppDelegate: "cmux://auth-callback?cmux_auth_state=UUID"
    AppDelegate->>HBSF: handleCallbackURL
    HBSF->>HBSF: validate state equals activeCallbackState
    HBSF->>HBSF: seed tokens and completeExternalSignIn
    HBSF->>Settings: "isSigningIn = false"
Loading

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

Comment on lines 120943 to 120977
}
}
},
"settings.account.error.signInTimedOut": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sign in timed out. Try again."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "サインインがタイムアウトしました。もう一度お試しください。"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Час очікування входу минув. Спробуйте ще раз."
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "로그인 시간이 초과되었습니다. 다시 시도하세요."
}
}
}
},
"remote.state.connected.vmNoProxy": {
"extractionState": "manual",
"localizations": {

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 Missing translations for 16 of 20 supported locales

The new settings.account.error.signInTimedOut string is only translated into en, ja, uk, and ko. The catalog already supports ar, bs, da, de, es, fr, it, km, nb, pl, pt-BR, ru, th, tr, zh-Hans, and zh-Hant — users on any of those locales will see the raw English fallback "Sign in timed out. Try again." when the timeout fires. The settings.account.error.missingRefreshToken and settings.account.error.invalidCallback strings added in companion PRs each carry all 20 translations; this one is the odd one out.

Rule Used: Flag production user-facing text that is not fully... (source)

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

Comment thread Sources/Auth/AuthManager.swift Outdated
Comment on lines +226 to +231
authLog("auth.browserSignIn begin url=\(Self.redactedURLDescription(signInURL))")
urlOpener(signInURL)
guard isCurrentBrowserSignInAttempt(attemptID) else {
return
}
session.presentationContextProvider = AuthPresentationContext.shared
session.prefersEphemeralWebBrowserSession = false
}

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 The guard isCurrentBrowserSignInAttempt(attemptID) check after urlOpener(signInURL) is unreachable dead code. urlOpener is a synchronous (URL) -> Void closure; because beginSignIn() runs on the main actor, no other actor-isolated code can execute during the call, so activeBrowserSignInAttemptID cannot change while urlOpener is running. Whether the guard passes or fails, there is no following work — the function ends in both branches. Remove it to avoid misleading future readers.

Suggested change
authLog("auth.browserSignIn begin url=\(Self.redactedURLDescription(signInURL))")
urlOpener(signInURL)
guard isCurrentBrowserSignInAttempt(attemptID) else {
return
}
session.presentationContextProvider = AuthPresentationContext.shared
session.prefersEphemeralWebBrowserSession = false
}
authLog("auth.browserSignIn begin url=\(Self.redactedURLDescription(signInURL))")
urlOpener(signInURL)
}

Comment thread web/app/handler/after-sign-in/page.tsx Outdated
Comment on lines +149 to +159
// Fallback: try native app only when we can preserve the requested native scheme.
const fallbackReturnTo = nativeAuthCallbackForReturnTo(nativeReturnTo);
if (
shouldEmitNativeHandoff({ refreshToken, accessToken }) &&
fallbackReturnTo
) {
const fallback = buildNativeHref(
fallbackReturnTo,
refreshToken,
accessCookie,
);

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 Fallback native-handoff block is unreachable

The fallback's conditions — shouldEmitNativeHandoff({ refreshToken, accessToken }) && fallbackReturnTo — are identical to the reduced conditions of the primary block above (lines 130–137). When both tokens are present and nativeReturnTo is a native scheme, the primary block's buildNativeHref call will always succeed (because accessCookie is guaranteed non-empty by the JSON.stringify([refreshToken, accessToken]) assignment on line 117 when both tokens are truthy). So the primary block always returns when its conditions hold, and the fallback's conditions can never be independently true. This block is dead code and can be removed.

Comment thread Sources/Auth/AuthManager.swift Outdated
Comment thread web/app/handler/after-sign-in/page.tsx 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.

Caution

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

⚠️ Outside diff range comments (2)
cmuxTests/AuthManagerExternalBrowserSignInTests.swift (1)

31-33: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider moving signOut() to cleanup rather than test body.

Calling signOut() at Line 33 before the assertions (Lines 35-45) is unusual ordering. If this is cleanup, use defer or a test teardown phase; if it's part of the test flow, add assertions to verify sign-out behavior.

🤖 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 `@cmuxTests/AuthManagerExternalBrowserSignInTests.swift` around lines 31 - 33,
The call to manager.signOut() is placed before the post-beginSignIn assertions,
which mixes cleanup with test verification; either move the signOut() call into
test teardown (use defer { try? await manager.signOut() } or the test class
tearDown/async tearDown) to ensure assertions about manager.isLoading and
related state run unmodified, or keep signOut() in the flow but add explicit
assertions verifying sign-out behavior (e.g., that isAuthenticated == false)
after calling manager.signOut(); locate the calls around manager.beginSignIn(),
manager.isLoading and manager.signOut() in
AuthManagerExternalBrowserSignInTests.swift to implement the change.
Sources/Auth/AuthManager.swift (1)

33-37: ⚠️ Potential issue | 🟠 Major

Add missing localized strings for settings.account.error.signInTimedOut for all supported locales.

Resources/Localizable.xcstrings only provides this key for en, ja, ko, and uk; missing: ar, bs, da, de, es, fr, it, km, no, pl, pt-BR, ru, th, tr, zh-CN, zh-TW.

🤖 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 `@Sources/Auth/AuthManager.swift` around lines 33 - 37, The localized string
key "settings.account.error.signInTimedOut" used in the AuthManager.swift case
.signInTimedOut is only present for en/ja/ko/uk; add this key with appropriate
translated values to Resources/Localizable.xcstrings for the missing locales
(ar, bs, da, de, es, fr, it, km, no, pl, pt-BR, ru, th, tr, zh-CN, zh-TW).
Ensure each entry uses the same key exactly
("settings.account.error.signInTimedOut") and follows the existing .xcstrings
format and context/notes conventions so String(localized:) resolves correctly at
runtime. Validate by building and testing that String(localized:
"settings.account.error.signInTimedOut", defaultValue: ...) returns the
localized text for each locale.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cmuxTests/AuthManagerExternalBrowserSignInTests.swift`:
- Around line 31-33: The call to manager.signOut() is placed before the
post-beginSignIn assertions, which mixes cleanup with test verification; either
move the signOut() call into test teardown (use defer { try? await
manager.signOut() } or the test class tearDown/async tearDown) to ensure
assertions about manager.isLoading and related state run unmodified, or keep
signOut() in the flow but add explicit assertions verifying sign-out behavior
(e.g., that isAuthenticated == false) after calling manager.signOut(); locate
the calls around manager.beginSignIn(), manager.isLoading and manager.signOut()
in AuthManagerExternalBrowserSignInTests.swift to implement the change.

In `@Sources/Auth/AuthManager.swift`:
- Around line 33-37: The localized string key
"settings.account.error.signInTimedOut" used in the AuthManager.swift case
.signInTimedOut is only present for en/ja/ko/uk; add this key with appropriate
translated values to Resources/Localizable.xcstrings for the missing locales
(ar, bs, da, de, es, fr, it, km, no, pl, pt-BR, ru, th, tr, zh-CN, zh-TW).
Ensure each entry uses the same key exactly
("settings.account.error.signInTimedOut") and follows the existing .xcstrings
format and context/notes conventions so String(localized:) resolves correctly at
runtime. Validate by building and testing that String(localized:
"settings.account.error.signInTimedOut", defaultValue: ...) returns the
localized text for each locale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 860c0204-7f7e-41e7-ab88-0c56e0db8691

📥 Commits

Reviewing files that changed from the base of the PR and between c92e4b9 and b53968a.

📒 Files selected for processing (2)
  • Sources/Auth/AuthManager.swift
  • cmuxTests/AuthManagerExternalBrowserSignInTests.swift

Comment thread Sources/Auth/AuthManager.swift Outdated
Comment on lines +421 to +425
if let activeState = activeBrowserSignInAttemptState,
callbackState != activeState {
authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil")")
return
}

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 State-bearing callbacks bypass staleness enforcement after an attempt has concluded. The current guard only fires when activeBrowserSignInAttemptState is non-nil — if the sign-in attempt already finished (or timed out) and then the user signed out without a pending attempt, cancelBrowserSignInForSignOut records nothing, both activeBrowserSignInAttemptState and signOutCancelledBrowserSignInAttemptState are nil, and a stale callback with the original state UUID re-authenticates the user. Replacing the if let opt-in with an unconditional guard equality check closes the gap: nil == nil still passes legacy stateless callbacks while "uuid-old" == nil is rejected.

Suggested change
if let activeState = activeBrowserSignInAttemptState,
callbackState != activeState {
authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil")")
return
}
guard callbackState == activeBrowserSignInAttemptState else {
authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil") activeState=\(activeBrowserSignInAttemptState ?? "nil")")
return
}

Comment thread Sources/Auth/AuthManager.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.

Caution

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

⚠️ Outside diff range comments (1)
Sources/Auth/AuthManager.swift (1)

431-434: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require active state to be non-nil before matching.

The guard callbackState == activeBrowserSignInAttemptState passes when both are nil, allowing a callback (possibly from a pre-state-tracking flow or malformed URL) to be processed when no sign-in attempt is active. The intent is to ignore callbacks that don't match an active attempt's state.

🔧 Proposed fix
-    guard callbackState == activeBrowserSignInAttemptState else {
+    guard let activeState = activeBrowserSignInAttemptState,
+          callbackState == activeState else {
         authLog("auth.browserSignIn ignored stale callback state=\(callbackState ?? "nil") activeState=\(activeBrowserSignInAttemptState ?? "nil")")
         return
     }
🤖 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 `@Sources/Auth/AuthManager.swift` around lines 431 - 434, The guard currently
allows both callbackState and activeBrowserSignInAttemptState to be nil and thus
proceeds; update the condition in the AuthManager logic so that it requires an
activeBrowserSignInAttemptState to be non-nil before matching (e.g., check
activeBrowserSignInAttemptState != nil && callbackState ==
activeBrowserSignInAttemptState) so stale or unsolicited callbacks are ignored;
locate the check that uses callbackState and activeBrowserSignInAttemptState in
AuthManager.swift and modify that guard accordingly, keeping the existing
authLog and return behavior when the guard fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Auth/AuthManager.swift`:
- Around line 431-434: The guard currently allows both callbackState and
activeBrowserSignInAttemptState to be nil and thus proceeds; update the
condition in the AuthManager logic so that it requires an
activeBrowserSignInAttemptState to be non-nil before matching (e.g., check
activeBrowserSignInAttemptState != nil && callbackState ==
activeBrowserSignInAttemptState) so stale or unsolicited callbacks are ignored;
locate the check that uses callbackState and activeBrowserSignInAttemptState in
AuthManager.swift and modify that guard accordingly, keeping the existing
authLog and return behavior when the guard fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2f4bfa7-ddca-4fb2-8312-4dbb93902087

📥 Commits

Reviewing files that changed from the base of the PR and between 0906d87 and 6775e2c.

📒 Files selected for processing (1)
  • Sources/Auth/AuthManager.swift

Comment thread Sources/Auth/AuthManager.swift Outdated
Comment thread Sources/Auth/AuthManager.swift Outdated

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

Comment thread Sources/Auth/AuthManager.swift Outdated
@kiankyars

Copy link
Copy Markdown

What's the status on this getting merged?

# Conflicts:
#	CHANGELOG.md
#	Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/AuthCallbackRouter.swift
#	Sources/Auth/AuthEnvironment.swift
#	Sources/Auth/AuthManager.swift
#	Sources/cmuxApp.swift
#	cmux.xcodeproj/project.pbxproj
#	cmuxTests/SocketControlPasswordStoreTests.swift
#	web/app/handler/after-sign-in/OpenNativeClient.tsx
#	web/app/handler/after-sign-in/page.tsx
@lawrencecchen
lawrencecchen dismissed coderabbitai[bot]’s stale review June 23, 2026 11:27

Stale CodeRabbit review on pre-revival commit c92e4b9. The PR was merged with current main; the current diff is six files and CodeRabbit passed/skipped on the fresh head 844a117.

@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 — 844a1178 Deployed Jun 23, 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.

Settings → Sign In… opens auth sheet that closes immediately (no page rendered)

4 participants