Add conversation search with modal and snippet highlighting#2276
Draft
gary149 wants to merge 10 commits into
Draft
Add conversation search with modal and snippet highlighting#2276gary149 wants to merge 10 commits into
gary149 wants to merge 10 commits into
Conversation
Re-adds the conversation search command bar from #1823/#1841/#1846 ported to the current /api/v2 + APIClient + Modal patterns. - New GET /api/v2/conversations/search backed by a Mongo $text index on title + messages.content, returning a 160-char snippet around the first match alongside the matched substring. - New SearchConversationsModal reusing Modal.svelte (ESC/backdrop/inert for free), debounced search, date-grouped results, infinite scroll. - NavConversationItem grows showDescription/description/searchInput props that render the snippet with the matched substring bolded (HTML-escaped to avoid XSS). - NavMenu gets a "Search chats" entrypoint with a Ctrl/Cmd+K hint hidden on touch devices. - +layout.svelte keyboard handler also toggles the search modal on Ctrl/Cmd+K (runs before the inert gate so the shortcut closes it too); existing Ctrl/Cmd+Shift+O New Chat shortcut keeps its inert guard. Snippet matching uses an in-tree case + diacritic-insensitive helper (src/lib/utils/snippet.ts) instead of pulling the `natural` dependency the original PR used; Mongo $text already does English stemming.
…llision
`messages: { $slice }` plus `"messages.content": 1` triggers a Mongo
"Path collision at messages.content remaining portion content" error,
which produced a 500 on every search. Switch to an aggregation pipeline
that slices the messages array and projects only `content` inside a
$map — same semantics, no path collision.
`max-w-xl` lets the modal shrink to its content; switch to a fixed width so the command bar stays at a consistent size. Modal.svelte's existing `max-w-[90dvw]` cap keeps it responsive on narrow viewports.
The "No matches" state is gated by `!loading && results.length === 0`, but `loading` only flipped to true inside the debounced runSearch, so for the 250ms between a keystroke and the fetch starting it would briefly render. Set `loading = true` synchronously in the query effect once the query reaches the minimum length, so the empty-state copy can't appear until a real fetch has returned. Also added a requestToken to discard out-of-order fetch results if the user keeps typing.
When the user types a fresh query with no prior results, the modal body was empty for the debounce + fetch window. Render "Searching…" with the same paddings/typography as the empty-hint copy so the height stays identical and there's no perceptible jump.
Replaces the bespoke input + result list with bits-ui Command
(Root / Input / List / Group / GroupHeading / GroupItems / LinkItem /
Loading) so the popup now ships keyboard navigation for free:
- Arrow keys move the highlighted row (with loop wrap-around)
- Enter activates the highlighted row (navigates + closes)
- Pointer hover updates the highlight, matching the keyboard state
- Proper combobox/listbox ARIA roles + aria-activedescendant
- shouldFilter={false} keeps the server as the source of truth; we
still drive debounce / token dedupe / infinite scroll ourselves
Modal.svelte stays as the outer shell (ESC, backdrop, inert, Portal,
focus). The first result is auto-selected on each fetch so Enter
works immediately. Highlight helper extracted to highlightSearch.ts;
NavConversationItem reverts to its pre-search shape now that results
no longer render through it.
… missing index) - Hoist <SearchConversationsModal /> from NavMenu (rendered twice — mobile drawer + desktop nav) up to +layout.svelte so it mounts exactly once. - Cmd/Ctrl+K now respects app#inert: if another modal owns inert, the shortcut is ignored unless the search modal itself is the one open (preventing the search modal from stranding inert when stacked). - loadMore() captures the query + requestToken before fetching and drops the page if either changed during the await, matching the initial search's stale-fetch dedupe. - /api/v2/conversations/search now catches Mongo IndexNotFound / "text index required" and returns an empty result instead of 500, so searches during the first prod deploy (while conv_text_idx is still building) degrade gracefully.
… p clamp, snippet window) - highlightMatch now matches on the original string and escapes match/non-match segments independently. The old code matched on the already-escaped haystack, so a needle like "amp" was shattering &/</>/"/' entities (cosmetic but visible mojibake). - fetchPage returns { items, hasMore } and propagates the server's hasMore flag to runSearch and loadMore. Eliminates a wasted "p=1" round-trip when the initial page already contained all results. - Search endpoint clamps p to >= 0 so a hand-crafted ?p=-1 can't trigger a Mongo $skip:negative 500. - buildSnippet clamps `before` to >= 0 so a query longer than maxLen can't shift the snippet window forward into the match and leave the client with nothing to highlight.
Cmd/Ctrl+K is still the entry point — the popup is hoisted at the layout level and reachable globally.
Collaborator
|
Hey @gary149 |
Collaborator
|
Hey @gary149 I see Percona already have Vector Search PR (Draft atm) open. |
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.
only UI (we'll update when semantic search is available @arekborucki - no urgency at all.
Summary
Adds a full-text search feature for conversations with a modal interface, snippet extraction, and search result highlighting. Users can now search their chat history via a new "Search chats" button or keyboard shortcut (Ctrl/Cmd+K).
Key Changes
Search Modal Component (
SearchConversationsModal.svelte): New modal interface with debounced search input, infinite scroll pagination, and results grouped by date (Today, This week, This month, Older)Search API Endpoint (
/api/v2/conversations/search): Backend endpoint that performs full-text search on conversation titles and message content, returns paginated results with snippet extraction and matched text highlightingSnippet Extraction (
snippet.ts+ tests): Utility to extract relevant context around search matches with configurable length, diacritic normalization, and ellipsis handling for truncated textEnhanced NavConversationItem: Extended component to support optional description display with search term highlighting via HTML escaping and regex-based markup
Search Modal Store (
searchModal.ts): Simple writable store for managing modal open/close state with toggle functionalityKeyboard Shortcut Integration (
+layout.svelte): Added global Ctrl/Cmd+K shortcut to toggle search modal, with platform-aware display (⌘ on Mac, Ctrl on others)Search Button (
NavMenu.svelte): New "Search chats" button in sidebar with keyboard shortcut hint, hidden on virtual keyboardsDatabase Index: Added MongoDB text index on
conversations.titleandconversations.messages.contentfor efficient full-text searchImplementation Details
https://claude.ai/code/session_01MxvNXQadF5KqpuCDK7jHoC