Skip to content

Add browser find focus debug logs - #1162

Merged
lawrencecchen merged 11 commits into
mainfrom
task-browser-find-focus-logs
Mar 11, 2026
Merged

lawrencecchen merged 11 commits into
mainfrom
task-browser-find-focus-logs

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add DEBUG logs around browser Cmd+F start, focus notifications, and current first responder state
  • log browser find overlay mount/update/removal and each focus request attempt inside the overlay

Testing

  • ./scripts/reload.sh --tag browser-find-focus-logs (build succeeded and launched the tagged app)

Issues

  • Related task: browser Cmd+F fails to focus the browser find search input

Summary by cubic

Adds debug logs and a unified, intent-driven focus system that keeps the browser Cmd+F search field owned by its panel while the find bar is open and prevents focus from jumping across splits. Focus capture, handoff, and restore are now consistent across browser and terminal panels, including command palette dismiss and tab/pane activation, addressing the Cmd+F search field focus failure.

  • New Features

    • Generalized PanelFocusIntent for terminal (surface/findField) and browser (webView/addressBar/findField) with capture/preferred/prepare/restore plus owned/yield semantics; Workspace yields foreign focus and restores the right target on pane/tab activation and command palette dismiss.
    • Browser find ownership persists while the find bar is open: BrowserPanel tracks preferredFocusIntent and searchFocusRequestGeneration; overlays receive focusRequestGeneration/canApplyFocusRequest, emit onFieldDidFocus, and the portal host associates overlays with their owning panel and can yield overlay focus.
    • Terminal panels implement intent-aware restore to avoid refocusing the terminal surface when targeting the find field, and resign owned first responder on deactivation.
  • Bug Fixes

    • Prevent search focus from jumping across splits by yielding foreign-owned responders before restoring focus.
    • Tighten portal-host “web-owned” checks to only the hosted WKWebView, allowing overlay text fields to become first responder.
    • Avoid redundant browser search-focus invalidation by tracking preferred intent and generation; stop re-focusing the terminal surface when restoring a terminal find field.

Written for commit 579d18f. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Cross-panel focus-intent system to capture/restore focus targets (web view, address bar, find field).
    • Find/search overlays now expose a field-focus callback so panels respond when the search field gains focus.
    • Command-palette restore now uses panel-captured focus intents for more reliable focus return.
  • Bug Fixes

    • Ensures search overlays attach, layout, and reuse correctly to avoid misplaced overlays and input/focus glitches.
    • Improves first-responder detection so overlays and embedded content maintain correct focus after navigation or activation.
  • Chores

    • Adds verbose, debug-only logging and clearer focus-request reasons to aid troubleshooting.

@vercel

vercel Bot commented Mar 10, 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 Mar 11, 2026 2:20am

@coderabbitai

coderabbitai Bot commented Mar 10, 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

Adds panel-level focus-intent APIs and wiring for capture/restore; instruments and traces search-overlay lifecycle and focus requests with origin/reason metadata; ensures portal overlay (re)attachment with full-edge constraints; tightens portal webview ownership checks and expands DEBUG-only focus logging across workspace, terminal, and panel layers.

Changes

Cohort / File(s) Summary
Panel focus-intent API & defaults
Sources/Panels/Panel.swift
Introduce TerminalPanelFocusIntent, BrowserPanelFocusIntent, PanelFocusIntent enums and add focus-intent APIs (captureFocusIntent(in:), preferredFocusIntentForActivation(), prepareFocusIntentForActivation(_:), restoreFocusIntent(_:)) with default implementations.
Panel implementations & wiring
Sources/Panels/TerminalPanel.swift, Sources/Panels/BrowserPanel.swift, Sources/Panels/BrowserPanelView.swift
TerminalPanel/BrowserPanel implement capture/preferred/restore flows; BrowserPanel tracks preferredFocusIntent and posts reasoned focus notifications; BrowserPanelView wires onFieldDidFocus into overlays and invokes panel note* focus hooks.
Search overlay callbacks & focus tracing
Sources/Find/BrowserSearchOverlay.swift, Sources/Find/SurfaceSearchOverlay.swift
Add public onFieldDidFocus callback; add AppKit import and DEBUG helpers; replace focus requests to carry an origin/reason string and emit structured DEBUG logs; call onFieldDidFocus() when field becomes focused.
Browser window portal overlay lifecycle
Sources/BrowserWindowPortal.swift
Add onFieldDidFocus to BrowserPortalSearchOverlayConfiguration; WindowBrowserSlotView.setSearchOverlay now logs create/remove/updateExisting (DEBUG), reattaches overlays as needed, and reapplies full-edge constraints.
Portal ownership / first-responder refinement
Sources/AppDelegate.swift
Narrow CmuxWebView detection: return portal webview only if current view is the portal webview or a descendant, preventing non-descendant chrome from being treated as web-owned.
Surface/terminal focus handling & logging
Sources/GhosttyTerminalView.swift
Replace direct responder clearing with resignOwnedFirstResponderIfNeeded(reason:); add capture/restore panel intent helpers, search-responder detection, and extensive DEBUG logging for focus/move/restore flows.
Workspace & ContentView focus integration
Sources/Workspace.swift, Sources/ContentView.swift
Introduce PendingTabSelectionRequest struct; carry focusIntent through activation; switch command-palette restore targets to use PanelFocusIntent from captureFocusIntent(in:); add DEBUG tracing and helper for formatting intents.
Misc focus-notification callers
Sources/.../BrowserPanel.swift, Sources/Panels/...
postBrowserSearchFocusNotification now accepts reason: String; callers updated with explicit reasons (immediate, async0, async50ms, restoreAfterNavigation) and added logging.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Panel as BrowserPanel
    participant NC as NotificationCenter
    participant Overlay as BrowserSearchOverlay
    participant Slot as WindowBrowserSlotView
    participant App as AppDelegate

    User->>Panel: startFind / request focus
    Panel->>NC: postBrowserSearchFocusNotification(reason)
    NC->>Overlay: browserSearchFocus notification
    Overlay->>Overlay: requestSearchFieldFocus(origin)
    Overlay->>Panel: onFieldDidFocus() when field focused
    Overlay->>Slot: ensure overlay attached / reattach if needed
    Slot->>App: query portal webview ownership (is descendant?)
    App-->>Slot: ownership result
    Slot-->>Overlay: apply constraints / attachment result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through overlays, soft and bright,
I chased each focus, logged every light.
Constraints stitched edges where portals lay,
Descendants checked before first-responder play.
A tiny rabbit cheers — focus traces take flight.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Add browser find focus debug logs' clearly describes the main change but significantly understates the scope; the PR also adds comprehensive PanelFocusIntent APIs and fixes focus-restoration bugs beyond just debug logging. Consider a more comprehensive title like 'Add browser find focus debug logs and fix focus restoration' to better reflect the substantial API additions and bug fixes included.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description covers the key requirements: a clear summary of changes (DEBUG logs and focus system), testing method, and related issue reference.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task-browser-find-focus-logs

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds #if DEBUG-guarded diagnostic logging around the browser Cmd+F (find-in-page) focus flow to help diagnose why the browser find search input fails to receive first-responder focus. Log points are added at overlay mount/update/removal (BrowserWindowPortal), at each focus-request attempt and focus-state change (BrowserSearchOverlay), and at the start of find and each focus-notification posting (BrowserPanel).

Key changes:

  • BrowserPanel.swift: postBrowserSearchFocusNotification gains a reason parameter; startFind logs window, key-window, and first-responder state at the call site.
  • BrowserWindowPortal.swift: logSearchOverlayEvent helper logs overlay lifecycle events (create / updateExisting / remove) with first-responder and window info — all correctly guarded by #if DEBUG.
  • BrowserSearchOverlay.swift: logFocusState and debugFirstResponderSummary helpers added; onChange(of: isSearchFieldFocused) observation introduced to trace when SwiftUI's @FocusState actually transitions. A DispatchQueue.main.async closure used solely for after-async logging is not guarded by #if DEBUG, so it enqueues a no-op closure in release builds. debugFirstResponderSummary is also missing a #if DEBUG guard.

Confidence Score: 4/5

  • Safe to merge — purely additive debug instrumentation with no changes to production logic.
  • All logging in BrowserPanel and BrowserWindowPortal is correctly enclosed in #if DEBUG. BrowserSearchOverlay has two small hygiene issues (an unguarded DispatchQueue.main.async and an unguarded helper function) that add minor overhead in release builds but do not affect correctness or behaviour.
  • Sources/Find/BrowserSearchOverlay.swift — unguarded async dispatch and helper function in release builds.

Important Files Changed

Filename Overview
Sources/Find/BrowserSearchOverlay.swift Adds logFocusState / debugFirstResponderSummary helpers and wires them into onAppear, onChange, and notification handler. A DispatchQueue.main.async block added outside #if DEBUG will enqueue a no-op closure in release builds; debugFirstResponderSummary is also missing a #if DEBUG guard.
Sources/BrowserWindowPortal.swift Adds logSearchOverlayEvent helper (correctly guarded by #if DEBUG) and calls it at overlay create, update, and remove sites. No functional issues.
Sources/Panels/BrowserPanel.swift Adds reason parameter to postBrowserSearchFocusNotification, threads it through all three call sites (immediate, async0, async50ms, restoreAfterNavigation), and logs start/notification events inside #if DEBUG blocks. Clean and correctly guarded.

Sequence Diagram

sequenceDiagram
    participant User
    participant BrowserPanel
    participant NotificationCenter
    participant BrowserWindowPortal
    participant BrowserSearchOverlay

    User->>BrowserPanel: Cmd+F (startFind)
    Note over BrowserPanel: dlog browser.find.start
    BrowserPanel->>BrowserPanel: searchState = BrowserSearchState()
    BrowserPanel->>NotificationCenter: post browserSearchFocus (reason=immediate)
    Note over BrowserPanel: dlog browser.find.focusNotification
    BrowserPanel-->>NotificationCenter: async post (reason=async0)
    BrowserPanel-->>NotificationCenter: asyncAfter 50ms post (reason=async50ms)

    BrowserWindowPortal->>BrowserWindowPortal: setSearchOverlay(config)
    Note over BrowserWindowPortal: dlog portal action=set
    alt New overlay
        BrowserWindowPortal->>BrowserSearchOverlay: NSHostingView(rootView:)
        Note over BrowserWindowPortal: dlog portal action=create
    else Existing overlay
        Note over BrowserWindowPortal: dlog portal action=updateExisting
        BrowserWindowPortal->>BrowserSearchOverlay: overlay.rootView = rootView
    end

    BrowserSearchOverlay->>BrowserSearchOverlay: onAppear
    Note over BrowserSearchOverlay: dlog browser.findbar.appear
    Note over BrowserSearchOverlay: logFocusState(appear)
    BrowserSearchOverlay->>BrowserSearchOverlay: requestSearchFieldFocus(origin=appear)
    Note over BrowserSearchOverlay: isSearchFieldFocused = true
    Note over BrowserSearchOverlay: logFocusState(request.afterAsync) [async]

    NotificationCenter-->>BrowserSearchOverlay: browserSearchFocus notification
    Note over BrowserSearchOverlay: logFocusState(notification.received)
    BrowserSearchOverlay->>BrowserSearchOverlay: requestSearchFieldFocus(origin=notification)

    BrowserSearchOverlay-->>BrowserSearchOverlay: onChange(isSearchFieldFocused)
    Note over BrowserSearchOverlay: logFocusState(focusState.change)
Loading

Last reviewed commit: 726f3a5

Comment on lines +44 to +46
DispatchQueue.main.async {
logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
}

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.

Unnecessary async dispatch in release builds

DispatchQueue.main.async is always enqueued — even in release builds — but its sole purpose is to call logFocusState, which compiles to an empty function when DEBUG is not set. The closure dispatch itself has runtime cost that cannot be optimised away. Wrapping the block in #if DEBUG removes this overhead:

Suggested change
DispatchQueue.main.async {
logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
}
#if DEBUG
DispatchQueue.main.async {
logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
}
#endif

Comment on lines +18 to +26
private func debugFirstResponderSummary() -> String {
guard let window = NSApp.keyWindow else { return "nil" }
guard let firstResponder = window.firstResponder else { return "nil" }
if let editor = firstResponder as? NSTextView, editor.isFieldEditor {
let delegateSummary = editor.delegate.map { String(describing: type(of: $0)) } ?? "nil"
return "fieldEditor(delegate=\(delegateSummary))"
}
return String(describing: type(of: firstResponder))
}

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.

Debug helper compiled into release builds

debugFirstResponderSummary() is only ever called inside the #if DEBUG block within logFocusState, yet the function itself has no #if DEBUG guard. It will be compiled (and occupy space in the binary) even in release builds. Consider guarding it the same way as logFocusState:

Suggested change
private func debugFirstResponderSummary() -> String {
guard let window = NSApp.keyWindow else { return "nil" }
guard let firstResponder = window.firstResponder else { return "nil" }
if let editor = firstResponder as? NSTextView, editor.isFieldEditor {
let delegateSummary = editor.delegate.map { String(describing: type(of: $0)) } ?? "nil"
return "fieldEditor(delegate=\(delegateSummary))"
}
return String(describing: type(of: firstResponder))
}
#if DEBUG
private func debugFirstResponderSummary() -> String {
guard let window = NSApp.keyWindow else { return "nil" }
guard let firstResponder = window.firstResponder else { return "nil" }
if let editor = firstResponder as? NSTextView, editor.isFieldEditor {
let delegateSummary = editor.delegate.map { String(describing: type(of: $0)) } ?? "nil"
return "fieldEditor(delegate=\(delegateSummary))"
}
return String(describing: type(of: firstResponder))
}
#endif

Comment thread Sources/Find/BrowserSearchOverlay.swift Outdated
Comment on lines 138 to 140
.onChange(of: isSearchFieldFocused) { focused in
logFocusState("focusState.change next=\(focused ? 1 : 0)")
}

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.

Deprecated onChange(of:) overload

The single-closure form of .onChange(of:) was deprecated in macOS 14 / iOS 17. The new two-parameter form (receiving oldValue and newValue) should be used instead:

Suggested change
.onChange(of: isSearchFieldFocused) { focused in
logFocusState("focusState.change next=\(focused ? 1 : 0)")
}
.onChange(of: isSearchFieldFocused) { _, newValue in
logFocusState("focusState.change next=\(newValue ? 1 : 0)")
}

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 3 files

@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.

🧹 Nitpick comments (3)
Sources/Find/BrowserSearchOverlay.swift (3)

44-46: Wrap the async dispatch in #if DEBUG to avoid unnecessary dispatch in release builds.

In release builds, this schedules an async dispatch that calls logFocusState, which has an empty body. This creates a small runtime overhead (dispatch queue scheduling) that serves no purpose. As per coding guidelines, debug events should be wrapped in #if DEBUG.

Suggested refactor
         logFocusState("request.begin origin=\(origin) remaining=\(maxAttempts)")
         isSearchFieldFocused = true
+#if DEBUG
         DispatchQueue.main.async {
             logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
         }
+#endif
         guard maxAttempts > 1 else { return }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Find/BrowserSearchOverlay.swift` around lines 44 - 46, The
DispatchQueue.main.async call that invokes logFocusState("request.afterAsync
origin=\(origin) remaining=\(maxAttempts)") should be executed only in debug
builds to avoid unnecessary dispatch overhead in release; wrap the
DispatchQueue.main.async { logFocusState(...) } block in an `#if` DEBUG ... `#endif`
so that logFocusState, DispatchQueue.main.async, origin and maxAttempts are only
used for debug instrumentation and omitted from release builds.

18-38: Consider wrapping debugFirstResponderSummary() in #if DEBUG.

The helper function is only called from within logFocusState's DEBUG-guarded body. In release builds, debugFirstResponderSummary() will be compiled but never called, resulting in dead code in the binary.

Suggested refactor
+#if DEBUG
 private func debugFirstResponderSummary() -> String {
     guard let window = NSApp.keyWindow else { return "nil" }
     guard let firstResponder = window.firstResponder else { return "nil" }
     if let editor = firstResponder as? NSTextView, editor.isFieldEditor {
         let delegateSummary = editor.delegate.map { String(describing: type(of: $0)) } ?? "nil"
         return "fieldEditor(delegate=\(delegateSummary))"
     }
     return String(describing: type(of: firstResponder))
 }
+#endif

 private func logFocusState(_ event: String) {
 `#if` DEBUG
     let keyWindow = NSApp.keyWindow
     dlog(
         "browser.findbar.focus panel=\(panelId.uuidString.prefix(5)) " +
         "event=\(event) keyWindow=\(keyWindow?.windowNumber ?? -1) " +
         "firstResponder=\(debugFirstResponderSummary()) " +
         "focused=\(isSearchFieldFocused ? 1 : 0)"
     )
 `#endif`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Find/BrowserSearchOverlay.swift` around lines 18 - 38, The helper
debugFirstResponderSummary() is only used inside the DEBUG-only body of
logFocusState(), leaving it as dead code in release builds; move or wrap its
entire declaration inside the same `#if` DEBUG block as logFocusState() (or inline
its logic into logFocusState()) so debugFirstResponderSummary() is only compiled
in DEBUG builds, keeping release binaries free of unused symbols while
preserving the existing behavior of logFocusState().

138-140: Wrap the debug-only onChange in #if DEBUG.

This onChange handler exists solely to call logFocusState, which is empty in release builds. The handler will still fire on every focus state change but perform no useful work.

Suggested refactor
             .onAppear {
 `#if` DEBUG
                 dlog("browser.findbar.appear panel=\(panelId.uuidString.prefix(5))")
-#endif
                 logFocusState("appear")
+#endif
                 requestSearchFieldFocus(origin: "appear")
             }
+#if DEBUG
             .onChange(of: isSearchFieldFocused) { focused in
                 logFocusState("focusState.change next=\(focused ? 1 : 0)")
             }
+#endif
             .onReceive(NotificationCenter.default.publisher(for: .browserSearchFocus)) { notification in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Find/BrowserSearchOverlay.swift` around lines 138 - 140, The
.onChange(of: isSearchFieldFocused) { focused in logFocusState(...) } handler in
BrowserSearchOverlay.swift should be compiled only in debug builds; wrap the
entire .onChange modifier in a conditional compilation block (`#if` DEBUG ...
`#endif`) so the handler (and its call to logFocusState) is removed in release
builds, ensuring view modifier balance and that you place the `#endif` immediately
after the .onChange(...) call to keep the SwiftUI view builder intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/Find/BrowserSearchOverlay.swift`:
- Around line 44-46: The DispatchQueue.main.async call that invokes
logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
should be executed only in debug builds to avoid unnecessary dispatch overhead
in release; wrap the DispatchQueue.main.async { logFocusState(...) } block in an
`#if` DEBUG ... `#endif` so that logFocusState, DispatchQueue.main.async, origin and
maxAttempts are only used for debug instrumentation and omitted from release
builds.
- Around line 18-38: The helper debugFirstResponderSummary() is only used inside
the DEBUG-only body of logFocusState(), leaving it as dead code in release
builds; move or wrap its entire declaration inside the same `#if` DEBUG block as
logFocusState() (or inline its logic into logFocusState()) so
debugFirstResponderSummary() is only compiled in DEBUG builds, keeping release
binaries free of unused symbols while preserving the existing behavior of
logFocusState().
- Around line 138-140: The .onChange(of: isSearchFieldFocused) { focused in
logFocusState(...) } handler in BrowserSearchOverlay.swift should be compiled
only in debug builds; wrap the entire .onChange modifier in a conditional
compilation block (`#if` DEBUG ... `#endif`) so the handler (and its call to
logFocusState) is removed in release builds, ensuring view modifier balance and
that you place the `#endif` immediately after the .onChange(...) call to keep the
SwiftUI view builder intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0281abb5-37f6-471e-992f-18f57c823996

📥 Commits

Reviewing files that changed from the base of the PR and between 22ae558 and 726f3a5.

📒 Files selected for processing (3)
  • Sources/BrowserWindowPortal.swift
  • Sources/Find/BrowserSearchOverlay.swift
  • Sources/Panels/BrowserPanel.swift

@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 the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 6831-6841: The early return when another search overlay owns
firstResponder prevents calling terminalSurface?.setFocus(false), leaving this
surface's cursor visually focused; before returning from the block guarded by
window.firstResponder / isSearchOverlayOrDescendant(firstResponder) /
!isCurrentSurfaceSearchResponder(firstResponder), ensure you clear this
surface's focus by invoking terminalSurface?.setFocus(false) (or check
terminalSurface.isFocused and call setFocus(false) only if true) so the terminal
surface visual state is kept in sync when yielding focus to another search
overlay.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43cfc74f-6397-426c-b5a8-7569ce18285d

📥 Commits

Reviewing files that changed from the base of the PR and between afc5786 and 81ceee1.

📒 Files selected for processing (3)
  • Sources/Find/SurfaceSearchOverlay.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift

Comment thread Sources/GhosttyTerminalView.swift

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 9 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="Sources/GhosttyTerminalView.swift">

<violation number="1" location="Sources/GhosttyTerminalView.swift:6868">
P2: Focus intent capture misclassifies terminal focus as `.findField` whenever search is open. That makes later focus restoration jump back to the find field even when the user was actively focused in the terminal.</violation>

<violation number="2" location="Sources/GhosttyTerminalView.swift:6889">
P1: Restoring `.surface` focus should set terminal intent first. Without updating `searchFocusTarget`, `setActive(true)` can immediately re-route focus back to the search field when search is open.</violation>
</file>

<file name="Sources/Panels/BrowserPanel.swift">

<violation number="1" location="Sources/Panels/BrowserPanel.swift:1715">
P1: `focus()` now clears previously tracked in-panel intent by forcing `.webView` before guard checks, which can make activation restore target the web view instead of the address bar/find field.</violation>
</file>

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

Comment thread Sources/GhosttyTerminalView.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift
Comment thread Sources/GhosttyTerminalView.swift

@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: 7

Caution

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

⚠️ Outside diff range comments (1)
Sources/Panels/BrowserPanelView.swift (1)

1499-1555: ⚠️ Potential issue | 🟠 Major

Record WebKit focus after the final handoff result, not only the eager attempt.

panel.noteWebViewFocused() only runs when the first window.makeFirstResponder(panel.webView) succeeds. If that call fails but restoreAddressBarPageFocusIfNeeded later restores page focus, or the fallback makeFirstResponder succeeds, the panel stays marked as .addressBar and later restoration can bounce focus back to the omnibar.

💡 Suggested fix
                 panel.restoreAddressBarPageFocusIfNeeded { restored in
                     guard shouldApplyAddressBarExitFallback(in: window) else {
 `#if` DEBUG
                         dlog(
                             "browser.focus.addressBar.exit.handoff panel=\(panel.id.uuidString.prefix(5)) " +
                             "result=skip_stale_restore restored=\(restored ? 1 : 0)"
                         )
 `#endif`
                         NotificationCenter.default.post(name: .browserDidExitAddressBar, object: panel.id)
                         return
                     }
-                    let hasWebViewResponder =
+                    var hasWebViewResponder =
                         browserFocusResponderChainContains(window.firstResponder, target: panel.webView)
                     if !hasWebViewResponder {
                         let fallbackFocusedWebView = window.makeFirstResponder(panel.webView)
+                        hasWebViewResponder = fallbackFocusedWebView
 `#if` DEBUG
                         dlog(
                             "browser.focus.addressBar.exit.handoff panel=\(panel.id.uuidString.prefix(5)) " +
                             "fallbackFocusedWebView=\(fallbackFocusedWebView ? 1 : 0) " +
                             "restored=\(restored ? 1 : 0)"
                         )
 `#endif`
                     }
+                    if hasWebViewResponder {
+                        panel.noteWebViewFocused()
+                    }
                     NotificationCenter.default.post(name: .browserDidExitAddressBar, object: panel.id)
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanelView.swift` around lines 1499 - 1555, The panel
currently calls panel.noteWebViewFocused() only when the eager
window.makeFirstResponder(panel.webView) succeeds, missing cases where
restoreAddressBarPageFocusIfNeeded or the fallback makeFirstResponder actually
result in the WebView becoming first responder; update the
restoreAddressBarPageFocusIfNeeded completion block to record WebKit focus there
as well: after computing hasWebViewResponder (using
browserFocusResponderChainContains(window.firstResponder, target:
panel.webView)), if hasWebViewResponder call panel.noteWebViewFocused();
otherwise call window.makeFirstResponder(panel.webView) (the existing
fallbackFocusedWebView) and if that returns true call
panel.noteWebViewFocused(); keep the NotificationCenter.post call as-is. This
ensures panel.noteWebViewFocused() is invoked after the final handoff attempt
(restore or fallback) rather than only the eager attempt.
♻️ Duplicate comments (1)
Sources/GhosttyTerminalView.swift (1)

6829-6839: ⚠️ Potential issue | 🟡 Minor

Clear Ghostty focus before yielding to a foreign search field.

This early return still skips terminalSurface?.setFocus(false). If this surface was already marked focused, the cursor can keep blinking while another split’s find field owns first responder.

Proposed fix
         case .searchField:
             if let firstResponder = window.firstResponder,
                isSearchOverlayOrDescendant(firstResponder),
                !isCurrentSurfaceSearchResponder(firstResponder) {
+                surfaceView.terminalSurface?.setFocus(false)
 `#if` DEBUG
                 dlog(
                     "find.restoreSearchFocus.skip surface=\(surfaceShort) target=searchField " +
                     "reason=foreignSearchResponder firstResponder=\(String(describing: firstResponder))"
                 )
 `#endif`
                 return
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 6829 - 6839, Before returning
when a foreign search field is the first responder (the branch checking
window.firstResponder along with isSearchOverlayOrDescendant(...) and
!isCurrentSurfaceSearchResponder(...)), ensure you clear this surface's focus
state by calling terminalSurface?.setFocus(false) (or equivalent) before the
early return so the cursor stops blinking; keep the existing debug dlog and
return behavior but insert terminalSurface?.setFocus(false) immediately prior to
the return to guarantee focus is cleared for this surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 5492-5495: The current builders always set
CommandPaletteRestoreFocusTarget.intent to a generic .terminal(.surface) or
.browser(.webView), which loses the clicked subcontrol; update the builders that
construct CommandPaletteRestoreFocusTarget (the occurrences at the shown spots
plus the similar blocks around lines 5507-5510 and 5548-5551) to pass through
the actual hit-test subcontrol instead of unconditionally using
.surface/.webView—i.e., use the original hit result or intent variable (the
exact value used to detect the clicked part) when creating
CommandPaletteRestoreFocusTarget(workspaceId:panelId:intent:) and only fall back
to .terminal(.surface)/.browser(.webView) when the hit truly indicates the panel
body, so restoreCommandPaletteFocus can return focus to address-bar/find-field
subcontrols.

In `@Sources/Find/BrowserSearchOverlay.swift`:
- Around line 41-51: The focus request currently sets isSearchFieldFocused in
requestSearchFieldFocus, but retries don't retrigger the confirmed-focus
callback because the `@FocusState` value is already true; instead, wire the
confirmed-focus signal to the actual AppKit responder callback (e.g.,
NSTextViewDelegate.textDidBeginEditing / beginEditing()) or explicitly call the
existing onFieldDidFocus callback from the real focus-change handler.
Concretely: stop relying solely on setting isSearchFieldFocused in
requestSearchFieldFocus; ensure the confirmed-focus callback used by
GhosttyTerminalView (referenced as onFieldDidFocus / beginEditing) is invoked
from the AppKit delegate or from the .onChange handler that observes the real
focus transition (the handler currently at lines ~139–143), or call that same
callback from beginEditing as SurfaceSearchOverlay does, so retries that finally
succeed still signal confirmed focus.

In `@Sources/GhosttyTerminalView.swift`:
- Around line 6886-6909: The current restorePanelFocusIntent(_:) bypasses
existing responder helpers: for the .surface case remove the direct
surfaceView.terminalSurface?.setFocus(true) call and instead call
setActive(true) then applyFirstResponderIfNeeded() so AppKit’s first-responder
logic is honored (leave applyFirstResponderIfNeeded() in place). For the
.findField case, keep the guard and setting searchFocusTarget = .searchField and
setActive(true), but remove the direct terminalSurface.setFocus(false) and the
NotificationCenter.post(name: .ghosttySearchFocus, ... ) call and instead route
through the existing restoreSearchFocus(window:) helper (pass the current
window) so the foreign-search-responder checks are applied; this preserves the
intended behavior while preventing cross-split focus stealing.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 3279-3294: The code treats searchState != nil as if the find field
is actively focused; change that check to confirm the find UI actually has focus
before returning .findField. In captureFocusIntent (and the analogous block at
3296-3304) replace the plain searchState != nil condition with a focus-aware
check (e.g. searchState != nil &&
Self.responderChainContains(window?.firstResponder, target: findField) or an
equivalent isFirstResponder check against the find field/view), so only when the
find field is the current first responder do we return .browser(.findField);
otherwise fall through to preferredFocusIntent.
- Around line 3325-3328: The switch case for `.findField` currently returns
false when searchState is nil which blocks
restoreFocusIntent(.browser(.findField)); instead always call startFind() (which
is idempotent and will create searchState) and return true. Concretely, remove
or change the guard that returns false and ensure the `.findField` branch simply
calls startFind() and then returns true so explicit restoration can reopen the
find field.

In `@Sources/Panels/TerminalPanel.swift`:
- Around line 209-213: The override restoreFocusIntent currently only handles
the .terminal case and returns false for PanelFocusIntent.panel; modify
restoreFocusIntent to preserve the generic .panel fallback by allowing case
.panel (or a default) to call hostedView.restorePanelFocusIntent or call focus()
as recovery so callers that pass .panel still get a true/focus fallback; update
the guard/logic in restoreFocusIntent to delegate .terminal to
hostedView.restorePanelFocusIntent(target) and handle .panel by delegating to
hostedView.restorePanelFocusIntent or invoking focus() before returning true.

In `@Sources/Workspace.swift`:
- Around line 3141-3145: The restore path currently calls
panel.preferredFocusIntentForActivation() twice and may autofocus the browser
omnibar even when the focus change wasn't user-driven; capture the result of
preferredFocusIntentForActivation() once into a local (e.g. let intent =
panel.preferredFocusIntentForActivation()), pass that same intent to
panel.restoreFocusIntent(...) and only call
maybeAutoFocusBrowserAddressBarOnPanelFocus(browserPanel, trigger:) when intent
indicates an explicit focus intent (or socket-allowed per your socket policy)
and the panel is a BrowserPanel whose intent == .browser(.webView); this
preserves the existing applyTabSelectionNow(...) gating and prevents
CLI/socket-initiated restores from stealing omnibar focus.

---

Outside diff comments:
In `@Sources/Panels/BrowserPanelView.swift`:
- Around line 1499-1555: The panel currently calls panel.noteWebViewFocused()
only when the eager window.makeFirstResponder(panel.webView) succeeds, missing
cases where restoreAddressBarPageFocusIfNeeded or the fallback
makeFirstResponder actually result in the WebView becoming first responder;
update the restoreAddressBarPageFocusIfNeeded completion block to record WebKit
focus there as well: after computing hasWebViewResponder (using
browserFocusResponderChainContains(window.firstResponder, target:
panel.webView)), if hasWebViewResponder call panel.noteWebViewFocused();
otherwise call window.makeFirstResponder(panel.webView) (the existing
fallbackFocusedWebView) and if that returns true call
panel.noteWebViewFocused(); keep the NotificationCenter.post call as-is. This
ensures panel.noteWebViewFocused() is invoked after the final handoff attempt
(restore or fallback) rather than only the eager attempt.

---

Duplicate comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 6829-6839: Before returning when a foreign search field is the
first responder (the branch checking window.firstResponder along with
isSearchOverlayOrDescendant(...) and !isCurrentSurfaceSearchResponder(...)),
ensure you clear this surface's focus state by calling
terminalSurface?.setFocus(false) (or equivalent) before the early return so the
cursor stops blinking; keep the existing debug dlog and return behavior but
insert terminalSurface?.setFocus(false) immediately prior to the return to
guarantee focus is cleared for this surface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47cf33bb-a723-4f53-9886-a9bbead2604d

📥 Commits

Reviewing files that changed from the base of the PR and between 81ceee1 and 4fa48b2.

📒 Files selected for processing (9)
  • Sources/BrowserWindowPortal.swift
  • Sources/ContentView.swift
  • Sources/Find/BrowserSearchOverlay.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Panels/Panel.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/Workspace.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/BrowserWindowPortal.swift

Comment thread Sources/ContentView.swift Outdated
Comment on lines +41 to 51
private func requestSearchFieldFocus(maxAttempts: Int = 3, origin: String) {
guard maxAttempts > 0 else { return }
logFocusState("request.begin origin=\(origin) remaining=\(maxAttempts)")
isSearchFieldFocused = true
DispatchQueue.main.async {
logFocusState("request.afterAsync origin=\(origin) remaining=\(maxAttempts)")
}
guard maxAttempts > 1 else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
requestSearchFieldFocus(maxAttempts: maxAttempts - 1)
requestSearchFieldFocus(maxAttempts: maxAttempts - 1, origin: origin)
}

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "BrowserSearchOverlay.swift" --type f

Repository: manaflow-ai/cmux

Length of output: 99


🏁 Script executed:

cat -n Sources/Find/BrowserSearchOverlay.swift

Repository: manaflow-ai/cmux

Length of output: 10467


🏁 Script executed:

rg "onFieldDidFocus" -A 3 -B 3

Repository: manaflow-ai/cmux

Length of output: 6250


🌐 Web query:

SwiftUI @focusstate onChange fires when focus actually lands or when state is written

💡 Result:

.onChange(of: someFocusState) runs when the @FocusState wrapped value actually changes (i.e., when SwiftUI/OS reports that focus entered/left a bound view), not at the instant you assign to it. Setting a @FocusState value is effectively a request to move focus; the wrapped value may update later (often next update cycle / after animations), so reading it immediately after writing can still show the old value. [1], [2], [3]

In other words:

  • User taps / focus moves → SwiftUI updates @FocusState → your .onChange fires. [3]
  • You assign to @FocusState → SwiftUI tries to move focus → when focus actually changes, @FocusState updates and then .onChange fires (not necessarily synchronously with your assignment). [1], [2]

Sources: [1] [2] [3]


onFieldDidFocus fires on the initial @FocusState write, but retries won't re-trigger the callback even if focus finally lands.

Setting isSearchFieldFocused = true (line 44) is a focus request; the .onChange at lines 139–143 fires when focus actually lands and the state transitions from false to true. However, on retry attempts (lines 49–50), isSearchFieldFocused is already true, so the state won't transition again. If the initial focus request fails and focus lands on a retry, the callback never fires, even though the field is now confirmed as first responder.

This breaks the downstream contract in GhosttyTerminalView.swift, which expects the callback to signal confirmed focus and unfocus the terminal surface. Compare to SurfaceSearchOverlay.swift, which correctly calls the callback from beginEditing() — an AppKit confirmed focus callback.

Use an AppKit responder callback (e.g., NSTextViewDelegate.textDidBeginEditing) to signal confirmed focus, not a @FocusState write.

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

In `@Sources/Find/BrowserSearchOverlay.swift` around lines 41 - 51, The focus
request currently sets isSearchFieldFocused in requestSearchFieldFocus, but
retries don't retrigger the confirmed-focus callback because the `@FocusState`
value is already true; instead, wire the confirmed-focus signal to the actual
AppKit responder callback (e.g., NSTextViewDelegate.textDidBeginEditing /
beginEditing()) or explicitly call the existing onFieldDidFocus callback from
the real focus-change handler. Concretely: stop relying solely on setting
isSearchFieldFocused in requestSearchFieldFocus; ensure the confirmed-focus
callback used by GhosttyTerminalView (referenced as onFieldDidFocus /
beginEditing) is invoked from the AppKit delegate or from the .onChange handler
that observes the real focus transition (the handler currently at lines
~139–143), or call that same callback from beginEditing as SurfaceSearchOverlay
does, so retries that finally succeed still signal confirmed focus.

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/Panels/BrowserPanel.swift
Comment thread Sources/Panels/BrowserPanel.swift
Comment thread Sources/Panels/TerminalPanel.swift
Comment thread Sources/Workspace.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 6 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="Sources/ContentView.swift">

<violation number="1" location="Sources/ContentView.swift:5337">
P2: When both optional UUIDs are `nil`, `nil == nil` is `true` in Swift, so this can return `true` even when no address bar was focused. Guard against this by requiring `focusedBrowserAddressBarPanelId` to be non-nil.</violation>
</file>

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

Comment thread Sources/ContentView.swift
focusedPanelId: UUID
focusedPanelId: UUID?
) -> Bool {
focusedPanelIsBrowser && focusedBrowserAddressBarPanelId == focusedPanelId

@cubic-dev-ai cubic-dev-ai Bot Mar 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When both optional UUIDs are nil, nil == nil is true in Swift, so this can return true even when no address bar was focused. Guard against this by requiring focusedBrowserAddressBarPanelId to be non-nil.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/ContentView.swift, line 5337:

<comment>When both optional UUIDs are `nil`, `nil == nil` is `true` in Swift, so this can return `true` even when no address bar was focused. Guard against this by requiring `focusedBrowserAddressBarPanelId` to be non-nil.</comment>

<file context>
@@ -5329,6 +5329,14 @@ struct ContentView: View {
+        focusedBrowserAddressBarPanelId: UUID?,
+        focusedPanelId: UUID?
+    ) -> Bool {
+        focusedPanelIsBrowser && focusedBrowserAddressBarPanelId == focusedPanelId
+    }
+
</file context>
Suggested change
focusedPanelIsBrowser && focusedBrowserAddressBarPanelId == focusedPanelId
focusedPanelIsBrowser && focusedBrowserAddressBarPanelId != nil && focusedBrowserAddressBarPanelId == focusedPanelId
Fix with Cubic

@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: 3

♻️ Duplicate comments (1)
Sources/ContentView.swift (1)

5500-5505: ⚠️ Potential issue | 🟠 Major

Backdrop hits still lose the clicked subcontrol intent.

These call sites now delegate to commandPaletteRestoreFocusTarget(...), but that helper recomputes intent from captureFocusIntent(in:) while the palette field is still first responder. So a backdrop click on a browser address bar, browser find field, or terminal find field can still collapse back to .browser(.webView) / .terminal(.surface) instead of restoring the clicked control. Please thread the hit-derived PanelFocusIntent through here, and only fall back to capture/default intent when the hit was actually the panel body.

Also applies to: 5516-5521, 5558-5563, 5569-5579

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

In `@Sources/ContentView.swift` around lines 5500 - 5505, The helper
commandPaletteRestoreFocusTarget(...) currently recomputes intent via
captureFocusIntent(in:) which loses the actual subcontrol clicked; change the
call sites that currently call
commandPaletteRestoreFocusTarget(workspaceId:panelId:fallbackIntent:in:) to pass
the hit-derived PanelFocusIntent (from the backdrop hit) into the helper and
update commandPaletteRestoreFocusTarget to accept an optional hitIntent
parameter; inside commandPaletteRestoreFocusTarget, use the provided hitIntent
when the hit is not the panel body and only call captureFocusIntent(in:) or use
fallbackIntent when the hit indicates the panel body or hitIntent is nil. Ensure
references to PanelFocusIntent and captureFocusIntent(in:) are used exactly as
named so reviewers can locate edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 6866-6880: In capturePanelFocusIntent, when a search is open
(surfaceView.terminalSurface?.searchState != nil) ensure a pending search-field
intent is preserved by checking searchFocusTarget == .searchField (or
isCurrentSurfaceSearchResponder(firstResponder)) before treating surfaceView
being firstResponder as meaning .surface; specifically, if firstResponder is
surfaceView or a descendant but searchFocusTarget == .searchField (or
isCurrentSurfaceSearchResponder returns true), return .findField instead of
.surface so the pending find-field intent isn't discarded.
- Around line 6979-6990: isCurrentSurfaceSearchResponder currently treats any
descendant of the surface as a search responder; restrict it to the actual
search-overlay controls by resolving the responder (as you already do) and then
verifying that the resolvedResponder is either the search overlay view or the
actual search field/editor instances instead of any descendant. Concretely, in
isCurrentSurfaceSearchResponder(_:) after computing resolvedResponder, check
whether it equals or isDescendant(of:) the specific search overlay container
(e.g. your search overlay view instance) or is the known search field/editor
control(s) (e.g. the find/search NSTextField or its field editor) used by your
search UI, and return true only in those cases so capturePanelFocusIntent(...)
and resignOwnedFirstResponderIfNeeded(...) only treat real search responders as
active.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 3333-3337: The .webView branch calls noteWebViewFocused() then
focus() but focus() currently returns early when searchState != nil, so
restoring from a captured .browser(.webView) reports success but leaves the page
unfocused; update the logic so focusing honors .webView restores by either
adding a parameter to focus(e.g. force: Bool) or a new method (e.g.
forceFocusWebView()) that bypasses the searchState early-exit and directly makes
the web view first responder, or modify focus() to check the restoration target
(from the switch) and skip the searchState bail when called for .webView; ensure
you adjust the call in the switch (currently calling noteWebViewFocused() and
focus()) to use the new force/ignore-search variant so the page actually becomes
focused.

---

Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 5500-5505: The helper commandPaletteRestoreFocusTarget(...)
currently recomputes intent via captureFocusIntent(in:) which loses the actual
subcontrol clicked; change the call sites that currently call
commandPaletteRestoreFocusTarget(workspaceId:panelId:fallbackIntent:in:) to pass
the hit-derived PanelFocusIntent (from the backdrop hit) into the helper and
update commandPaletteRestoreFocusTarget to accept an optional hitIntent
parameter; inside commandPaletteRestoreFocusTarget, use the provided hitIntent
when the hit is not the panel body and only call captureFocusIntent(in:) or use
fallbackIntent when the hit indicates the panel body or hitIntent is nil. Ensure
references to PanelFocusIntent and captureFocusIntent(in:) are used exactly as
named so reviewers can locate edits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 137fd52d-862d-418a-af6d-015372abe13f

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa48b2 and 49dc0e4.

📒 Files selected for processing (8)
  • Sources/ContentView.swift
  • Sources/Find/BrowserSearchOverlay.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Panels/Panel.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/Workspace.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/Find/BrowserSearchOverlay.swift
  • Sources/Panels/Panel.swift

Comment on lines +6866 to +6880
func capturePanelFocusIntent(in window: NSWindow?) -> TerminalPanelFocusIntent {
if surfaceView.terminalSurface?.searchState != nil {
if let firstResponder = window?.firstResponder as? NSView,
(firstResponder === surfaceView || firstResponder.isDescendant(of: surfaceView)) {
return .surface
}
if let firstResponder = window?.firstResponder,
isCurrentSurfaceSearchResponder(firstResponder) {
return .findField
}
if searchFocusTarget == .searchField {
return .findField
}
}
return .surface

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 | 🟠 Major

Preserve pending find-field intent while AppKit focus is still catching up.

When the find bar is open, AppKit can still report surfaceView as first responder for a turn before the search field claim lands. Returning .surface in that state discards the already-selected .searchField intent, so the next activation can restore terminal focus instead of the find field.

💡 Suggested fix
     func capturePanelFocusIntent(in window: NSWindow?) -> TerminalPanelFocusIntent {
         if surfaceView.terminalSurface?.searchState != nil {
+            if searchFocusTarget == .searchField {
+                return .findField
+            }
             if let firstResponder = window?.firstResponder as? NSView,
                (firstResponder === surfaceView || firstResponder.isDescendant(of: surfaceView)) {
                 return .surface
             }
             if let firstResponder = window?.firstResponder,
                isCurrentSurfaceSearchResponder(firstResponder) {
                 return .findField
             }
-            if searchFocusTarget == .searchField {
-                return .findField
-            }
         }
         return .surface
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 6866 - 6880, In
capturePanelFocusIntent, when a search is open
(surfaceView.terminalSurface?.searchState != nil) ensure a pending search-field
intent is preserved by checking searchFocusTarget == .searchField (or
isCurrentSurfaceSearchResponder(firstResponder)) before treating surfaceView
being firstResponder as meaning .surface; specifically, if firstResponder is
surfaceView or a descendant but searchFocusTarget == .searchField (or
isCurrentSurfaceSearchResponder returns true), return .findField instead of
.surface so the pending find-field intent isn't discarded.

Comment thread Sources/GhosttyTerminalView.swift
Comment thread Sources/Panels/BrowserPanel.swift
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@lawrencecchen
lawrencecchen merged commit ec10dfd into main Mar 11, 2026
12 checks passed
@lawrencecchen
lawrencecchen deleted the task-browser-find-focus-logs branch March 11, 2026 02:31

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 579d18fca9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let firstResponderSummary: String = {
guard let firstResponder = window?.firstResponder else { return "nil" }
if let editor = firstResponder as? NSTextView, editor.isFieldEditor {
let delegateSummary = editor.delegate.map { String(describing: type(of: $0)) } ?? "nil"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop reading field-editor delegate in debug logging

NSTextView.delegate is an unsafe-unretained AppKit property, so reading it during responder teardown can crash with an unowned-reference trap; this helper now does that on every browser find-overlay log path. Since overlay mount/remove frequently coincides with first-responder churn, this reintroduces the exact crash pattern already called out elsewhere in the codebase (see the guard comment in AppDelegate around field-editor delegate access).

Useful? React with 👍 / 👎.

bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
* Add browser find focus debug logs

* Allow browser find bar focus in portal host

* Add split and terminal find debug logs

* Avoid stealing search focus across splits

* Generalize panel focus restore intent

* Unify split focus intent activation

* Address focus restore review feedback

* Yield inactive panel focus before restore

* Gate browser find focus retries by generation

* Avoid repeated browser focus invalidation

* Keep browser find ownership while find bar is open

This branch was successfully deployed

1 active deployment
Preview — 579d18fc Deployed Mar 11, 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