From f3991ac416c2acd485e2151b68ecf9857eb0ce72 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 15 Jun 2026 23:48:37 -0700 Subject: [PATCH] Chat search: search all messages, user messages first (#6350) * Chat search: match across all messages, not just title and preview The Cmd/Ctrl+K chat search only matched the thread title and a 120 character preview of the newest message, so keywords anywhere else in a conversation were unfindable. Build a per-thread haystack from the title plus every message's text and match against that. The haystack is lowercased once at index build time, so the keystroke filter only normalizes the short query instead of re-lowercasing the whole conversation per item. * Chat search: search user messages first, expand to all only if no hit User messages are short while assistant replies can be very long, so matching the whole conversation on every keystroke scales with the assistant text. Store a separate userSearchText (title plus user messages) and match it first, expanding to the full per thread searchText only when nothing matches user text anywhere. Row filtering moves into selectVisibleChats with cmdk shouldFilter disabled so the tier choice is deterministic. * Chat search: include tool calls and sources in the full-text tier extractText now also pulls reasoning/thinking, tool call name/args/result and cited source title/url, so the expanded full-conversation tier finds keywords that only appear in tool activity. Drop the now-unused preview field since search no longer matches on it. * Chat search: drop base64 image/audio payloads from the index extractText stringified tool-call results wholesale, so an image_generation result (image_b64) or audio payload would pour megabytes of base64 into searchText and get lowercased on every rebuild. Add searchableText, which keeps readable tool args/results (tool name, prompt, text) but skips binary keys and strips data URLs, long base64 runs and the __IMAGES__ suffix. --- .../chat/components/chat-search-dialog.tsx | 53 ++++++++---- .../chat/hooks/use-chat-search-index.ts | 80 ++++++++++++++----- 2 files changed, 97 insertions(+), 36 deletions(-) diff --git a/studio/frontend/src/features/chat/components/chat-search-dialog.tsx b/studio/frontend/src/features/chat/components/chat-search-dialog.tsx index e6f51e2da8..cbd02d2bae 100644 --- a/studio/frontend/src/features/chat/components/chat-search-dialog.tsx +++ b/studio/frontend/src/features/chat/components/chat-search-dialog.tsx @@ -12,23 +12,32 @@ import { Cancel01Icon, Message01Icon, Search01Icon } from "@hugeicons/core-free- import { HugeiconsIcon } from "@hugeicons/react"; import { useNavigate } from "@tanstack/react-router"; import { Command as CommandPrimitive } from "cmdk"; -import { useEffect } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useChatSearchIndex } from "../hooks/use-chat-search-index"; import { useChatSearchStore } from "../stores/chat-search-store"; -// cmdk's default fuzzy scorer keeps non-matching rows visible (issue #5572), so -// require every whitespace token to be a substring of the item's keywords. -// `value` is the unique thread id (cmdk selection); title/preview come via keywords. -export function chatSearchFilter( - _value: string, - search: string, - keywords?: string[], -): number { - const query = search.trim().toLowerCase(); - if (query === "") return 1; - const haystack = (keywords ?? []).join(" ").toLowerCase(); - const tokens = query.split(/\s+/); - return tokens.every((token) => haystack.includes(token)) ? 1 : 0; +// Lowercased whitespace tokens of the query (haystacks are lowercased in the index). +function queryTokens(search: string): string[] { + return search.trim().toLowerCase().split(/\s+/).filter(Boolean); +} + +function haystackMatches(haystack: string, tokens: string[]): boolean { + return tokens.every((token) => haystack.includes(token)); +} + +// We filter rows here (cmdk runs with shouldFilter=false) so we control the +// two-tier behavior and avoid cmdk's fuzzy scorer keeping non-matches visible +// (issue #5572): every whitespace token must be a substring. User messages are +// searched first; expand to the full conversation only when user text alone +// matches nothing anywhere (user messages are short, assistant replies can be huge). +export function selectVisibleChats< + T extends { userSearchText: string; searchText: string }, +>(items: T[], search: string): T[] { + const tokens = queryTokens(search); + if (tokens.length === 0) return items; + const userHits = items.filter((it) => haystackMatches(it.userSearchText, tokens)); + if (userHits.length > 0) return userHits; + return items.filter((it) => haystackMatches(it.searchText, tokens)); } function formatRelative(createdAt: number): string { @@ -46,6 +55,16 @@ export function ChatSearchDialog() { const close = useChatSearchStore((s) => s.close); const navigate = useNavigate(); const { items, loading } = useChatSearchIndex(isOpen); + const [query, setQuery] = useState(""); + + const visibleItems = useMemo( + () => selectVisibleChats(items, query), + [items, query], + ); + + useEffect(() => { + if (!isOpen) setQuery(""); + }, [isOpen]); useEffect(() => { const handler = (e: KeyboardEvent) => { @@ -67,7 +86,7 @@ export function ChatSearchDialog() { className="chat-search-surface rounded-3xl! top-1/2 -translate-y-1/2 w-[635px] max-w-[calc(100%-2rem)] gap-0 p-0 ring-0 sm:max-w-[635px]" overlayClassName="bg-transparent" > - +