Skip to content

Add find-in-page (Cmd+F) for browser panels - #875

Merged
lawrencecchen merged 2 commits into
manaflow-ai:mainfrom
y-agatsuma:837-browser-find-in-page
Mar 5, 2026
Merged

lawrencecchen merged 2 commits into
manaflow-ai:mainfrom
y-agatsuma:837-browser-find-in-page

Conversation

@y-agatsuma

@y-agatsuma y-agatsuma commented Mar 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #837

  • Adds Cmd+F find-in-page to browser panels using JavaScript TreeWalker + <mark> highlights
  • Matches the existing terminal find bar UI (draggable overlay, match counter, keyboard nav)
  • Routes TabManager find commands (Cmd+F/G) to browser when a browser panel is focused

Implementation

  • BrowserFindJavaScript — JS generation for search/next/prev/clear with visibility filtering (skips script/style/hidden/aria-hidden elements) and stale DOM guards
  • BrowserSearchOverlay — SwiftUI overlay matching SurfaceSearchOverlay design, with IME-safe onSubmit instead of onKeyPress(.return)
  • BrowserSearchState — Observable state (needle/selected/total) with Combine debounce
  • TabManager routing — startSearch/findNext/findPrevious/hideFind dispatch to browser when focused
  • Navigation cleanup — Clears find highlights on didFinish and didFailNavigation

Test plan

  • Cmd+F opens find bar in browser panel
  • Text input highlights matches with count display
  • Return / Shift+Return navigates next/previous
  • Cmd+G / Cmd+Shift+G from menu works
  • Esc closes find bar and clears highlights
  • Cmd+F re-press re-focuses search field
  • Page navigation auto-closes find bar
  • Terminal Cmd+F still works when terminal is focused
  • Split panes have independent search states
  • Drag to snap find bar to corners
  • 0 matches shows "0/0"
  • Japanese IME input + Enter confirms text (does not trigger next)
  • Unit tests pass (cmuxTests/BrowserFindJavaScriptTests)

Note

Terminal find bar has the same Japanese IME issue (onKeyPress(.return) intercepts IME confirm). Will address in a separate PR.


Summary by cubic

Adds find-in-page (Cmd+F) to browser panels with in-page highlights and a draggable find bar. Implements Linear #837 and routes search commands to the focused browser panel while keeping terminal find behavior unchanged.

  • New Features
    • JavaScript TreeWalker wraps matches in , skips hidden/script/style/aria-hidden elements, and guards stale nodes.
    • SwiftUI BrowserSearchOverlay with match counter, IME-safe onSubmit, drag-to-corner, Esc to close, and Cmd+F re-focus via notification.
    • Keyboard: Return/Shift+Return for next/previous, plus Cmd+G/Cmd+Shift+G.
    • Per-pane BrowserSearchState with debounce; highlights auto-clear on navigation.
    • TabManager routing for startSearch/findNext/findPrevious/hideFind; terminal find still active when terminal is focused.
    • Added unit tests for BrowserFindJavaScript.

Written for commit 5f37ef8. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added in-page search functionality for browser content with automatic highlighting of matches
    • Search overlay with text input field, match counter, and next/previous navigation buttons
    • Repositionable search overlay that can dock to any corner of the screen
    • Integrated browser search with the application's find system for consistent behavior
  • Tests

    • Added comprehensive test coverage for search functionality

JavaScript-based find using TreeWalker + <mark> highlights with
match counter, next/previous navigation, and drag-to-corner overlay
matching the existing terminal find bar.

- BrowserFindJavaScript: JS generation for search/next/prev/clear
- BrowserSearchOverlay: SwiftUI overlay with IME-safe onSubmit
- BrowserSearchState: Observable state (needle/selected/total)
- TabManager routing: Cmd+F/G dispatches to browser when focused
- Visibility filter: skips script/style/hidden/aria-hidden elements
- Stale DOM guard: isConnected check in next/previous scripts
- Navigation cleanup: clears find on didFinish and didFailNavigation
@vercel

vercel Bot commented Mar 4, 2026

Copy link
Copy Markdown

@y-agatsuma is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Mar 4, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change implements find-in-page functionality for the embedded browser panel. It introduces JavaScript-based search utilities (BrowserFindJavaScript), a SwiftUI search overlay (BrowserSearchOverlay), observable search state management (BrowserSearchState), and wires these into BrowserPanel and the global TabManager find flow with comprehensive test coverage.

Changes

Cohort / File(s) Summary
Browser Find JavaScript Infrastructure
Sources/Find/BrowserFindJavaScript.swift, cmuxTests/BrowserFindJavaScriptTests.swift
Implements JavaScript snippet generation for in-page find operations, including search, navigation (next/previous), and clear functions with DOM state management and string escaping utilities. Test suite covers all methods and edge cases including special character handling.
Search Overlay UI Component
Sources/Find/BrowserSearchOverlay.swift
New SwiftUI view rendering a draggable search bar with text input, status indicator, and Next/Previous/Close buttons. Manages focus, keyboard interactions, corner-anchoring, and debounced callbacks.
Browser Panel State & Integration
Sources/Panels/BrowserPanel.swift
Introduces BrowserSearchState observable class and integrates find subsystem into BrowserPanel with public entry points (startFind, findNext, findPrevious, hideFind), debounced search execution, JavaScript result parsing, and clear-on-navigation logic.
Browser Panel View Integration
Sources/Panels/BrowserPanelView.swift
Adds conditional rendering of BrowserSearchOverlay when search state exists, wiring callbacks to BrowserPanel find methods.
Global Find Flow Integration
Sources/GhosttyTerminalView.swift, Sources/TabManager.swift
Adds browserSearchFocus notification and extends TabManager find methods to route to focused browser panels when active, maintaining terminal find behavior otherwise.
Project Configuration
GhosttyTabs.xcodeproj/project.pbxproj
Registers new source files (BrowserFindJavaScript.swift, BrowserSearchOverlay.swift) and test file in build phases and file references.

Sequence Diagram

sequenceDiagram
    actor User
    participant TabManager
    participant BrowserPanel
    participant BrowserSearchOverlay
    participant BrowserPanel as BP<br/>(JavaScript)
    participant WKWebView

    User->>TabManager: startSearch() / findNext() / findPrevious()
    activate TabManager
    TabManager->>BrowserPanel: startFind() / findNext() / findPrevious()
    deactivate TabManager
    
    activate BrowserPanel
    BrowserPanel->>BrowserSearchOverlay: Create/Update searchState
    BrowserPanel->>BP: Generate JavaScript (searchScript/nextScript)
    deactivate BrowserPanel

    activate BP
    BP->>WKWebView: executeJavaScript()
    activate WKWebView
    WKWebView->>WKWebView: Highlight matches, track state
    WKWebView-->>BP: Return JSON {total, current}
    deactivate WKWebView
    BP->>BrowserPanel: Parse result, update searchState
    deactivate BP

    activate BrowserSearchOverlay
    BrowserSearchOverlay->>User: Display match count & highlights
    deactivate BrowserSearchOverlay

    User->>BrowserSearchOverlay: Click Next/Prev or press Cmd+G
    activate BrowserSearchOverlay
    BrowserSearchOverlay->>BrowserPanel: onNext() / onPrevious()
    deactivate BrowserSearchOverlay
    
    BrowserPanel->>BP: Execute nextScript() / previousScript()
    BP->>WKWebView: executeJavaScript()
    WKWebView-->>BP: Return updated position
    BP->>BrowserPanel: Update searchState
    BrowserSearchOverlay->>User: Reflect new position
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A find bar hops through the web so fine,
With JavaScript marks in a gleaming line,
Next and previous, no need to roam,
Search through the browser, right at home! 🔍

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding find-in-page functionality (Cmd+F) to browser panels.
Linked Issues check ✅ Passed All objectives from issue #837 are met: Cmd+F opens a find bar overlay, match count indicator, next/previous navigation, esc to dismiss, and TabManager routing to browser panels.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing find-in-page for browser panels; no unrelated modifications detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@greptile-apps

greptile-apps Bot commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements find-in-page functionality for browser panels, mirroring the existing terminal find feature. The implementation uses JavaScript TreeWalker to scan and highlight text matches with <mark> elements, avoiding modification of script/style/hidden content through visibility filtering.

Key implementation details:

  • BrowserFindJavaScript generates safe JavaScript with comprehensive string escaping to prevent injection attacks (handles backslashes, quotes, newlines, unicode separators)
  • BrowserSearchOverlay provides a draggable SwiftUI overlay matching the terminal find bar design, using onSubmit instead of onKeyPress(.return) for IME compatibility
  • BrowserSearchState manages search state with Combine-based debouncing (300ms for queries < 3 chars, immediate otherwise)
  • TabManager routes find commands (Cmd+F/G/Shift+G) to the focused panel type (browser vs terminal)
  • Navigation cleanup automatically clears find highlights on page load (didFinish/didFailNavigation)
  • Stale DOM guards using isConnected checks prevent errors when page content changes during search

Architecture:
The implementation follows existing patterns from the terminal find feature, maintaining consistency in UX and code organization. Memory management is sound with proper weak self captures and automatic AnyCancellable cleanup. The JavaScript uses standard DOM APIs (TreeWalker, DocumentFragment) for efficient text scanning and highlighting.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-structured with comprehensive security measures (JavaScript injection prevention through proper escaping), thorough testing (116 lines of unit tests covering edge cases), proper memory management (weak self captures, automatic cleanup), and defensive programming (stale DOM guards, navigation cleanup). The code follows existing architectural patterns and includes IME compatibility improvements over the terminal implementation.
  • No files require special attention

Important Files Changed

Filename Overview
Sources/Find/BrowserFindJavaScript.swift New file implementing JavaScript generation for find-in-page with comprehensive string escaping, visibility filtering, and stale DOM guards
Sources/Find/BrowserSearchOverlay.swift New SwiftUI overlay matching terminal find bar UI, uses IME-safe onSubmit and includes drag-to-snap positioning
Sources/Panels/BrowserPanel.swift Adds BrowserSearchState class and find methods with Combine debounce for performance, properly clears state on navigation
Sources/TabManager.swift Routes find commands (Cmd+F/G) to browser panel when focused, with clean fallback to terminal behavior
cmuxTests/BrowserFindJavaScriptTests.swift Comprehensive unit tests for JavaScript generation and string escaping, covering edge cases and injection prevention

Sequence Diagram

sequenceDiagram
    participant User
    participant TabManager
    participant BrowserPanel
    participant WKWebView
    participant BrowserSearchOverlay

    User->>TabManager: Cmd+F (startSearch)
    TabManager->>BrowserPanel: startFind()
    BrowserPanel->>BrowserPanel: Create BrowserSearchState
    BrowserPanel->>BrowserSearchOverlay: Display overlay
    BrowserSearchOverlay->>User: Focus search field

    User->>BrowserSearchOverlay: Type search query
    BrowserSearchOverlay->>BrowserPanel: Update needle (debounced)
    BrowserPanel->>BrowserPanel: Generate searchScript(query)
    BrowserPanel->>WKWebView: evaluateJavaScript(searchScript)
    WKWebView->>WKWebView: TreeWalker scan + mark elements
    WKWebView-->>BrowserPanel: {total: N, current: 0}
    BrowserPanel->>BrowserSearchOverlay: Update match count

    User->>TabManager: Cmd+G (findNext)
    TabManager->>BrowserPanel: findNext()
    BrowserPanel->>WKWebView: evaluateJavaScript(nextScript)
    WKWebView->>WKWebView: Update current class + scroll
    WKWebView-->>BrowserPanel: {total: N, current: M}
    BrowserPanel->>BrowserSearchOverlay: Update current match

    User->>BrowserSearchOverlay: Esc (close)
    BrowserSearchOverlay->>BrowserPanel: hideFind()
    BrowserPanel->>WKWebView: evaluateJavaScript(clearScript)
    WKWebView->>WKWebView: Remove mark elements
    BrowserPanel->>BrowserPanel: Clear searchState
Loading

Last reviewed commit: 9488aeb

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

🤖 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/Find/BrowserFindJavaScript.swift`:
- Around line 38-42: The script assumes DOM roots exist; update searchScript to
guard before using document.body and document.head: check if document.body
exists before calling document.createTreeWalker (or fall back to
document.documentElement) and only create/use walker when a root is present
(referencing the walker and createTreeWalker usage and isVisible filter), and
check if document.head exists before calling document.head.appendChild(style)
(or fall back to appending to document.documentElement or doing an early return)
so neither TreeWalker creation nor style injection throws when roots are null.

In `@Sources/Find/BrowserSearchOverlay.swift`:
- Around line 45-55: Add unified debug logging by inserting calls to the free
function dlog("...") inside the new handlers wrapped in `#if` DEBUG / `#endif`
blocks; specifically, in the onExitCommand { onClose() } and onSubmit { ... }
handlers add dlog messages (e.g. dlog("BrowserSearchOverlay.onExitCommand") and
dlog("BrowserSearchOverlay.onSubmit
shift=\(NSEvent.modifierFlags.contains(.shift))")) before invoking onClose(),
onPrevious(), or onNext(). Do the same pattern for the other new key/focus/drag
handler functions referenced in this diff (use their function/closure names as
the message), ensuring every debug call is inside `#if` DEBUG / `#endif` and uses
the dlog free function.
- Around line 116-134: The drag gesture is currently applied after the infinite
.frame which causes drags to be recognized outside the visible bar — move the
.gesture(DragGesture()...) modifier so it is attached before the .frame(...) to
limit its hit area to the actual view; keep the existing logic that computes
centerPosition(for:corner:in:barSize:), creates newCenter and chooses newCorner
via closestCorner(to:in:), and updates corner and dragOffset inside
withAnimation. Also add debug logging wrapped in `#if` DEBUG / `#endif` in the
relevant handlers: call dlog(...) in the .onExitCommand handler, the .onSubmit
handler, the .onReceive that manages focus, and inside the DragGesture
.onChanged and .onEnded blocks to log value.translation and the computed
newCorner/newCenter to aid debugging. Ensure all added logs reference the same
identifying symbols (corner, dragOffset, centerPosition, closestCorner) so they
are easy to correlate.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 1456-1471: The current sink on searchState.$needle triggers
executeFindSearch asynchronously without cancelling prior work, so stale JS
responses can overwrite newer selected/total state; modify the implementation to
track and cancel or ignore prior in-flight find requests (e.g., add a stored
Task? findTask or a query token/UUID) inside the BrowserPanel and update
executeFindSearch to either return a cancellable Task or accept/return the query
token, cancel the previous Task before starting a new one (or compare tokens on
response and only apply results when the token matches the latest), and update
references to selected/total only for the latest query; apply the same pattern
for the other occurrences mentioned (lines ~2588-2616 and 2618-2629) referencing
executeFindSearch, searchNeedleCancellable, and the selected/total update logic.
- Around line 1455-1470: Replace any production NSLog that prints the raw search
needle with a privacy-safe debug-only log: remove the needle from the always-on
NSLog calls and instead wrap a dlog call in `#if` DEBUG / `#endif` that logs either
only the panel id or a redacted/masked version of the needle. Specifically,
update the NSLog in the search subscription (references:
searchNeedleCancellable, searchState.$needle, executeFindSearch) so it no longer
emits the raw `needle` to NSLog; if you still need to record it for debugging,
call dlog("Find: browser needle updated panel=%@ needle=%@", id.uuidString,
maskedNeedle) inside a DEBUG guard. Ensure the production path only logs
non-identifying context (e.g., panel id) and that executeFindSearch continues to
receive the full needle unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b8fcf18-233f-4ba7-9988-d714c945fbd2

📥 Commits

Reviewing files that changed from the base of the PR and between e0ec448 and 9488aeb.

📒 Files selected for processing (8)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/Find/BrowserFindJavaScript.swift
  • Sources/Find/BrowserSearchOverlay.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/TabManager.swift
  • cmuxTests/BrowserFindJavaScriptTests.swift

Comment on lines +38 to +42
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{ acceptNode(node) { return isVisible(node.parentElement) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } }
);

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:

# First, check if the file exists and get its structure
fd -t f "BrowserFindJavaScript.swift"

Repository: manaflow-ai/cmux

Length of output: 100


🏁 Script executed:

# If found, read the file to inspect lines 38-42 and 91-99
cat -n Sources/Find/BrowserFindJavaScript.swift | head -120

Repository: manaflow-ai/cmux

Length of output: 5733


🏁 Script executed:

# Check if the file contains any debug events (keys, mouse, focus, splits, tabs)
# These would need to be wrapped with dlog() and `#if` DEBUG blocks
rg -i 'console\.log|debug|key.*event|mouse.*event|focus.*event|split|tab' Sources/Find/BrowserFindJavaScript.swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Verify the complete context around lines 38-42 and 91-99 to see full guard conditions
sed -n '20,45p; 85,105p' Sources/Find/BrowserFindJavaScript.swift

Repository: manaflow-ai/cmux

Length of output: 2012


🏁 Script executed:

# Check if there are other unguarded DOM accesses in the file
rg -n 'document\.(body|head|getElementById|createElement|createTreeWalker)' Sources/Find/BrowserFindJavaScript.swift

Repository: manaflow-ai/cmux

Length of output: 509


🏁 Script executed:

# Verify the context around isVisible to understand the guard logic better
sed -n '28,37p' Sources/Find/BrowserFindJavaScript.swift

Repository: manaflow-ai/cmux

Length of output: 501


Guard against missing DOM roots before TreeWalker and style injection.

searchScript assumes document.body and document.head always exist. In non-HTML documents or during transient navigation states, these can be null, causing exceptions that leave find state broken.

Line 39: createTreeWalker(document.body, ...) executes without checking if document.body exists.
Line 98: document.head.appendChild(style) executes without checking if document.head exists.

Suggested fixes
          const query = "\(escaped)";
          if (!query) return JSON.stringify({total: 0, current: 0});
+         if (!document.body) return JSON.stringify({total: 0, current: 0});

          if (!document.getElementById('__cmux-find-style')) {
            const style = document.createElement('style');
            style.id = '__cmux-find-style';
            style.textContent = `...`;
-           document.head.appendChild(style);
+           (document.head || document.documentElement).appendChild(style);
          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Find/BrowserFindJavaScript.swift` around lines 38 - 42, The script
assumes DOM roots exist; update searchScript to guard before using document.body
and document.head: check if document.body exists before calling
document.createTreeWalker (or fall back to document.documentElement) and only
create/use walker when a root is present (referencing the walker and
createTreeWalker usage and isVisible filter), and check if document.head exists
before calling document.head.appendChild(style) (or fall back to appending to
document.documentElement or doing an early return) so neither TreeWalker
creation nor style injection throws when roots are null.

Comment on lines +45 to +55
.onExitCommand {
onClose()
}
.onSubmit {
// onSubmit fires only after IME composition is committed.
if NSEvent.modifierFlags.contains(.shift) {
onPrevious()
} else {
onNext()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add unified debug logging for new key/focus/drag handlers.

The new handlers currently miss dlog instrumentation in #if DEBUG blocks.

🛠️ Suggested patch
@@
                 .onExitCommand {
+#if DEBUG
+                    dlog("browser.findbar.escape panel=\(panelId.uuidString.prefix(5))")
+#endif
                     onClose()
                 }
                 .onSubmit {
+                    `#if` DEBUG
+                    let isShiftSubmit = NSEvent.modifierFlags.contains(.shift)
+                    dlog("browser.findbar.submit panel=\(panelId.uuidString.prefix(5)) shift=\(isShiftSubmit ? 1 : 0)")
+                    `#endif`
                     // onSubmit fires only after IME composition is committed.
                     if NSEvent.modifierFlags.contains(.shift) {
@@
             .onReceive(NotificationCenter.default.publisher(for: .browserSearchFocus)) { notification in
                 guard let notifiedPanelId = notification.object as? UUID,
                       notifiedPanelId == panelId else { return }
+#if DEBUG
+                dlog("browser.findbar.focusRequest panel=\(panelId.uuidString.prefix(5))")
+#endif
                 DispatchQueue.main.async {
                     isSearchFieldFocused = true
                 }
             }
@@
                     .onEnded { value in
+#if DEBUG
+                        dlog("browser.findbar.drag.end panel=\(panelId.uuidString.prefix(5)) dx=\(Int(value.translation.width)) dy=\(Int(value.translation.height))")
+#endif
                         let centerPos = centerPosition(for: corner, in: geo.size, barSize: barSize)

As per coding guidelines, **/*.swift: All debug events (keys, mouse, focus, splits, tabs) must go to the unified debug log, with free function dlog("message") wrapping all call sites in #if DEBUG / #endif.

Also applies to: 100-106, 118-133

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

In `@Sources/Find/BrowserSearchOverlay.swift` around lines 45 - 55, Add unified
debug logging by inserting calls to the free function dlog("...") inside the new
handlers wrapped in `#if` DEBUG / `#endif` blocks; specifically, in the
onExitCommand { onClose() } and onSubmit { ... } handlers add dlog messages
(e.g. dlog("BrowserSearchOverlay.onExitCommand") and
dlog("BrowserSearchOverlay.onSubmit
shift=\(NSEvent.modifierFlags.contains(.shift))")) before invoking onClose(),
onPrevious(), or onNext(). Do the same pattern for the other new key/focus/drag
handler functions referenced in this diff (use their function/closure names as
the message), ensuring every debug call is inside `#if` DEBUG / `#endif` and uses
the dlog free function.

Comment on lines +116 to +134
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: corner.alignment)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation
}
.onEnded { value in
let centerPos = centerPosition(for: corner, in: geo.size, barSize: barSize)
let newCenter = CGPoint(
x: centerPos.x + value.translation.width,
y: centerPos.y + value.translation.height
)
let newCorner = closestCorner(to: newCenter, in: geo.size)
withAnimation(.easeOut(duration: 0.2)) {
corner = newCorner
dragOffset = .zero
}
}
)

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:

#!/bin/bash
set -euo pipefail

# First, check if the file exists and its general structure
echo "=== File existence check ==="
ls -la Sources/Find/BrowserSearchOverlay.swift 2>/dev/null && echo "File found" || echo "File not found"

# Get file size
echo -e "\n=== File size ==="
wc -l Sources/Find/BrowserSearchOverlay.swift 2>/dev/null || echo "Cannot read file"

Repository: manaflow-ai/cmux

Length of output: 251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the file around lines 116-134 to verify the modifier order
echo "=== Code at lines 110-140 ==="
sed -n '110,140p' Sources/Find/BrowserSearchOverlay.swift 2>/dev/null || echo "Cannot read file"

# Also check for dlog usage in the file
echo -e "\n=== dlog usage in file ==="
rg -n 'dlog' Sources/Find/BrowserSearchOverlay.swift 2>/dev/null || echo "No dlog found"

# Check for event handlers and gesture modifiers
echo -e "\n=== Event handlers and gesture modifiers ==="
rg -n -i 'onExitCommand|onSubmit|browserSearchFocus|DragGesture|\.gesture' Sources/Find/BrowserSearchOverlay.swift 2>/dev/null || echo "Pattern not found"

Repository: manaflow-ai/cmux

Length of output: 2028


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full file to understand overall context
echo "=== Full file content ==="
cat -n Sources/Find/BrowserSearchOverlay.swift 2>/dev/null || echo "Cannot read file"

Repository: manaflow-ai/cmux

Length of output: 8186


Drag gesture is scoped too broadly and can steal panel interactions.

Because .gesture is applied after an infinite frame (line 116–117), dragging is recognized outside the visible bar area. Reorder modifiers so the gesture is attached before the infinite frame.

Additionally, key/focus/mouse event handlers must include debug logging per guidelines:

  • .onExitCommand (line 45), .onSubmit (line 48), .onReceive for focus (line 100), and .gesture drag events (line 118) are missing dlog calls wrapped in #if DEBUG / #endif.
Suggested fix for gesture scope
             .padding(padding)
             .offset(dragOffset)
-            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: corner.alignment)
             .gesture(
                 DragGesture()
@@ 
                     }
             )
+            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: corner.alignment)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Find/BrowserSearchOverlay.swift` around lines 116 - 134, The drag
gesture is currently applied after the infinite .frame which causes drags to be
recognized outside the visible bar — move the .gesture(DragGesture()...)
modifier so it is attached before the .frame(...) to limit its hit area to the
actual view; keep the existing logic that computes
centerPosition(for:corner:in:barSize:), creates newCenter and chooses newCorner
via closestCorner(to:in:), and updates corner and dragOffset inside
withAnimation. Also add debug logging wrapped in `#if` DEBUG / `#endif` in the
relevant handlers: call dlog(...) in the .onExitCommand handler, the .onSubmit
handler, the .onReceive that manages focus, and inside the DragGesture
.onChanged and .onEnded blocks to log value.translation and the computed
newCorner/newCenter to aid debugging. Ensure all added logs reference the same
identifying symbols (corner, dragOffset, centerPosition, closestCorner) so they
are easy to correlate.

Comment on lines +1455 to +1470
NSLog("Find: browser search state created panel=%@", id.uuidString)
searchNeedleCancellable = searchState.$needle
.removeDuplicates()
.map { needle -> AnyPublisher<String, Never> in
if needle.isEmpty || needle.count >= 3 {
return Just(needle).eraseToAnyPublisher()
}
return Just(needle)
.delay(for: .milliseconds(300), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.switchToLatest()
.sink { [weak self] needle in
guard let self else { return }
NSLog("Find: browser needle updated panel=%@ needle=%@", self.id.uuidString, needle)
self.executeFindSearch(needle)

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

Avoid logging raw find queries in production.

Line 1469 logs the user’s search text, which is a privacy/compliance risk and should not be emitted via always-on NSLog.

🔧 Suggested fix
-                NSLog("Find: browser search state created panel=%@", id.uuidString)
+                `#if` DEBUG
+                dlog("find.browser.state.created panel=\(id.uuidString.prefix(5))")
+                `#endif`
...
-                        NSLog("Find: browser needle updated panel=%@ needle=%@", self.id.uuidString, needle)
+                        `#if` DEBUG
+                        dlog("find.browser.needle.updated panel=\(self.id.uuidString.prefix(5)) chars=\(needle.count)")
+                        `#endif`
...
-                NSLog("Find: browser search state cleared panel=%@", id.uuidString)
+                `#if` DEBUG
+                dlog("find.browser.state.cleared panel=\(id.uuidString.prefix(5))")
+                `#endif`
As per coding guidelines `**/*.swift`: All debug events (keys, mouse, focus, splits, tabs) must go to the unified debug log, with free function `dlog("message")` wrapping all call sites in `#if DEBUG` / `#endif`.

Also applies to: 1474-1474

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

In `@Sources/Panels/BrowserPanel.swift` around lines 1455 - 1470, Replace any
production NSLog that prints the raw search needle with a privacy-safe
debug-only log: remove the needle from the always-on NSLog calls and instead
wrap a dlog call in `#if` DEBUG / `#endif` that logs either only the panel id or a
redacted/masked version of the needle. Specifically, update the NSLog in the
search subscription (references: searchNeedleCancellable, searchState.$needle,
executeFindSearch) so it no longer emits the raw `needle` to NSLog; if you still
need to record it for debugging, call dlog("Find: browser needle updated
panel=%@ needle=%@", id.uuidString, maskedNeedle) inside a DEBUG guard. Ensure
the production path only logs non-identifying context (e.g., panel id) and that
executeFindSearch continues to receive the full needle unchanged.

Comment on lines +1456 to +1471
searchNeedleCancellable = searchState.$needle
.removeDuplicates()
.map { needle -> AnyPublisher<String, Never> in
if needle.isEmpty || needle.count >= 3 {
return Just(needle).eraseToAnyPublisher()
}
return Just(needle)
.delay(for: .milliseconds(300), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
.switchToLatest()
.sink { [weak self] needle in
guard let self else { return }
NSLog("Find: browser needle updated panel=%@ needle=%@", self.id.uuidString, needle)
self.executeFindSearch(needle)
}

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

Guard against stale async find results overwriting newer state.

Current find execution spawns uncancelled tasks, so older JS responses can win the race and update selected/total for an outdated query.

🔧 Suggested fix
-    private var searchNeedleCancellable: AnyCancellable?
+    private var searchNeedleCancellable: AnyCancellable?
+    private var searchExecutionTask: Task<Void, Never>?
...
             } else if oldValue != nil {
                 searchNeedleCancellable = nil
+                searchExecutionTask?.cancel()
+                searchExecutionTask = nil
                 NSLog("Find: browser search state cleared panel=%@", id.uuidString)
                 executeFindClear()
             }
...
     private func executeFindSearch(_ needle: String) {
         guard !needle.isEmpty else {
             executeFindClear()
             searchState?.selected = nil
             searchState?.total = nil
             return
         }
-        Task { `@MainActor` [weak self] in
+        searchExecutionTask?.cancel()
+        searchExecutionTask = Task { `@MainActor` [weak self] in
             guard let self else { return }
             let js = BrowserFindJavaScript.searchScript(query: needle)
             do {
                 let result = try await self.webView.evaluateJavaScript(js)
+                guard !Task.isCancelled, self.searchState?.needle == needle else { return }
                 self.parseFindResult(result)
             } catch {
                 NSLog("Find: browser JS search error: %@", error.localizedDescription)
             }
         }
     }

     private func executeFindClear() {
-        Task { `@MainActor` [weak self] in
+        searchExecutionTask?.cancel()
+        searchExecutionTask = Task { `@MainActor` [weak self] in
             guard let self else { return }
             do {
                 _ = try await self.webView.evaluateJavaScript(BrowserFindJavaScript.clearScript())
             } catch {
                 NSLog("Find: browser JS clear error: %@", error.localizedDescription)
             }
         }
     }

Also applies to: 2588-2616, 2618-2629

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

In `@Sources/Panels/BrowserPanel.swift` around lines 1456 - 1471, The current sink
on searchState.$needle triggers executeFindSearch asynchronously without
cancelling prior work, so stale JS responses can overwrite newer selected/total
state; modify the implementation to track and cancel or ignore prior in-flight
find requests (e.g., add a stored Task? findTask or a query token/UUID) inside
the BrowserPanel and update executeFindSearch to either return a cancellable
Task or accept/return the query token, cancel the previous Task before starting
a new one (or compare tokens on response and only apply results when the token
matches the latest), and update references to selected/total only for the latest
query; apply the same pattern for the other occurrences mentioned (lines
~2588-2616 and 2618-2629) referencing executeFindSearch,
searchNeedleCancellable, and the selected/total update logic.

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

4 issues found across 8 files

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/Find/BrowserSearchOverlay.swift">

<violation number="1" location="Sources/Find/BrowserSearchOverlay.swift:4">
P3: BrowserSearchOverlay is largely a copy of SurfaceSearchOverlay. Consider extracting a shared generic overlay to avoid duplicated UI/drag logic that will need to be updated in two places.</violation>
</file>

<file name="Sources/Find/BrowserFindJavaScript.swift">

<violation number="1" location="Sources/Find/BrowserFindJavaScript.swift:24">
P2: Add a null check for `document.body` to prevent a `TypeError` crash when searching in non-HTML or incomplete documents.</violation>

<violation number="2" location="Sources/Find/BrowserFindJavaScript.swift:30">
P1: SVG elements are not correctly skipped because their `tagName` is lowercase (`"svg"`). Convert `el.tagName` to uppercase before checking the `SKIP_TAGS` set.</violation>

<violation number="3" location="Sources/Find/BrowserFindJavaScript.swift:54">
P0: String length misalignment caused by `.toLowerCase()` will permanently corrupt text content. Use `RegExp` with the case-insensitive flag instead.</violation>
</file>

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

const parts = [];
let lastEnd = 0;
while (true) {
const idx = lowerText.indexOf(lowerQuery, startIndex);

@cubic-dev-ai cubic-dev-ai Bot Mar 4, 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.

P0: String length misalignment caused by .toLowerCase() will permanently corrupt text content. Use RegExp with the case-insensitive flag instead.

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

<comment>String length misalignment caused by `.toLowerCase()` will permanently corrupt text content. Use `RegExp` with the case-insensitive flag instead.</comment>

<file context>
@@ -0,0 +1,207 @@
+            const parts = [];
+            let lastEnd = 0;
+            while (true) {
+              const idx = lowerText.indexOf(lowerQuery, startIndex);
+              if (idx === -1) break;
+              parts.push({ start: idx, end: idx + query.length });
</file context>
Fix with Cubic

const SKIP_TAGS = new Set(['SCRIPT','STYLE','NOSCRIPT','TEMPLATE','IFRAME','SVG']);
const isVisible = (el) => {
while (el && el !== document.body) {
if (SKIP_TAGS.has(el.tagName)) return false;

@cubic-dev-ai cubic-dev-ai Bot Mar 4, 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.

P1: SVG elements are not correctly skipped because their tagName is lowercase ("svg"). Convert el.tagName to uppercase before checking the SKIP_TAGS set.

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

<comment>SVG elements are not correctly skipped because their `tagName` is lowercase (`"svg"`). Convert `el.tagName` to uppercase before checking the `SKIP_TAGS` set.</comment>

<file context>
@@ -0,0 +1,207 @@
+          const SKIP_TAGS = new Set(['SCRIPT','STYLE','NOSCRIPT','TEMPLATE','IFRAME','SVG']);
+          const isVisible = (el) => {
+            while (el && el !== document.body) {
+              if (SKIP_TAGS.has(el.tagName)) return false;
+              if (el.getAttribute('aria-hidden') === 'true') return false;
+              const st = getComputedStyle(el);
</file context>
Fix with Cubic

\(clearBody)

const query = "\(escaped)";
if (!query) return JSON.stringify({total: 0, current: 0});

@cubic-dev-ai cubic-dev-ai Bot Mar 4, 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: Add a null check for document.body to prevent a TypeError crash when searching in non-HTML or incomplete documents.

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

<comment>Add a null check for `document.body` to prevent a `TypeError` crash when searching in non-HTML or incomplete documents.</comment>

<file context>
@@ -0,0 +1,207 @@
+          \(clearBody)
+
+          const query = "\(escaped)";
+          if (!query) return JSON.stringify({total: 0, current: 0});
+
+          const lowerQuery = query.toLowerCase();
</file context>
Fix with Cubic

import Bonsplit
import SwiftUI

struct BrowserSearchOverlay: View {

@cubic-dev-ai cubic-dev-ai Bot Mar 4, 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.

P3: BrowserSearchOverlay is largely a copy of SurfaceSearchOverlay. Consider extracting a shared generic overlay to avoid duplicated UI/drag logic that will need to be updated in two places.

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

<comment>BrowserSearchOverlay is largely a copy of SurfaceSearchOverlay. Consider extracting a shared generic overlay to avoid duplicated UI/drag logic that will need to be updated in two places.</comment>

<file context>
@@ -0,0 +1,183 @@
+import Bonsplit
+import SwiftUI
+
+struct BrowserSearchOverlay: View {
+    let panelId: UUID
+    @ObservedObject var searchState: BrowserSearchState
</file context>
Fix with Cubic

@lawrencecchen

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

ℹ️ 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".

@lawrencecchen

Copy link
Copy Markdown
Contributor

Thank you for the contribution! Will merge for now, but there's a slight issue, notice the letter "A" in "Avoid":

Screen.Recording.2026-03-04.at.4.13.10.PM.mov

Additionally the cmd+f popover is slightly covering the browser omnibar.

Would appreciate follow-up PRs for those!

@lawrencecchen
lawrencecchen merged commit 76bdf76 into manaflow-ai:main Mar 5, 2026
9 of 10 checks passed
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…ow-ai#875)

JavaScript-based find using TreeWalker + <mark> highlights with
match counter, next/previous navigation, and drag-to-corner overlay
matching the existing terminal find bar.

- BrowserFindJavaScript: JS generation for search/next/prev/clear
- BrowserSearchOverlay: SwiftUI overlay with IME-safe onSubmit
- BrowserSearchState: Observable state (needle/selected/total)
- TabManager routing: Cmd+F/G dispatches to browser when focused
- Visibility filter: skips script/style/hidden/aria-hidden elements
- Stale DOM guard: isConnected check in next/previous scripts
- Navigation cleanup: clears find on didFinish and didFailNavigation

Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
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.

Add find-in-page (Cmd+F) to embedded browser

2 participants