Skip to content

Add conversation search with modal and snippet highlighting#2276

Draft
gary149 wants to merge 10 commits into
mainfrom
claude/find-search-command-pr-JKbzs
Draft

Add conversation search with modal and snippet highlighting#2276
gary149 wants to merge 10 commits into
mainfrom
claude/find-search-command-pr-JKbzs

Conversation

@gary149
Copy link
Copy Markdown
Collaborator

@gary149 gary149 commented May 19, 2026

only UI (we'll update when semantic search is available @arekborucki - no urgency at all.

image

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 highlighting

  • Snippet Extraction (snippet.ts + tests): Utility to extract relevant context around search matches with configurable length, diacritic normalization, and ellipsis handling for truncated text

  • Enhanced 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 functionality

  • Keyboard 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 keyboards

  • Database Index: Added MongoDB text index on conversations.title and conversations.messages.content for efficient full-text search

Implementation Details

  • Search requires minimum 2 characters to prevent excessive queries
  • Results are debounced (250ms) to reduce API calls while typing
  • Snippet extraction normalizes diacritics for matching while preserving original text in output
  • HTML escaping prevents XSS when highlighting matched terms
  • Modal closes automatically when a result is clicked
  • Search is only available to authenticated users

https://claude.ai/code/session_01MxvNXQadF5KqpuCDK7jHoC

claude and others added 10 commits May 19, 2026 19:04
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
  &amp;/&lt;/&gt;/&quot;/&#39; 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.
@arekborucki
Copy link
Copy Markdown
Collaborator

Hey @gary149
Percona announced that vector search is expected to be available around late September / early October, but I will confirm the timeline with Percona and update you

@arekborucki
Copy link
Copy Markdown
Collaborator

Hey @gary149 I see Percona already have Vector Search PR (Draft atm) open.
percona/percona-server-mongodb-operator#2360

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.

3 participants