Skip to content

Fix browser URL bar stealing focus on tab switch - #4623

Merged
austinywang merged 4 commits into
mainfrom
issue-4616-browser-url-bar-focus-steal
May 23, 2026
Merged

austinywang merged 4 commits into
mainfrom
issue-4616-browser-url-bar-focus-steal

Conversation

@austinywang

@austinywang austinywang commented May 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4616.

Summary

  • Reproduced the tab-switch focus bug in the shared browser panel state path: a panel with a real remembered URL could still be classified as a new tab while its WebView was not rendered yet.
  • Tightened the new-tab/blank-content decision to use the panel's known omnibar URL, not only the transient rendered WebView state.
  • Updated browser view autofocus/empty-state checks so WebKit attach/discard timing does not redirect focus to the URL bar for a real page.

Regression coverage

  • Added a failing-first regression test for a deferred/restored browser URL that must not be treated as a new tab.

Local verification

  • Not run locally per task instruction; CI is the verification path for this PR.

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


Note

Medium Risk
Medium risk because it changes browser panel lifecycle state and blank/new-tab detection used to drive focus and empty-state UI, which could affect navigation and tab UX across multiple code paths.

Overview
Prevents the URL bar from stealing focus during tab switches by refining what counts as a real blank/new-tab state when a WKWebView hasn’t been attached yet.

The panel now distinguishes .deferredURL (a known/restored URL with deferred rendering) from .newTab, and BrowserPanelView switches autofocus and empty-state overlay checks to use the model’s preferredURLStringForOmnibar() / panel.isShowingNewTabPage rather than about:blank/WebView URL state.

Adds regression tests ensuring deferred initial URLs are not treated as new tabs and that lifecycle state reflects .deferredURL until rendering begins.

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


Summary by cubic

Fixes #4616. Stops the URL bar from stealing focus on tab switch by treating deferred/restored URLs as real content and introducing a deferredURL lifecycle state.

  • Bug Fixes
    • A tab is only a new tab when no WebView is rendered and there’s no known omnibar URL; deferred/restored URLs are classified as deferredURL, not new tabs.
    • Replaced about:blank/WebView URL checks with a model-based blank check and used isShowingNewTabPage for the empty-state overlay; autofocus now skips when content isn’t truly blank so pages keep focus.
    • Added tests for deferred initial URLs and lifecycle to prevent regressions.

Written for commit 3b7be1b. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • More precise detection of the new-tab/empty state so UI elements and focus behave correctly when a web view is not rendered.
    • Blank-browser handling updated to rely on panel state, preventing incorrect empty-state overlays and unwanted omnibar autofocus.
  • Tests

    • Added tests validating lifecycle and initialization when an initial URL is deferred.

Review Change Stack

@vercel

vercel Bot commented May 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Canceled Canceled May 23, 2026 4:23am
cmux-staging Building Building Preview, Comment May 23, 2026 4:23am

@coderabbitai

coderabbitai Bot commented May 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR moves blank-state logic from WebView inspection to the BrowserPanel model, adds a .deferredURL webview lifecycle state for non-renderable panels with a preferred URL, updates BrowserPanelView to use the model-based blank check, and adds tests validating deferred initial URLs.

Changes

Omnibar blank-state detection consolidation

Layer / File(s) Summary
Model: lifecycle & new-tab condition
Sources/Panels/BrowserPanel.swift
Adds BrowserWebViewLifecycleState.deferredURL; refreshWebViewLifecycleState() now sets .deferredURL when !shouldRenderWebView and preferredURLStringForOmnibar() is non-nil; isShowingNewTabPage requires !shouldRenderWebView && preferredURLStringForOmnibar() == nil.
View: omnibar blankness & focus
Sources/Panels/BrowserPanelView.swift
Replaces isWebViewBlank() with isBrowserContentBlankForOmnibar() (based on preferredURLStringForOmnibar()); updates currentURL blur handling, empty-state import overlay visibility (uses panel.isShowingNewTabPage), and omnibar auto-focus gating.
Tests: lifecycle and deferred URL
cmuxTests/BrowserConfigTests.swift, cmuxTests/GhosttyConfigTests.swift
Adds and updates tests to assert .deferredURL lifecycle for panels created with initialURL and renderInitialNavigation: false, and that such panels are not reported as new-tab pages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#4245: Modifies BrowserPanel webview lifecycle behavior and hidden-WebView handling that relate to this PR's lifecycle adjustments.
  • manaflow-ai/cmux#4244: Changes BrowserPanel lifecycle/visibility reporting and BrowserPanelView wiring, overlapping with this PR's model/view changes.
  • manaflow-ai/cmux#4243: Adjusts refreshWebViewLifecycleState/shouldRenderWebView derivation used by this PR.

Poem

🐰 I nibbled at blankness, found a clue,
The omnibar whispers what it should do,
Deferred URLs wait, the view stays calm,
Focus follows model — a tidy charm. 🥕✨

🚥 Pre-merge checks | ✅ 16 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix browser URL bar stealing focus on tab switch' directly and concisely summarizes the main bug fix addressed in the PR, matching the primary objective to prevent omnibar focus during tab switches.
Linked Issues check ✅ Passed The PR successfully addresses issue #4616 by introducing deferredURL state to distinguish remembered URLs from true new tabs, and updating autofocus/empty-state checks to use model-aware blank detection instead of WebView state, preventing omnibar focus theft during tab switches.
Out of Scope Changes check ✅ Passed All changes are within scope: BrowserPanel logic refinements, BrowserPanelView autofocus fixes, and regression tests directly address the issue #4616 objectives of preventing unintended URL bar focus during tab activation.
Cmux Swift Actor Isolation ✅ Passed The PR adds enum case to already-nonisolated BrowserWebViewLifecycleState, modifies logic in @MainActor class, adds tests. No new isolation issues introduced.
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing primitives added. Changes involve enum case, state logic tightening, and deterministic helpers without sleep, locks, or asyncAfter.
Cmux No Hacky Sleeps ✅ Passed This PR modifies only Swift files, which are explicitly excluded from this rule's scope that covers TypeScript, JavaScript, shell, and non-Swift build/runtime scripts.
Cmux Swift Concurrency ✅ Passed PR adds no legacy async patterns: enum case, synchronous logic updates, simple helper—no background queues, Combine, completion handlers, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed PR contains no new async functions, @concurrent annotations, or concurrency-related changes. All modifications are to synchronous properties and methods with proper nonisolated enum definition.
Cmux Swift File And Package Boundaries ✅ Passed Focused bug fix adding only +1 net line to already-oversized BrowserPanel.swift, fits allowed exceptions for incidental touches and small bug fixes.
Cmux Swift Logging ✅ Passed No logging violations. Changes to BrowserPanel.swift and BrowserPanelView.swift contain no print, debugPrint, dump, or NSLog statements violating the logging rules.
Cmux User-Facing Error Privacy ✅ Passed PR changes only internal state management (BrowserPanel, BrowserPanelView) with new enum case "deferred_url" and helper functions. No user-facing error messages, alerts, or sensitive data exposed.
Cmux Full Internationalization ✅ Passed PR contains no user-facing text additions. Changes are limited to internal enum states, doc comments, private helper functions, and tests—all exempt from i18n requirements.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations. PR modifies existing ObservableObject model logic and helper functions without adding new @Published/@StateObject/@observable or render-time mutations.
Cmux Architecture Rethink ✅ Passed No anti-patterns: no timing/dispatch/observer/duplicate-ownership introduced. Clear correctness fix tightening isShowingNewTabPage to prevent misclassifying real URLs as new tabs when unrendered.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies browser panel lifecycle logic only; no new NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup declarations introduced.
Description check ✅ Passed The PR description covers the bug summary, root cause, solution approach, testing strategy, and regression tests, matching the template structure.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-4616-browser-url-bar-focus-steal

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 May 23, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a focus-steal bug where switching to a tab with a deferred/restored URL caused the URL bar to grab focus because the panel was misclassified as a new-tab page.

  • BrowserPanel.swift: Adds .deferredURL lifecycle state; tightens isShowingNewTabPage to require both !shouldRenderWebView and preferredURLStringForOmnibar() == nil, so a tab with a remembered URL is never treated as blank.
  • BrowserPanelView.swift: Replaces the about:blank/WebView-URL check (isWebViewBlank()) with a model-aware isBrowserContentBlankForOmnibar() that consults both the WebKit URL and the panel model, updating autofocus guard and the import-overlay empty-state check accordingly.
  • cmuxTests/BrowserConfigTests.swift / GhosttyConfigTests.swift: Adds a failing-first regression test for initialURL + renderInitialNavigation: false; renames and updates an existing lifecycle test to assert .deferredURL instead of .newTab.

Confidence Score: 5/5

Safe to merge — the change is small, localized to new-tab detection and omnibar autofocus, backed by a targeted regression test, and leaves no unsafe intermediate state representable.

All three callsites of resetWebViewLifecycleMetadata() are safe: the workspace-context-reset guard checks currentURL != nil before ever entering the short-circuit path, so .newTab can only be hardcoded when there truly is no URL. The new .deferredURL classification in refreshWebViewLifecycleState() correctly encodes the invariant, and preferredURLStringForOmnibar() is consistently used in both model and view layers.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Panels/BrowserPanel.swift Adds .deferredURL lifecycle state and tightens isShowingNewTabPage to guard against URL-bearing deferred tabs being misclassified as new tabs; all resetWebViewLifecycleMetadata() callers are safe.
Sources/Panels/BrowserPanelView.swift Replaces WebKit-URL-only blank check with model-aware isBrowserContentBlankForOmnibar() across all three callsites consistently.
cmuxTests/BrowserConfigTests.swift Adds a failing-first regression test covering all three affected properties for the deferred-URL case.
cmuxTests/GhosttyConfigTests.swift Renames and updates existing lifecycle test to assert .deferredURL instead of .newTab.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Tab Switch / Panel Focus] --> B{shouldRenderWebView?}
    B -- Yes --> C[Live WebView active]
    C --> D{isWebViewLoading?}
    D -- Yes --> E[Skip autofocus — navigation in progress]
    D -- No --> F{preferredURLStringForOmnibar == nil?}
    F -- No --> G[Skip autofocus — real URL present]
    F -- Yes --> H[Autofocus URL bar — true new tab]
    B -- No --> I{preferredURLStringForOmnibar == nil?}
    I -- Yes --> J[isShowingNewTabPage = true, webViewLifecycleState = .newTab, Autofocus URL bar, Show import overlay]
    I -- No --> K[isShowingNewTabPage = false, webViewLifecycleState = .deferredURL, Do NOT autofocus]
Loading

Reviews (2): Last reviewed commit: "fix: classify deferred browser URLs expl..." | Re-trigger Greptile

@austinywang
austinywang merged commit ad8446f into main May 23, 2026
19 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 3b7be1b3 Deployed May 23, 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.

Browser URL/search bar steals focus when switching between browser tabs

1 participant