Skip to content

feat(search): Cmd+F-style global all-tabs search palette - #5591

Open
Supersynergy wants to merge 1 commit into
manaflow-ai:mainfrom
Supersynergy:feat/global-search-toggles
Open

Supersynergy wants to merge 1 commit into
manaflow-ai:mainfrom
Supersynergy:feat/global-search-toggles

Conversation

@Supersynergy

@Supersynergy Supersynergy commented Jun 8, 2026 •

Copy link
Copy Markdown

What

Re-cuts this PR on current main after the menubar global search base landed upstream. Keeps the existing local FTS5 architecture and adds a Cmd+F-style global palette: scope icons, result counter, previous/next/close controls, and keyboard traversal across all open windows/tabs/panels.

Shortcut contract

  • Command-F stays focused-panel Find.
  • Command-Shift-F stays Find in Directory.
  • Option-Command-F opens Global Search.
  • Command-G / Option-Command-G move next/previous while the global palette is open.

Review fixes

  • Resolves branch conflicts by re-cutting onto current main.
  • Splits search palette support types into separate Swift files.
  • Uses DispatchSourceTimer plus stored cancellable tasks already in main; no Task.sleep debounce.
  • Preserves one AppDelegate focus route via GlobalSearchCoordinator preview/activate.
  • Adds localized tooltip strings and shortcut regression coverage.

Verification

  • swiftc -parse search sources: pass
  • python3 -m json.tool Resources/Localizable.xcstrings: pass
  • ./scripts/check-pbxproj.sh: pass
  • ./scripts/lint-pbxproj-test-wiring.sh: pass
  • git diff --check: pass
  • grepgod review HEAD~1..HEAD: no diff findings

Blocked locally: full xcodebuild/reload.sh cannot run because this machine only has CommandLineTools selected and no Xcode.app installed. reload.sh reaches xcodebuild and fails with: active developer directory /Library/Developer/CommandLineTools is a command line tools instance.

Expected external noise: Vercel checks require Manaflow team authorization and are not code failures.

@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

@Supersynergy 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 Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements a comprehensive global cross-window search feature for the cmux application. It introduces a persistent SQLite FTS5 search index, a multi-component ranking pipeline (combining BM25, recency, and click-history priors), dual UI entry points (titlebar inline field and menubar status-item popover), global hotkey activation, and content capture hooks for browser and markdown panels. All components are wired together via AppDelegate, NotificationCenter, and FeedCoordinator focus events.

Changes

Global Search Feature

Layer / File(s) Summary
SearchIndex: FTS5 actor and data contracts
Sources/Search/SearchIndex.swift
SearchIndex actor manages a SQLite FTS5 virtual table for storing indexed text chunks (terminal, browser, markdown). Defines Hit and Kind types; implements upsert/search/purge operations with prepared statements.
SmartRanker: Result ranking and click feedback
Sources/Search/SmartRanker.swift
Reorders hits by combining inverted BM25, recency boost, and Thompson-style click-history priors. Persists click stats to JSON; provides rank(), reward(), and recordImpressions() feedback methods.
GlobalSearchHotkey: Process-wide keyboard activation
Sources/Search/GlobalSearchHotkey.swift
Registers Carbon hotkeys (⇧⌘F, ⌥⌘F) at process level. When pressed, posts .cmuxFocusTitlebarSearch if a key window exists, otherwise toggles MenubarSearchPopover.
MenubarSearchPopover: Status-item search UI
Sources/Search/MenubarSearchPopover.swift
NSPopover-backed status bar item with search palette: query field, scope toggles (terminal/browser/markdown/title), and results list. Selecting a hit posts .cmuxJumpToSearchHit and filters by active scopes.
TitlebarSearchField: Inline titlebar search box
Sources/Search/TitlebarSearchField.swift
SwiftUI search field in titlebar with scope-prefix parsing (t:, b:, m:, w:), SmartRanker result ranking, and keyboard navigation (arrows, Cmd+number, Escape). Posts selection and debounced (120ms) preview notifications.
Content capture hooks: Browser and Markdown indexing
Sources/Search/BrowserCaptureHook.swift, Sources/Search/MarkdownCaptureHook.swift
CmuxBrowserCaptureHook installs as WKNavigationDelegate, debounces page loads, extracts body text via JavaScript, and upserts with URL anchor. CmuxMarkdownCaptureHook provides a static feed method for markdown upsertion.
App integration: AppDelegate wiring and titlebar placement
Sources/AppDelegate.swift, Sources/Update/UpdateTitlebarAccessory.swift
AppDelegate adds lazy SearchIndex property backed by cmux/search.db. During launch, installs hotkey/palette behavior, sets up menubar status item, and wires NotificationCenter observers (.cmuxJumpToSearchHit, .cmuxPreviewSearchHit) to call FeedCoordinator.focus. TitlebarSearchField inserted into titlebar controls.
Design documentation
docs/menubar-global-search.md
Comprehensive design doc detailing feature UI, scopes, hotkeys, multi-component pipeline (FTS5 recall → SmartRanker → event-based focus bridge), phased implementation roadmap, storage location, and non-goals.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • manaflow-ai/cmux#3908: Both PRs implement the menubar global search feature, introducing SearchIndex, MenubarSearchPopover, and global hotkey integration through AppDelegate.
  • manaflow-ai/cmux#5062: Both PRs modify the titlebar accessory UI in Sources/Update/UpdateTitlebarAccessory.swift (the TitlebarControlsView layout), so spacing and layout metric changes may interact.

Poem

🔍 A search emerges from the code so deep,
Through FTS5 vaults where queries leap,
With hotkey chords and ranking wise,
Click history lifts the best to rise,
From menubar and titlebar bright,
The rabbit finds what users seek at night! ✨


Important

Pre-merge checks failed

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

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

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Task.sleep in production code at BrowserCaptureHook.swift:78 (600ms) and TitlebarSearchField.swift:171 (120ms), prohibited per swift-blocking-runtime rules. Replace Task.sleep with Timer-based or callback-driven debouncing mechanisms.
Cmux Algorithmic Complexity ❌ Error PR violates algorithmic complexity: SmartRanker performs synchronous JSON encode + atomic disk writes on @MainActor on every impression in hot search UI path, and calls sort() on every keystroke. Defer disk persistence to background queue, debounce impression recording, or batch writes. SmartRanker.rank() sorting should use result limits from FTS5 rather than post-processing.
Cmux Swift Concurrency ❌ Error MarkdownCaptureHook.swift line 25 uses Task.detached for meaningful upsert work without storing, cancelling, or tying to caller lifecycle—violates fire-and-forget Task rule. Replace Task.detached with an actor method or structured async call; or store/track the task for cancellation on panel close.
Cmux Swift @Concurrent ❌ Error MarkdownCaptureHook.feed's Task.detached closure is nonisolated async work expected to leave MainActor but lacks @concurrent annotation. Add @concurrent to the Task.detached closure in MarkdownCaptureHook.feed to properly declare executor boundary crossing under Swift 6's NonisolatedNonsendingByDefault behavior.
Cmux Swift File And Package Boundaries ❌ Error MenubarSearchPopover.swift mixes UI (NSPopover, ScopeToggleBar, SearchPaletteView, HitRow), state, and query logic, violating the one-major-type-per-file rule for new code. Split MenubarSearchPopover.swift into separate files: MenubarSearchPopover.swift, ScopeToggleBar.swift, SearchPaletteView.swift, HitRow.swift.
Cmux Full Internationalization ❌ Error Unlocalized user text: TitlebarSearchField line 188 (kind badge), MenubarSearchPopover scopeLabel (tooltip strings), missing catalog entries for 19 locales. Localize kind display and scopeLabel; add all keys to Localizable.xcstrings with entries for all 19 supported locales.
Cmux Architecture Rethink ❌ Error AppDelegate introduces observer pattern creating split focus ownership: search posts new notifications, AppDelegate observes and calls FeedCoordinator.focus, duplicating existing focus routing. Consolidate routing: have search UI post to FeedCoordinator.feedRequestFocus or extend FeedCoordinator with search-focus method; remove duplicate observer bridge from AppDelegate.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux User-Facing Error Privacy ❓ Inconclusive Rule file .github/review-bot-rules/user-facing-errors.md does not exist; cannot validate check against non-existent rule. Confirm rule file path or create if intended; code found no privacy violations in user-facing content.
Cmux Source Artifacts ❓ Inconclusive The rule file .github/review-bot-rules/source-control-artifacts.md referenced in the custom check does not exist in the repository, making the check unverifiable against authoritative criteria. The rule file must be created or the check must reference an existing rule. All PR files appear to be legitimate product code and documentation with no obvious artifacts.
✅ Passed checks (9 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed All new types use proper actor isolation: SearchIndex is an actor, UI singletons are @MainActor classes, TitlebarSearchField is @MainActor View, async boundaries properly use await.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift and Markdown files; the rule applies exclusively to TypeScript/JavaScript/shell/runtime scripts, which are explicitly excluded from the check scope.
Cmux Swift Logging ✅ Passed All 7 new Swift files (852 lines total) and modified code sections contain zero logging violations: no print, debugPrint, dump, NSLog, or ad hoc file logging; no secrets exposed.
Cmux Swiftui State Layout ✅ Passed PR uses @State (not @Published), passes immutable Hit snapshots to List rows, avoids GeometryReader, and schedules updates from lifecycle callbacks via MainActor.run, not body.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Introduces NSPopover (explicitly allowed) and SwiftUI Views. No new NSWindow, NSPanel, NSWindowController, Window, or WindowGroup types requiring cmux.* identifiers.
Title check ✅ Passed The title accurately describes the main feature: a global search palette with Cmd+F-style access across all tabs.
Description check ✅ Passed The description comprehensively covers what was changed, why, shortcut contracts, review fixes, and verification steps taken. However, no demo video or testing evidence is provided.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extracts the global search palette from MenubarSearchPopover into standalone files, adds scope-toggle buttons (all / macwindow / globe / doc.richtext), wires ⌥⌘F as the global search shortcut, and adds keyboard navigation (↑↓, ⌘G, ⌘1–9). The debounce and task-cancellation patterns in GlobalSearchPaletteView are well-structured using a DispatchSourceTimer + generation counter.

  • The four new Localizable.xcstrings keys only include en and ja translations while the catalog already ships 16+ other locales; every other locale will fall back to English text in production.
  • GlobalSearchCoordinator.preview calls AppDelegate.shared?.openGlobalSearchHit with the same arguments as activate, so arrowing through results triggers the same full window/panel navigation as pressing Enter — the "soft-focus preview" described in the PR is not implemented.
  • The PR title and description advertise ⇧⌘F as the primary chord but the test and implementation register ⌥⌘F; ⇧⌘F is already occupied by "Find in directory."

Confidence Score: 3/5

Not safe to merge without addressing the missing translations and the preview/activate conflation.

Three concrete defects need resolution: (1) four new string-catalog keys ship without translations for 16+ already-supported locales, so every non-English/non-Japanese user sees raw English text in the new scope toggles and navigation buttons; (2) GlobalSearchCoordinator.preview is body-identical to activate, meaning every arrow keypress performs a full window/panel switch rather than the soft-focus the design doc describes; (3) the advertised ⇧⌘F chord is not implemented — the shortcut registered is ⌥⌘F, which conflicts with the PR title and leaves the documented primary binding absent.

Resources/Localizable.xcstrings needs translations for all existing locales on the four new keys. Sources/Search/GlobalSearchCoordinator.swift needs a distinct preview API. web/data/cmux-shortcuts.ts and README.md need to reconcile the advertised ⇧⌘F chord with the actual ⌥⌘F registration.

Important Files Changed

Filename Overview
Sources/Search/GlobalSearchPaletteView.swift Extracted from MenubarSearchPopover; adds scope toggles, ⌘G navigation, DispatchSourceTimer debounce with generation guards. Generation+task cancellation pattern is sound. preview delegates to the same function as activate, causing hard navigation on every arrow key.
Sources/Search/GlobalSearchCoordinator.swift Adds a configurable limit parameter and a new preview function that is body-identical to activate, making soft-focus semantics impossible without AppDelegate changes.
Resources/Localizable.xcstrings Adds 4 new string keys with only en+ja translations while the catalog already covers 16+ locales; users in all other supported locales see English fallback text.
Sources/Search/GlobalSearchKeyEvent.swift Thin Sendable wrapper extracted from MenubarSearchPopover; adds hasExactModifiers helper that properly strips capsLock. No issues.
Sources/Search/GlobalSearchResultRow.swift Extracted model; .title icon changed from rectangle.stack to macwindow to match new scope toggle. No issues.
Sources/Search/GlobalSearchResultRowView.swift Pure presentation view extracted from MenubarSearchPopover with no substantive changes. No issues.
Sources/Search/GlobalSearchEmptyStateView.swift Trivial 11-line view extracted from MenubarSearchPopover. No issues.
Sources/Search/MenubarSearchPopover.swift All private view types removed; only the MenubarSearchPopover NSObject class remains. Straightforward extraction, no issues.
cmuxTests/GlobalSearchShortcutSettingsTests.swift Adds regression tests confirming globalSearch=⌥⌘F, find=⌘F, findInDirectory=⌘⇧F. Confirms ⇧⌘F for global search is not implemented despite the PR title.
web/data/cmux-shortcuts.ts Updates the globalSearch description text but uses an inline { en, ja } locale map that silently omits 18 of the 20 locales in web/i18n/routing.ts.
cmux.xcodeproj/project.pbxproj Adds 5 new source file references for the extracted Search views. Straightforward project file update.
README.md Adds ⌥⌘F to the shortcuts table but not ⇧⌘F, which the PR title advertises as the primary chord.

Sequence Diagram

sequenceDiagram
    participant User
    participant GlobalSearchPaletteView
    participant GlobalSearchCoordinator
    participant AppDelegate

    User->>GlobalSearchPaletteView: Open (⌥⌘F / menubar / titlebar)
    GlobalSearchPaletteView->>GlobalSearchCoordinator: refreshLiveIndex()
    GlobalSearchCoordinator-->>GlobalSearchPaletteView: done

    User->>GlobalSearchPaletteView: Type query
    GlobalSearchPaletteView->>GlobalSearchPaletteView: scheduleSearch (DispatchSourceTimer 80ms)
    GlobalSearchPaletteView->>GlobalSearchCoordinator: search(query, limit)
    GlobalSearchCoordinator-->>GlobalSearchPaletteView: [SearchIndexHit]
    GlobalSearchPaletteView->>GlobalSearchPaletteView: applyHits (filter by activeKindIDs)

    User->>GlobalSearchPaletteView: ↑/↓ arrow key
    GlobalSearchPaletteView->>GlobalSearchCoordinator: preview(hit, query)
    GlobalSearchCoordinator->>AppDelegate: openGlobalSearchHit(hit, query)
    Note over GlobalSearchCoordinator,AppDelegate: ⚠️ Same call as activate — hard navigation

    User->>GlobalSearchPaletteView: ↵ Enter
    GlobalSearchPaletteView->>GlobalSearchCoordinator: activate(hit, query)
    GlobalSearchCoordinator->>AppDelegate: openGlobalSearchHit(hit, query)
    AppDelegate-->>User: Focus window → workspace → panel
Loading

Reviews (2): Last reviewed commit: "feat(search): polish global search palet..." | Re-trigger Greptile

Comment thread Sources/Search/BrowserCaptureHook.swift Outdated
Comment on lines +76 to +82
debounce?.cancel()
debounce = Task { [weak self] in
try? await Task.sleep(nanoseconds: 600_000_000)
guard !Task.isCancelled, let self else { return }
await self.capture()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Task.sleep for debounce synchronization in production

Task.sleep is not an allowed debounce mechanism in shipped app code per cmux's blocking-runtime rule — it holds a task alive for a fixed wall-clock interval with no backpressure, is not cancellation-aware in the error path (only via Task.isCancelled), and does not compose with the scheduler. The same pattern appears in TitlebarSearchField.preview (120 ms). A Clock-based debouncer, an AsyncStream-backed pipeline, or an explicit DispatchSourceTimer with a real cancellation path should replace both instances.

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

text: $query)
.textFieldStyle(.plain)
.font(.system(size: 16))
.onChange(of: query) { _, new in Task { await refresh(new) } }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Fire-and-forget Task for refresh causes stale results

Task { await refresh(new) } is never stored or cancelled. Every keystroke while the palette is open creates an independent concurrent task; if an earlier (slower) search for a longer query completes after a shorter one, hits is overwritten with stale results. The same pattern appears in the onChange(of: scopes) handler on line 187 and in TitlebarSearchField line 49. The fix is to store the refresh task handle and cancel the previous one before scheduling a new one, matching the pattern already used for previewTask in TitlebarSearchField.

Comment thread Sources/Search/SmartRanker.swift Outdated
Comment on lines +95 to +102
private func save() {
try? FileManager.default.createDirectory(
at: storeURL.deletingLastPathComponent(),
withIntermediateDirectories: true)
if let data = try? JSONEncoder().encode(stats) {
try? data.write(to: storeURL, options: .atomic)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Synchronous file I/O on @MainActor on every impression

save() does a synchronous data.write(to:options:.atomic) directly on the main actor. It is called by recordImpressions (once per visible result set) and reward (on every accept). On a busy session with dozens of results, this writes to disk on the UI thread each time the search palette refreshes, which can cause measurable frame drops. The stats should be persisted asynchronously — either with a stored Task.detached that coalesces dirty-flushes, or by moving save to a background actor.

Comment thread Sources/Search/SearchIndex.swift Outdated
Comment on lines +54 to +73
public func upsert(
windowID: UUID, workspaceID: UUID, panelID: UUID,
kind: Kind, anchor: String, text: String
) {
let sql = """
INSERT INTO chunks(window_id, workspace_id, panel_id, kind, ts, anchor, text)
VALUES(?, ?, ?, ?, ?, ?, ?)
"""
var stmt: OpaquePointer?
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return }
defer { sqlite3_finalize(stmt) }
sqlite3_bind_text(stmt, 1, windowID.uuidString, -1, nil)
sqlite3_bind_text(stmt, 2, workspaceID.uuidString, -1, nil)
sqlite3_bind_text(stmt, 3, panelID.uuidString, -1, nil)
sqlite3_bind_text(stmt, 4, kind.rawValue, -1, nil)
sqlite3_bind_double(stmt, 5, Date().timeIntervalSince1970)
sqlite3_bind_text(stmt, 6, anchor, -1, nil)
sqlite3_bind_text(stmt, 7, text, -1, nil)
sqlite3_step(stmt)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 upsert always inserts — FTS5 index grows unboundedly

The function is named upsert but executes a bare INSERT, not DELETE … INSERT or any form of row replacement. FTS5 virtual tables do not support INSERT OR REPLACE. Every call from CmuxBrowserCaptureHook (once per finished navigation) and CmuxMarkdownCaptureHook (once per save) appends a new row for the same (panel_id, anchor) pair. Between panel closes (the only purge trigger), the same content is indexed repeatedly, inflating BM25 scores for frequently-visited panels and growing the on-disk database without bound. The correct shape is: DELETE FROM chunks WHERE panel_id = ? AND anchor = ? before the INSERT, or a MERGE/UPDATE OR INSERT strategy keyed on (panel_id, anchor).

Comment thread Sources/Search/SearchIndex.swift Outdated
Comment on lines +34 to +35
deinit {
if db != nil { sqlite3_close(db) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Actor deinit accesses isolated state without isolation

In Swift 6, deinit of an actor is implicitly nonisolated, so reading db (an actor-isolated var) from deinit is technically unsound — the compiler emits a warning under strict concurrency. The standard fix is to hoist the pointer into a local nonisolated let at the point where isolation is still valid (e.g., in a close() method called explicitly before deallocation), or to capture the raw pointer in a nonisolated stored property.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/menubar-global-search.md`:
- Around line 73-83: Add the required blank lines around fenced code blocks and
after the headings "## Storage", "## Localization", and "## Non-goals" to
satisfy MD031/MD022: ensure there's a blank line above and below each ```fenced
block``` (including the Swift snippet with TitlebarSearchField and the
AppDelegate snippet) and add a single blank line immediately after each of the
three headings so the markdown linter stops reporting spacing violations; edit
the docs/menubar-global-search.md heading and code-block regions to insert these
blank lines.
- Around line 12-16: Reword the conflicting lines to state that the menubar-only
plan was dropped but dual entry points remain: update the paragraph mentioning
`NSStatusItem`/`MenubarSearchPopover` so it reads something like “menubar-only
plan dropped; dual entry points (titlebar + menubar) retained,” and keep the
`magnifyingglass` menubar item and `MenubarSearchPopover` wiring references to
avoid confusion about the menubar still being present alongside the titlebar
entry.
- Around line 61-67: The doc still references a Synapse-backed path and
components (e.g. SynapseBridge, ~/.synapse) even though the design is
local-only; remove all references to SynapseBridge and any ~/.synapse storage
path and update the P1/P3/Storage sections to state the shipped FTS5-only, local
architecture. Specifically: delete SynapseBridge from the P1 scaffold list,
remove any mentions of Synapse, SynapseBridge, or Unix-socket clients in P3 and
Storage paragraphs, and replace storage details with the local FTS5 storage
description (FTS5 actor / SearchIndex and local file locations) so the doc
strictly reflects the FTS5-only design.

In `@Sources/AppDelegate.swift`:
- Around line 1017-1043: AppDelegate contains bootstrapping for global search
(GlobalSearchHotkey.shared.install(),
MenubarSearchPopover.shared.installStatusItem(index:), and the two
NotificationCenter observers handling .cmuxJumpToSearchHit and
.cmuxPreviewSearchHit that call FeedCoordinator.focus(workspaceId:surfaceId:)),
so extract that logic into a new integration type (e.g., GlobalSearchIntegration
or GlobalSearchBootstrap) that encapsulates installing the hotkey, installing
the menubar popover, and registering the two notification observers; expose a
simple install() or start() method on that type and replace the inline setup in
AppDelegate with a single call to the new integration to keep AppDelegate
limited to composition wiring and remove feature orchestration from the
app-target root.

In `@Sources/Search/BrowserCaptureHook.swift`:
- Around line 47-52: CmuxBrowserCaptureHook.install currently replaces
webView.navigationDelegate with the hook but only forwards two methods, breaking
other optional WKNavigationDelegate callbacks; fix this by making
CmuxBrowserCaptureHook inherit from NSObject and implement robust selector
forwarding: add a weak var forwardDelegate: WKNavigationDelegate? (already used)
and override responds(to:) to return true if super or forwardDelegate responds
to the selector, and implement forwardingTarget(for:) to return forwardDelegate
for selectors the hook doesn’t implement, so all optional WKNavigationDelegate
methods (policy, auth, navigation, etc.) are forwarded transparently; ensure
these overrides live on the CmuxBrowserCaptureHook type so setting
webView.navigationDelegate = hook and objc_setAssociatedObject(webView,
&keyHolder, hook, .OBJC_ASSOCIATION_RETAIN) preserves previously implemented
delegate behavior.

In `@Sources/Search/GlobalSearchHotkey.swift`:
- Around line 29-33: The defaultChords array in GlobalSearchHotkey.swift
hard-codes cmux-owned chords (Chord instances) instead of wiring them through
the existing KeyboardShortcutSettings/configurable path; replace the hard-coded
defaultChords usage with a retrieval from KeyboardShortcutSettings (falling back
to the proper defaults defined in settings.json), register the chords from
KeyboardShortcutSettings (or its helper API) so they become editable in Settings
and persisted to ~/.config/cmux/cmux.json, and ensure the Chord id values and
keyCode/modifier mapping match the settings schema so documentation and settings
export/import work correctly.
- Around line 50-58: Reviewer notes that the DispatchQueue.main.async inside the
InstallEventHandler legacy callback is an allowed minimal UI hop; either leave
it as-is or modernize by replacing the GCD hop with a main-actor Task to avoid
explicit GCD usage. Specifically, in the InstallEventHandler callback that
references NSApp.keyWindow, NotificationCenter.default.post(name:
.cmuxFocusTitlebarSearch, ...) and MenubarSearchPopover.shared.toggle(), you can
remove DispatchQueue.main.async and wrap the UI work in Task { `@MainActor` in ...
} so the checks and calls run on the main actor instead of using DispatchQueue.

In `@Sources/Search/MarkdownCaptureHook.swift`:
- Around line 15-31: Replace the no-case namespace enum CmuxMarkdownCaptureHook
with a value-typed struct to allow behavior composition and DI: change
CmuxMarkdownCaptureHook to a public struct and convert the static `@MainActor`
feed(text:windowID:workspaceID:panelID:anchor:index:) into an instance
`@MainActor` method (or provide an instance method and a small static convenience
initializer if needed), keep the same parameter list and internal logic (guard
let index, truncate payload, Task.detached calling index.upsert), and update
callers to obtain/inject an instance of CmuxMarkdownCaptureHook instead of
calling a static method; this preserves behavior while enabling
testing/injection.

In `@Sources/Search/MenubarSearchPopover.swift`:
- Around line 36-38: The accessibilityDescription and any user-facing literals
passed into NSImage(systemSymbolName:accessibilityDescription:) (in
MenubarSearchPopover) must be replaced with localized string lookups (e.g.
NSLocalizedString or a Localization helper) rather than hardcoded English;
update the two occurrences that set accessibilityDescription and the
tooltip/title strings referenced in this file to use localized keys and add
matching entries to the app’s Localizable.strings (and comment strings for
translators). Specifically, change the literals used in the NSImage initializers
and any tooltip/accessibility text in MenubarSearchPopover to
NSLocalizedString("MENU_SEARCH_TOOLTIP", comment: "Search cmux windows and
tabs") (and a separate key for VoiceOver if needed), and ensure corresponding
keys are added to the translation catalog for all supported locales.
- Around line 174-175: The async refresh invocations are racy: store the
in-flight Task in a property (e.g., refreshTask: Task<Void, Never>?), cancel any
existing task before starting a new one, then assign refreshTask = Task { await
refresh(newQuery) } from both onChange(of: query) and the other caller (the
block that sets hits at refresh -> hits assignment), and ensure refresh respects
cancellation (check Task.isCancelled or return early) so a slower previous query
cannot overwrite hits; update references to onChange(of: query) and the
refresh(new)/hits update flow accordingly.
- Around line 84-220: This file defines multiple major types — extension
SearchIndex.Kind, ScopeToggleBar, SearchPaletteView, and HitRow — which must be
split into one major type per file; move each top-level declaration into its own
Swift file named after the type (ScopeToggleBar.swift, SearchPaletteView.swift,
HitRow.swift, and if needed SearchIndex+Kind.swift or MenubarSearchPopover.swift
for the popover type), preserve all property signatures, bindings, and
initializer signatures (e.g. ScopeToggleBar(enabled:),
SearchPaletteView(index:onPick:), HitRow(hit:hotkey:)), keep any modifiers and
imports, update any internal references or access levels if necessary, and
ensure the original file only contains the single remaining major type (or just
a small coordinating file) so compilation and symbol references stay unchanged.

In `@Sources/Search/SearchIndex.swift`:
- Around line 54-61: The upsert function currently uses a plain INSERT that
always appends duplicates; modify the SQL in the upsert function (the sql
variable in upsert) to perform a real upsert — either use SQLite syntax "INSERT
OR REPLACE INTO chunks(...)" or use "INSERT INTO chunks(...) VALUES(...) ON
CONFLICT(<unique key columns>) DO UPDATE SET kind=excluded.kind, ts=excluded.ts,
anchor=excluded.anchor, text=excluded.text" and ensure the conflict target
matches the unique constraint you want (e.g., window_id, workspace_id, panel_id,
anchor) so panel content is replaced instead of duplicated.

In `@Sources/Search/SmartRanker.swift`:
- Around line 55-62: The code is synchronously calling save() inside
recordImpressions (and similarly at the refresh path around the stats
persistence), causing JSON encode + atomic file writes on `@MainActor`; remove the
direct save() call from recordImpressions and instead batch/debounce
persistence: keep updating the in-memory stats (stats, key(_:)), and schedule an
async non‑blocking persistence task that performs encoding and atomic write off
the main actor (e.g., a single shared Task/DispatchQueue or a coalescing timer
that calls saveAsync/persistStats), and ensure save/saveAsync runs off the main
actor and is safe for concurrent updates.

In `@Sources/Search/TitlebarSearchField.swift`:
- Around line 91-94: The selector currently accepts any modifier set that
includes .command (press.modifiers.contains(.command)), which lets chords like
⌘⌥1 or ⌘⇧1 match; change the modifier check in the .onKeyPress closure to
require exact command-only modifiers (e.g., replace contains(.command) with a
strict equality check like press.modifiers == .command or an exact OptionSet
comparison) so that only pure Cmd+digit triggers the hits selection logic; keep
the rest of the guard (Int(press.characters), hits.indices.contains(n - 1))
unchanged.
- Line 188: Replace the direct user-facing string usage in TitlebarSearchField
(Text(hit.kind.rawValue.uppercased())) with a localized catalog-backed key: add
localized keys for each Hit.Kind (e.g. "result_kind_file" / "result_kind_folder"
etc.) in Localizable.strings, add a computed property on the Hit.Kind enum (e.g.
var localizedKey: LocalizedStringKey {
LocalizedStringKey("result_kind_\(rawValue)") } or var localizedString: String {
NSLocalizedString("result_kind_\(rawValue)", comment: "") }), then update the
view to use Text(kind.localizedKey) (or
Text(kind.localizedString).textCase(.uppercase) if you need uppercase) so the
badge label is served from the localization catalog rather than rawValue.
- Line 76: Cancel the pending preview debounce task when the search is closed or
a hit is accepted: locate the debounce scheduling code that queues the preview
post (the preview debounce Task/DispatchWorkItem used around Line 170 that
ultimately posts .cmuxPreviewSearchHit) and add logic to cancel that scheduled
work from the .onKeyPress(.escape) handler (the block that sets fieldFocus =
false) and from the code path that accepts a hit / calls pick (the dismiss logic
around Lines 156–162 and 166–175). Specifically, call cancel() on the stored
preview task (e.g., previewDebounceTask.cancel() or previewTask?.cancel()) and
clear the reference (set to nil) before closing/dismissing so no late
.cmuxPreviewSearchHit is posted.
- Around line 49-50: The refresh tasks launched from the onChange and other
paths can overlap and let older tasks later call recordImpressions and set hits
for a newer query; to fix, add a stored Task? property (e.g. refreshTask) and
cancel any existing refreshTask before creating a new one when calling
refresh(query) (references: refresh, query, onChange handler), assign the newly
created Task to refreshTask, and inside the async refresh function check
Task.isCancelled and/or verify the query still matches the current query before
calling recordImpressions or assigning hits (references: recordImpressions,
hits) so stale tasks cannot mutate state or pollute ranking feedback.

In `@Sources/Update/UpdateTitlebarAccessory.swift`:
- Around line 582-584: The shortcut hint X positions are computed in
titlebarButtonRightEdge(for:) but it still assumes three adjacent fixed-size
buttons and doesn't account for the inserted TitlebarSearchField; update
titlebarButtonRightEdge(for:) (and any helper that computes the
.newTab/.newWorkspace offsets) to include the TitlebarSearchField’s width
(TitlebarSearchField / its frame width 220 or config.buttonSize where
appropriate) when calculating the right-edge X offsets so the shortcut pill
offsets (e.g., for .newTab/.newWorkspace) shift right by the search field width;
ensure the same correction is applied to the other affected call site(s) noted
around the later block (also around the 681-684 region).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dfbcb9c4-d362-44f3-a9c1-6e5e5ad6eeab

📥 Commits

Reviewing files that changed from the base of the PR and between 7135f32 and ef996db.

📒 Files selected for processing (11)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift
  • Sources/Search/BrowserCaptureHook.swift
  • Sources/Search/GlobalSearchHotkey.swift
  • Sources/Search/MarkdownCaptureHook.swift
  • Sources/Search/MenubarSearchPopover.swift
  • Sources/Search/SearchIndex.swift
  • Sources/Search/SmartRanker.swift
  • Sources/Search/TitlebarSearchField.swift
  • Sources/Update/UpdateTitlebarAccessory.swift
  • docs/menubar-global-search.md

Comment thread docs/menubar-global-search.md Outdated
Comment on lines +12 to +16
(`NSStatusItem`) plan dropped — inline is one click closer, per-window,
and shares focus with the rest of the titlebar controls.

**Menubar status item is now wired** (`MenubarSearchPopover`): a
`magnifyingglass` item in the system menubar opens the same palette,

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 | ⚡ Quick win

Clarify the menubar statement to avoid a self-contradiction.

Line 12 says the menubar plan was dropped, while Line 15 says the menubar item is now wired. Reword this as “menubar-only plan dropped; dual entry points (titlebar + menubar) retained.”

🧰 Tools
🪛 LanguageTool

[grammar] ~13-~13: Ensure spelling is correct
Context: ..., and shares focus with the rest of the titlebar controls. **Menubar status item is now...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/menubar-global-search.md` around lines 12 - 16, Reword the conflicting
lines to state that the menubar-only plan was dropped but dual entry points
remain: update the paragraph mentioning `NSStatusItem`/`MenubarSearchPopover` so
it reads something like “menubar-only plan dropped; dual entry points (titlebar
+ menubar) retained,” and keep the `magnifyingglass` menubar item and
`MenubarSearchPopover` wiring references to avoid confusion about the menubar
still being present alongside the titlebar entry.

Comment thread docs/menubar-global-search.md Outdated
Comment on lines +61 to +67
- **P1 (this PR)** — scaffold:
- `SearchIndex` (FTS5 actor)
- `SynapseBridge` (Unix-socket client, graceful no-op on miss)
- `SmartRanker` (BM25 + recency + Thompson click-history)
- `TitlebarSearchField` (inline SwiftUI field, popover results)
- `MenubarSearchPopover` (opt-in fallback)
- `GlobalSearchHotkey` (default `⌥⌘F` → focus the inline field)

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 | ⚡ Quick win

Remove stale SynapseBridge references from the phase and storage plan.

This doc still describes a Synapse-backed path (SynapseBridge, ~/.synapse/) even though this PR’s architecture is explicitly local-only and notes Synapse was dropped. Please align P1/P3/Storage text with the shipped FTS5-only design to avoid misleading follow-up work.

Also applies to: 88-90, 101-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/menubar-global-search.md` around lines 61 - 67, The doc still references
a Synapse-backed path and components (e.g. SynapseBridge, ~/.synapse) even
though the design is local-only; remove all references to SynapseBridge and any
~/.synapse storage path and update the P1/P3/Storage sections to state the
shipped FTS5-only, local architecture. Specifically: delete SynapseBridge from
the P1 scaffold list, remove any mentions of Synapse, SynapseBridge, or
Unix-socket clients in P3 and Storage paragraphs, and replace storage details
with the local FTS5 storage description (FTS5 actor / SearchIndex and local file
locations) so the doc strictly reflects the FTS5-only design.

Comment thread docs/menubar-global-search.md Outdated
Comment on lines +73 to +83
```swift
TitlebarSearchField(index: AppDelegate.shared?.searchIndex)
.frame(width: 220)
```
- In `AppDelegate`, lazily create the shared index:
```swift
let url = URL.applicationSupportDirectory
.appending(path: "cmux/search.db")
self.searchIndex = try? SearchIndex(url: url)
GlobalSearchHotkey.shared.install()
```

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 | ⚡ Quick win

Fix markdownlint spacing violations around fenced blocks and headings.

Please add required blank lines around the fenced code blocks and after ## Storage, ## Localization, and ## Non-goals to satisfy MD031/MD022.

Also applies to: 101-111

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 73-73: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 76-76: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 78-78: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 83-83: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/menubar-global-search.md` around lines 73 - 83, Add the required blank
lines around fenced code blocks and after the headings "## Storage", "##
Localization", and "## Non-goals" to satisfy MD031/MD022: ensure there's a blank
line above and below each ```fenced block``` (including the Swift snippet with
TitlebarSearchField and the AppDelegate snippet) and add a single blank line
immediately after each of the three headings so the markdown linter stops
reporting spacing violations; edit the docs/menubar-global-search.md heading and
code-block regions to insert these blank lines.

Source: Linters/SAST tools

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +1017 to +1043
// Global search: ⇧⌘F / ⌥⌘F focus the inline titlebar field (key
// window) or toggle the menubar palette. Tracking #3865.
GlobalSearchHotkey.shared.install()

// Menubar status item → cross-window/cross-tab search palette.
MenubarSearchPopover.shared.installStatusItem(index: searchIndex)

// Bridge: a picked search hit → focus its workspace+surface via
// the existing Feed focus pathway (so the user lands in the
// correct window/workspace/panel without inventing a new route).
NotificationCenter.default.addObserver(
forName: .cmuxJumpToSearchHit, object: nil, queue: .main
) { note in
guard let hit = note.object as? SearchIndex.Hit else { return }
FeedCoordinator.focus(
workspaceId: hit.workspaceID.uuidString,
surfaceId: hit.panelID.uuidString)
}
// Soft-focus while arrowing — same focus path, no reward/dismiss.
NotificationCenter.default.addObserver(
forName: .cmuxPreviewSearchHit, object: nil, queue: .main
) { note in
guard let hit = note.object as? SearchIndex.Hit else { return }
FeedCoordinator.focus(
workspaceId: hit.workspaceID.uuidString,
surfaceId: hit.panelID.uuidString)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Extract global-search bootstrap from AppDelegate into a dedicated integration type.

This adds more feature orchestration to an already oversized app-target file. Move hotkey/popover/notification bridge setup into a focused integration unit and keep AppDelegate to composition wiring.

As per coding guidelines, production Swift files under Sources/ should avoid accumulating independent feature logic in the app-target root and should enforce file-size/single-responsibility discipline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AppDelegate.swift` around lines 1017 - 1043, AppDelegate contains
bootstrapping for global search (GlobalSearchHotkey.shared.install(),
MenubarSearchPopover.shared.installStatusItem(index:), and the two
NotificationCenter observers handling .cmuxJumpToSearchHit and
.cmuxPreviewSearchHit that call FeedCoordinator.focus(workspaceId:surfaceId:)),
so extract that logic into a new integration type (e.g., GlobalSearchIntegration
or GlobalSearchBootstrap) that encapsulates installing the hotkey, installing
the menubar popover, and registering the two notification observers; expose a
simple install() or start() method on that type and replace the inline setup in
AppDelegate with a single call to the new integration to keep AppDelegate
limited to composition wiring and remove feature orchestration from the
app-target root.

Source: Coding guidelines

Comment thread Sources/Search/BrowserCaptureHook.swift Outdated
Comment on lines +47 to +52
// Retain via objc associated object; chain the previous delegate.
hook.forwardDelegate = webView.navigationDelegate
webView.navigationDelegate = hook
objc_setAssociatedObject(
webView, &keyHolder, hook, .OBJC_ASSOCIATION_RETAIN)
}

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Sources/Search/BrowserCaptureHook.swift"

# Show the relevant region around the reported lines
sed -n '1,140p' "$FILE" | cat -n

# Also capture any delegate methods further down (just in case Lines 64-73 are offset)
rg -n "WKNavigationDelegate|didFinish|didFailProvisionalNavigation|install\(" "$FILE" || true
rg -n "forwardDelegate|objc_setAssociatedObject|keyHolder" "$FILE" || true

Repository: manaflow-ai/cmux

Length of output: 5623


Critical: WKNavigationDelegate forwarding is incomplete after swapping delegates in CmuxBrowserCaptureHook.install.

Sources/Search/BrowserCaptureHook.swift replaces webView.navigationDelegate with CmuxBrowserCaptureHook (around lines 47-52) and only forwards webView(_:didFinish:) + webView(_:didFailProvisionalNavigation:withError:) (around lines 64-73). Any other optional WKNavigationDelegate methods implemented by the previous delegate will no longer be called, breaking navigation/policy/auth flows that rely on them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Search/BrowserCaptureHook.swift` around lines 47 - 52,
CmuxBrowserCaptureHook.install currently replaces webView.navigationDelegate
with the hook but only forwards two methods, breaking other optional
WKNavigationDelegate callbacks; fix this by making CmuxBrowserCaptureHook
inherit from NSObject and implement robust selector forwarding: add a weak var
forwardDelegate: WKNavigationDelegate? (already used) and override responds(to:)
to return true if super or forwardDelegate responds to the selector, and
implement forwardingTarget(for:) to return forwardDelegate for selectors the
hook doesn’t implement, so all optional WKNavigationDelegate methods (policy,
auth, navigation, etc.) are forwarded transparently; ensure these overrides live
on the CmuxBrowserCaptureHook type so setting webView.navigationDelegate = hook
and objc_setAssociatedObject(webView, &keyHolder, hook,
.OBJC_ASSOCIATION_RETAIN) preserves previously implemented delegate behavior.

Comment on lines +49 to +50
.onChange(of: query) { _, new in Task { await refresh(new) } }
.onSubmit { acceptSelected() }

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 | ⚡ Quick win

Uncancelled refresh tasks can both show stale hits and corrupt ranking feedback.

Lines 49 and 75 launch overlapping refresh tasks; an older task can later run Line 132 (recordImpressions) and Line 135 (hits = ...) after a newer query, polluting SmartRanker stats and UI state.

Also applies to: 75-75, 114-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Search/TitlebarSearchField.swift` around lines 49 - 50, The refresh
tasks launched from the onChange and other paths can overlap and let older tasks
later call recordImpressions and set hits for a newer query; to fix, add a
stored Task? property (e.g. refreshTask) and cancel any existing refreshTask
before creating a new one when calling refresh(query) (references: refresh,
query, onChange handler), assign the newly created Task to refreshTask, and
inside the async refresh function check Task.isCancelled and/or verify the query
still matches the current query before calling recordImpressions or assigning
hits (references: recordImpressions, hits) so stale tasks cannot mutate state or
pollute ranking feedback.

.frame(width: 480, height: 354)
}
.onChange(of: scopes) { _, _ in Task { await refresh(query) } }
.onKeyPress(.escape) { fieldFocus = false; return .handled }

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 | ⚡ Quick win

Cancel pending preview debounce when search closes or a hit is accepted.

A queued preview task from Line 170 can still fire after Escape (Line 76) or after pick dismisses (Lines 160–162), causing unexpected late .cmuxPreviewSearchHit posts.

Also applies to: 156-162, 166-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Search/TitlebarSearchField.swift` at line 76, Cancel the pending
preview debounce task when the search is closed or a hit is accepted: locate the
debounce scheduling code that queues the preview post (the preview debounce
Task/DispatchWorkItem used around Line 170 that ultimately posts
.cmuxPreviewSearchHit) and add logic to cancel that scheduled work from the
.onKeyPress(.escape) handler (the block that sets fieldFocus = false) and from
the code path that accepts a hit / calls pick (the dismiss logic around Lines
156–162 and 166–175). Specifically, call cancel() on the stored preview task
(e.g., previewDebounceTask.cancel() or previewTask?.cancel()) and clear the
reference (set to nil) before closing/dismissing so no late
.cmuxPreviewSearchHit is posted.

Comment on lines +91 to +94
.onKeyPress(keys: Set("123456789".map { KeyEquivalent($0) })) { press in
guard press.modifiers.contains(.command),
let n = Int(press.characters), hits.indices.contains(n - 1) else {
return .ignored

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 | ⚡ Quick win

Cmd+digit should require exact command-only modifiers.

Line 92 uses contains(.command), so chords like ⌘⌥1/⌘⇧1 can be consumed unintentionally by search selection logic.

Proposed fix
-            guard press.modifiers.contains(.command),
+            guard press.modifiers == [.command],
                   let n = Int(press.characters), hits.indices.contains(n - 1) else {
                 return .ignored
             }

Based on learnings, keyboard handlers in this repo should treat command-navigation chords as exact modifier sets (not broad contains checks) to avoid hijacking other shortcuts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Search/TitlebarSearchField.swift` around lines 91 - 94, The selector
currently accepts any modifier set that includes .command
(press.modifiers.contains(.command)), which lets chords like ⌘⌥1 or ⌘⇧1 match;
change the modifier check in the .onKeyPress closure to require exact
command-only modifiers (e.g., replace contains(.command) with a strict equality
check like press.modifiers == .command or an exact OptionSet comparison) so that
only pure Cmd+digit triggers the hits selection logic; keep the rest of the
guard (Int(press.characters), hits.indices.contains(n - 1)) unchanged.

Source: Learnings

ScrollViewReader { proxy in
List(Array(hits.enumerated()), id: \.offset, selection: $selection) { idx, hit in
HStack(spacing: 8) {
Text(hit.kind.rawValue.uppercased())

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 | ⚡ Quick win

Localize the result-kind badge label.

Line 188 renders hit.kind.rawValue.uppercased() directly, which is user-facing text and not localizable.

As per coding guidelines, production user-facing Swift text must come from localized APIs with catalog-backed entries for supported locales.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Search/TitlebarSearchField.swift` at line 188, Replace the direct
user-facing string usage in TitlebarSearchField
(Text(hit.kind.rawValue.uppercased())) with a localized catalog-backed key: add
localized keys for each Hit.Kind (e.g. "result_kind_file" / "result_kind_folder"
etc.) in Localizable.strings, add a computed property on the Hit.Kind enum (e.g.
var localizedKey: LocalizedStringKey {
LocalizedStringKey("result_kind_\(rawValue)") } or var localizedString: String {
NSLocalizedString("result_kind_\(rawValue)", comment: "") }), then update the
view to use Text(kind.localizedKey) (or
Text(kind.localizedString).textCase(.uppercase) if you need uppercase) so the
badge label is served from the localization catalog rather than rawValue.

Source: Coding guidelines

Comment on lines +582 to +584
TitlebarSearchField(index: AppDelegate.shared?.searchIndex)
.frame(width: 220, height: config.buttonSize)

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 | ⚡ Quick win

Shortcut-hint geometry is now incorrect for the New Workspace button.

After inserting TitlebarSearchField before the plus button, titlebarButtonRightEdge(for:) still assumes three adjacent fixed-size buttons. The .newTab hint offset no longer includes the search field width, so its shortcut pill is rendered in the wrong X position.

Suggested fix
+private let titlebarSearchFieldWidth: CGFloat = 220
+
 private func titlebarButtonRightEdge(for slot: HintSlot, config: TitlebarControlsStyleConfig) -> CGFloat {
-    let index = CGFloat(slot.rawValue)
-    return (index + 1) * config.buttonSize + index * config.spacing
+    switch slot {
+    case .toggleSidebar:
+        return config.buttonSize
+    case .showNotifications:
+        return (2 * config.buttonSize) + config.spacing
+    case .newTab:
+        return (3 * config.buttonSize)
+            + (3 * config.spacing)
+            + titlebarSearchFieldWidth
+    }
 }

Also applies to: 681-684

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Update/UpdateTitlebarAccessory.swift` around lines 582 - 584, The
shortcut hint X positions are computed in titlebarButtonRightEdge(for:) but it
still assumes three adjacent fixed-size buttons and doesn't account for the
inserted TitlebarSearchField; update titlebarButtonRightEdge(for:) (and any
helper that computes the .newTab/.newWorkspace offsets) to include the
TitlebarSearchField’s width (TitlebarSearchField / its frame width 220 or
config.buttonSize where appropriate) when calculating the right-edge X offsets
so the shortcut pill offsets (e.g., for .newTab/.newWorkspace) shift right by
the search field width; ensure the same correction is applied to the other
affected call site(s) noted around the later block (also around the 681-684
region).

@Supersynergy

Copy link
Copy Markdown
Author

UX audit: Cmd+F-style global findbar

This PR already covers the core user goal: local-only search across windows/workspaces/tabs with a shared index, scope icons, result focus, and Supersynergy attribution on the PR.

Audit against existing cmux find behavior:

  • Existing cmux has a compact Find bar for terminal/browser panels: search field, match count, chevron.up, chevron.down, xmark; shortcuts are documented as ⌘F Find, ⌘G / ⌥⌘G next/previous, ⌥⌘⇧F hide.
  • Existing cmux also documents ⇧⌘F as Find in directory. Using ⇧⌘F as the primary global-search hotkey in this PR conflicts with current behavior and will increase shortcut confusion.
  • The PR titlebar search handles ↑ / ↓, Enter, ⌘1..9, and soft-focus preview. The menubar palette does not yet expose the same visible next/previous controls or key handling path.
  • The existing local Find bar is the right mental model for this feature. Global search should feel like “same Cmd+F bar, but scope = all tabs”, not a separate command surface users have to remember.

Proposed UX direction from the compact Find bar mockup:

  1. Keep ⌘F as the entry point for Find. Open the familiar floating/top-right findbar for the focused panel by default.
  2. Add a visible All Tabs scope icon in that bar, e.g. square.stack.3d.up / rectangle.stack, with active accent tint and tooltip “Search all windows & tabs”. When enabled, the same query runs through SearchIndex and SmartRanker instead of only the focused panel.
  3. Add one smart scope icon beside it: macwindow for project/window-title mode. This gives the fast project-finder path: ⌘F → click/toggle project scope → type project word → arrows → Enter.
  4. Keep visible navigation controls in the bar: chevron.up, chevron.down, xmark. In global mode, ↑/↓ previews/focuses the previous/next result across windows/workspaces/tabs; Enter accepts; Esc closes; ⌘G / ⌥⌘G can map to next/previous global result while global mode is active.
  5. Keep ⌥⌘F as the direct “open global/all-tabs find” accelerator. Do not take over ⇧⌘F unless Find in Directory is deliberately moved and documented.

Target result: one recognizable Find UX with two extra icons for scope, fewer commands, less tab/project confusion, and direct keyboard traversal through every matching word across open cmux state.

@Supersynergy
Supersynergy force-pushed the feat/global-search-toggles branch from ef996db to 7ce45de Compare June 9, 2026 12:09
@Supersynergy Supersynergy changed the title feat(search): global cross-tab search — menubar item, scope toggles, ⇧⌘F feat(search): Cmd+F-style global all-tabs search palette Jun 9, 2026
@Supersynergy

Copy link
Copy Markdown
Author

Re-cut update:

  • Branch is now based on current main and GitHub reports it as mergeable.
  • The old review findings were mostly against the stale pre-Menubar global search P1 #3908 branch state; this update keeps the newer upstream Global Search architecture instead of reviving the old implementation.
  • Shortcut contract is now explicit: Command-F = local Find, Command-Shift-F = Find in Directory, Option-Command-F = Global Search.
  • Palette UX now follows the compact findbar model: scope icons, result counter, previous/next/close controls, Command-G / Option-Command-G traversal, and soft-focus preview on arrow navigation.
  • Local gates passed: Swift parse, xcstrings JSON validation, pbxproj checks, test-wiring lint, diff whitespace, and grepgod review HEAD~1..HEAD with no diff findings.

Remaining non-code blocker: Vercel deployment checks still require Manaflow team authorization.

@teamleaderleo teamleaderleo added area: command-palette Command palette, quick open, find S3: minor Wrong behavior with a workaround labels Sep 30, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

You had the palette polish here first; the current global-search implementation on main now covers this work through #14881 and #11665, so I’m closing this superseded PR. Thank you for getting the UX work started :)

#14881
#11665

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Reopening: current main has the global-search foundation, but the scope controls and Cmd-G / Option-Cmd-G traversal described here are still missing. The earlier superseded classification was premature; this remains a live product call.

@github-actions

Copy link
Copy Markdown
Contributor


Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


I have read the CLA Document v2.2 and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@teamleaderleo teamleaderleo added the needs a call Finished and held for a team design or product decision (see #13742 and the gallery in #15427) label Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: command-palette Command palette, quick open, find needs a call Finished and held for a team design or product decision (see #13742 and the gallery in #15427) S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants