Skip to content

Fix browser up/down arrows after omnibar focus#1784

Open
lawrencecchen wants to merge 48 commits intomainfrom
task-browser-up-down-arrow-keys
Open

Fix browser up/down arrows after omnibar focus#1784
lawrencecchen wants to merge 48 commits intomainfrom
task-browser-up-down-arrow-keys

Conversation

@lawrencecchen
Copy link
Copy Markdown
Contributor

@lawrencecchen lawrencecchen commented Mar 19, 2026

Summary

  • add a regression test for stale browser omnibar focus intercepting plain Down Arrow after focus returns to the page
  • clear stale omnibar focus when a browser WKWebView becomes first responder again

Testing

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-task-browser-up-down-arrow-keys test -only-testing:cmuxTests/AppDelegateShortcutRoutingTests/testDownArrowIsNotConsumedAfterBrowserWebViewRetakesFocusFromStaleAddressBarState
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-task-browser-up-down-arrow-keys test -only-testing:cmuxTests/CmuxWebViewKeyEquivalentTests/testPointerFocusAllowanceCanTemporarilyOverrideBlockedFirstResponderAcquisition
  • ./scripts/reload.sh --tag task-browser-up-down-arrow-keys

Task

  • Browser workspace up/down arrows do not move the cursor inside web text fields after returning focus from the omnibar

Summary by cubic

Fixes omnibar focus and key routing so arrows and Return/Enter reach page inputs after leaving the address bar. Web‑view click handoff now uses authoritative omnibar state so focus reliably returns to the page.

  • Bug Fixes

    • Bypass performKeyEquivalent for browser content keys; send keyDown to the web view or field editor when a browser responder is focused and the omnibar isn’t, then fall back to app/menu shortcuts before keyDown so arrows (incl. Cmd/Shift) and Return/Enter reach inputs/contenteditables before AppKit.
    • Use authoritative omnibar focus for web‑view click handoff. Clear stale focus when the web view retakes first responder. Post .browserWillBlurAddressBarForWebViewClick and .browserDidBlurAddressBar. Keep web‑view focus policy in sync via new BrowserPanel helpers.
  • Tests/Tooling

    • Added unit tests for direct routing and stale‑focus clearing, plus web‑view click focus regressions. New XCUITests cover textarea and contenteditable after Cmd+L and after click using native key events and a control‑socket harness.
    • Hardened UI‑test startup with activation retries, per‑run launch diagnostics/tags, broader CLI discovery, longer socket timeouts, DOM‑readiness polling, and an app‑side contenteditable harness; fixed e2e JSON quoting, corrected XCUITest counter baselines, updated expectations for new routing, and used native arrow function‑key codes for event generation.

Addresses the Linear task “Browser workspace up/down arrows do not move the cursor inside web text fields after returning focus from the omnibar.”

Written for commit 07b36b9. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Clear stale address-bar focus when a web view retakes first-responder status, aborting focus-repeat edge cases and adding debug logging.
  • Tests

    • Added a unit test ensuring Down Arrow events reach the web view after focus transitions.
    • Added an end-to-end UI test validating arrow-key behavior across omnibar focus and a subsequent page click, plus browser-side instrumentation and CLI-based helpers for verification.
  • Chores

    • Simplified CI manifest generation.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 19, 2026

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 24, 2026 2:54am

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 19, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an AppDelegate observer to clear stale address-bar focus when a WKWebView becomes first responder; updates observer setup; adds unit and UI tests verifying arrow/shortcut routing across omnibar↔page focus transitions; minor CI manifest write change.

Changes

Cohort / File(s) Summary
AppDelegate focus handling
Sources/AppDelegate.swift
Adds private var browserWebViewFocusObserver: NSObjectProtocol?; includes it in the single-setup guard and registers for .browserDidBecomeFirstResponderWebView to post .browserDidBlurAddressBar and emit a debug log to clear stale address-bar focus.
Unit test — shortcut routing
cmuxTests/AppDelegateShortcutRoutingTests.swift
Adds import WebKit and testDownArrowIsNotConsumedAfterBrowserWebViewRetakesFocusFromStaleAddressBarState to simulate stale omnibar focus, signal WebView first-responder, send Down Arrow key event, assert event is not consumed and stale focus cleared.
UI tests — arrow/omnibar flow & helpers
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
Adds testArrowKeysReachClickedPageInputAfterCmdL plus helpers to inject an in-page arrow-key harness and evaluate it via a resolved cmux CLI (browserEval, executeCmuxCommand, resolveCmuxCLIPath, BrowserArrowReport, javaScriptLiteral, etc.).
CI workflow minor change
.github/workflows/test-e2e.yml
Replaced a here-document manifest write with a single-line printf emitting the same JSON manifest.

Sequence Diagram

sequenceDiagram
    participant Client as Keyboard/EventSource
    participant WebView as WKWebView
    participant NC as NotificationCenter
    participant AppD as AppDelegate
    participant AB as AddressBar

    WebView->>NC: post browserDidBecomeFirstResponderWebView
    NC->>AppD: notify browserWebViewFocusObserver
    AppD->>NC: post browserDidBlurAddressBar
    NC->>AB: browserDidBlurAddressBar (clear stale focus)
    Note right of AppD: Debug log (focus cleared)
    Client->>WebView: key event (e.g., Down Arrow)
    WebView->>WebView: handle key normally
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped from bar to page with glee,
A blur was sent to set me free,
Arrows skip to inputs bright,
No stale focus in my sight,
Hooray — the keys and I agree!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: restoring browser up/down arrow functionality after omnibar focus.
Description check ✅ Passed PR description includes Summary and Testing sections but lacks Demo Video, Review Trigger, and complete Checklist items.

✏️ 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 task-browser-up-down-arrow-keys

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.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

@jtdoth
Copy link
Copy Markdown

jtdoth commented Mar 19, 2026

I ran into the same issue and did some debugging in #1423 — wanted to share what I found in case it's useful.

When I traced the key events, I noticed there are actually two layers where arrow keys get intercepted:

  1. Stale omnibar focus in handleCustomShortcut — this is what your PR fixes, and it works for cases where browserAddressBarFocusedPanelId stays set after focus returns to the page.

  2. SwiftUI hosting view in performKeyEquivalent — even after the stale state is cleared, the original NSWindow.performKeyEquivalent walks the view hierarchy, and SwiftUI's hosting view can return true without actually handling the event. In my debug log I could see → consumed by original performKeyEquivalent with fr=CmuxWebView after the address bar had already blurred.

This second layer seems to happen when SwiftUI's internal focus system gets into a broken state after a WKWebView has been in the responder chain — same issue that the terminal path already works around at line 11159 by routing non-Command keys directly to ghosttyView.performKeyEquivalent.

Not sure if you're seeing the same thing on your end, but thought it might be worth flagging.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift (1)

1343-1375: Minor: Pipe reading order after waitUntilExit() could block on large outputs.

Reading from pipes after waitUntilExit() (lines 1368-1369) is safe for the small JSON outputs expected here, but could cause a deadlock if the process writes enough to fill the pipe buffer before exiting.

For this use case (cmux CLI returning short JSON), the current implementation is fine. If this helper is ever reused for commands with larger outputs, consider reading pipes asynchronously or before waiting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 1343 -
1375, The helper executeCmuxCommand reads stdout/stderr synchronously after
waitUntilExit(), which can deadlock if a subprocess produces large output;
change it to read the pipes asynchronously (or start background reads on
stdoutPipe.fileHandleForReading and stderrPipe.fileHandleForReading using
readToEndOfFileInBackgroundAndNotify or Dispatch I/O) before calling
waitUntilExit(), capture the completed Data results, convert/trimming them the
same way, and then return the terminationStatus, stdout and stderr; ensure you
still set process.environment and process.arguments as currently done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 1343-1375: The helper executeCmuxCommand reads stdout/stderr
synchronously after waitUntilExit(), which can deadlock if a subprocess produces
large output; change it to read the pipes asynchronously (or start background
reads on stdoutPipe.fileHandleForReading and stderrPipe.fileHandleForReading
using readToEndOfFileInBackgroundAndNotify or Dispatch I/O) before calling
waitUntilExit(), capture the completed Data results, convert/trimming them the
same way, and then return the terminationStatus, stdout and stderr; ensure you
still set process.environment and process.arguments as currently done.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c499150c-f11a-492f-8535-712ef089c340

📥 Commits

Reviewing files that changed from the base of the PR and between 2a11cfe and 70221f4.

📒 Files selected for processing (1)
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmuxUITests/BrowserPaneNavigationKeybindUITests.swift (1)

415-418: Replace the fixed sleep before the click.

The 0.15 second delay is still a race on slower UI runners; wait for browserPane to be hittable, or another explicit post-Cmd+L readiness signal, before clicking.

♻️ Suggested change
-        RunLoop.current.run(until: Date().addingTimeInterval(0.15))
+        XCTAssertTrue(
+            waitForElementToBecomeHittable(browserPane, timeout: 6.0),
+            "Expected browser pane to be hittable before clicking the secondary page input"
+        )
         browserPane
             .coordinate(withNormalizedOffset: CGVector(dx: harness.secondaryCenterX, dy: harness.secondaryCenterY))
             .click()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift` around lines 415 -
418, Replace the hard-coded RunLoop delay with an explicit wait for browserPane
to be ready: remove the RunLoop.current.run(until:
Date().addingTimeInterval(0.15)) call and instead wait for browserPane to become
hittable (e.g., use an NSPredicate "isHittable == true" with
expectation(for:evaluatedWith:handler:) and wait(for:timeout:) or XCTWaiter)
before calling browserPane.coordinate(...).click(); reference the XCUIElement
instance browserPane and the existing harness.secondaryCenterX/secondaryCenterY
coordinate usage when implementing the wait.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 1359-1368: The browserEval helper currently calls
executeCmuxCommand which can block indefinitely; update browserEval (or
executeCmuxCommand) to enforce a subprocess timeout (e.g., pass a timeout
parameter or implement a DispatchWorkItem/Timer that kills the Process and
returns a timeout result) so the test predicate never hangs, and ensure stderr
is captured and surfaced in the returned result or error logging (include stderr
text when returning nil/timeout) to aid diagnostics; change references inside
browserEval (function name: browserEval, called function: executeCmuxCommand)
accordingly so a non-zero/timeout termination returns promptly with
stdout/stderr available.

---

Nitpick comments:
In `@cmuxUITests/BrowserPaneNavigationKeybindUITests.swift`:
- Around line 415-418: Replace the hard-coded RunLoop delay with an explicit
wait for browserPane to be ready: remove the RunLoop.current.run(until:
Date().addingTimeInterval(0.15)) call and instead wait for browserPane to become
hittable (e.g., use an NSPredicate "isHittable == true" with
expectation(for:evaluatedWith:handler:) and wait(for:timeout:) or XCTWaiter)
before calling browserPane.coordinate(...).click(); reference the XCUIElement
instance browserPane and the existing harness.secondaryCenterX/secondaryCenterY
coordinate usage when implementing the wait.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a2e4417-4ee7-4ffd-904c-0aadb5c80be9

📥 Commits

Reviewing files that changed from the base of the PR and between 0a1342b and e52458a.

📒 Files selected for processing (1)
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift

Comment thread cmuxUITests/BrowserPaneNavigationKeybindUITests.swift Outdated
…arrow-keys-merge

# Conflicts:
#	cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmuxUITests/BrowserPaneNavigationKeybindUITests.swift">

<violation number="1" location="cmuxUITests/BrowserPaneNavigationKeybindUITests.swift:1976">
P2: The counter-stability helper can pass even when no telemetry snapshot is read, so the new arrow-leak tests may report success without actually verifying counters.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
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.

2 participants