Add global search across Markdown Files (#117)#159
Merged
Conversation
|
@zcuric is attempting to deploy a commit to the bholmesdev's projects Team on Vercel. A member of the Team first needs to authorize it. |
bholmesdev
added a commit
to zcuric/hubble.md
that referenced
this pull request
Jul 11, 2026
- fix truncated flag races at the 50-file cap - strip inline markdown from excerpts, keep snake_case searchable - drop line numbers, render excerpts in sans - chin: drop hint text, left-align keyboard legend - copy: files -> notes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Records the architecture decision the issue asks for: naive on-demand search on both surfaces, no dedicated content index, and Pagefind rejected as a build-time indexer that does not fit markdown files that change on every keystroke. Desktop reads content from the paths the sidebar snapshot already lists, so search inherits ADR-0008's staleness contract instead of introducing a second view of the folder or a recursive watcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cmd+P (or File > Go to File...) opens a cmdk palette that fuzzy-matches file names and paths synchronously, and searches file content in the Electron main process behind a 150ms debounce. Results are grouped, a file matching both ways appears once, and selecting one navigates via the existing loadPath chokepoint. Content search takes its candidate paths from the sidebar snapshot the renderer already holds rather than re-walking the folder, so search sees exactly what the sidebar sees (ADR-0008). Main still asserts granted scope on every path. A superseded search is abandoned by comparing request ids between files, since an AbortSignal cannot cross IPC. Measured at ~45ms for a full scan of 2,000 notes / 16 MiB; a common needle short-circuits on the 50-file cap. Two base-ui traps, both found by driving the running app: - Dialog.Popup stays mounted after close and keeps hit-testing at opacity 0, so a click in the middle of the editor opened whichever file sat under the cursor. Fixed with data-[closed]:pointer-events-none on the popup and backdrop. - autoFocus fires only on mount, so a second Cmd+P left the caret in the editor and the palette swallowed every keystroke. Fixed with initialFocus on Dialog.Popup; a manual focus() races base-ui's own focus management and lands non-deterministically. Modal has both latent behaviors and is left for a separate change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fix truncated flag races at the 50-file cap - strip inline markdown from excerpts, keep snake_case searchable - drop line numbers, render excerpts in sans - chin: drop hint text, left-align keyboard legend - copy: files -> notes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c4cb899 to
ba8a8d3
Compare
bholmesdev
approved these changes
Jul 11, 2026
Contributor
Author
|
@bholmesdev thanks! I really appreciate it. I used Fable to shape it up. Hope I can contribute mode of these kinds of PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes #117.
Adds an MVP global search palette to Hubble Desktop: press Cmd+P (or File → Go to File…) to find a Markdown File by name, by path, or by a phrase inside it, then Enter to open it.
The issue asked for the indexing decision to be made as part of the work, so this PR contains both the spec that records it and the implementation that follows it.
The decision: naive on-demand search, no index
Recorded in
specs/gh-117/TECH.md. Three reasons, in descending order of how costly they'd be to get wrong:filestable already storescontentinline andgetFilesByWorkspacereturns full rows — which is whyloadPathinapps/wwwdoes a.find()rather than a fetch. Every note body is already in client memory. A.searchIndex()would build a server-side index to search data the client already has.What changed
packages/ui/src/lib/fuzzy.ts— path-aware fuzzy scoring (exact > prefix > substring > subsequence, matching the existing command menus) plusmatchRangesso the palette can emphasize where a query matched. A basename hit outranks a parent-folder-only hit.packages/ui/src/components/GlobalSearchPalette.tsx— sharedcmdkpalette. Name matching is synchronous; content search is injected as asearchContentsprop, debounced 150 ms and gated at 3 characters. A file matching both ways appears once, under Files.apps/desktop/src/lib/searchContent.ts— literal, case-insensitive line matcher with windowed excerpts. Deliberately not a regex: user input would need escaping anyway, and a regex dialect is a promise we don't want to make for an MVP search box.apps/desktop/electron/main.ts—desktop:search-file-contentsIPC. Concurrency-pooled reads,assertGrantedon every path, 2 MiB per-file cap, 50-file result cap surfaced in the UI rather than silently truncated. A superseded search is abandoned by comparing request ids between files, since anAbortSignalcan't cross IPC.CmdOrCtrl+Pin the existingkeymatchchain plus a File menu item, both open-only and idempotent (this is whatCmdOrCtrl+Shift+Oalready does). Selecting a result goes through the existingloadPathchokepoint, so search results participate in document history for free.Performance
specs/gh-117/TECH.mdoriginally carried an estimate; it now carries a measurement. Over 2,000 markdown files totalling 16 MiB, concurrency 8, warm cache:The worst case is a full scan at ~45 ms, comfortably inside the 150 ms debounce. A common needle is faster, not slower, because the result cap short-circuits the walk almost immediately. If a much larger corpus ever misses this budget, the escalation is a
stat-validated content cache in main — strictly cheaper than an index, and still no watcher.Two base-ui bugs found while testing
Both share one root cause —
Dialog.Popupstays mounted after close — and neither was caught by tests. Both were found by driving the running app over CDP.opacity: 0the popup still hit-tests. After opening the palette even once, a real click in the middle of the editor landed on an invisible result row and opened whichever file sat under the cursor (reproduced: a click meant for the text navigated to Project Ideas). Fixed withdata-[closed]:pointer-events-noneon the popup and the backdrop.autoFocusfires only on mount, so the second Cmd+P left the caret in the editor: the palette rendered but swallowed every keystroke and Enter selected nothing. Fixed with base-ui'sinitialFocus={inputRef}onDialog.Popup, which runs on every open. Note that a manualfocus()in an effect is not sufficient — it races base-ui's own focus management and lands non-deterministically (it passed once, then failed on the next run).packages/ui/src/primitives/modal.tsxhas both latent behaviors — I confirmed the Settings modal also stays mounted and captures clicks. It's harmless there today because it's a small centered dialog, but it's the same trap waiting for the next full-width overlay. Left for a separate change and recorded in the spec's risks.Testing
fuzzy.test.tscovers the score ladder, separator normalization, basename preference, andmatchRangesoffsets including the subsequence case.searchContent.test.tscovers line numbers, excerpt windowing at line start/end/mid-line, offset correctness after trimming indentation, CRLF, the per-file match cap, and blank queries.pnpm checkandpnpm build:desktopare green.Cmd+P;mtgntsfuzzy-matchedmeeting-notes-2026-07-02.mdwith the subsequence characters emphasized;pagefindproduced a name hit plus a content hit inindexing-strategies.mdat line 3, correctly deduped;always-on recursiveproduced a content-only hit;Enternavigated in every case. Three consecutive open → type →Entercycles verified after the focus fix. Ghost-click regression verified: with the palette closed, a real click at the editor centre hits the editor, not a result row. Checked in both light and dark themes.Not in this slice
apps/wwwis untouched. Its content is already client-side, so it's a small addition behind the same injectedsearchContentsprop.Reviewer notes
The two base-ui bugs above have no regression test — they're behavioral and only reproduce in a real Electron renderer. A future refactor of the palette can silently reintroduce them. Happy to add a harness if you'd like one, though it needs a running app rather than jsdom.
How it works in the app
Screenshare.-.2026-07-09.9_02_20.PM.mp4