Chat search: search all messages, user messages first#6350
Conversation
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.
There was a problem hiding this comment.
Code Review
This pull request optimizes chat search performance by pre-building a lowercased searchText field (containing the thread title and all message texts) during index creation, avoiding repetitive lowercasing on every keystroke. The review feedback suggests capping the maximum length of the accumulated search text per thread to prevent memory bloat and UI thread lag on extremely long chat threads.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let preview = ""; | ||
| // Title + every message so search spans the whole conversation. | ||
| const haystackParts: string[] = [item.title]; | ||
| for (const m of merged) { | ||
| const text = extractText(m); | ||
| if (text) { | ||
| preview = truncate(text, PREVIEW_MAX); | ||
| break; | ||
| } | ||
| if (!text) continue; | ||
| if (!preview) preview = truncate(text, PREVIEW_MAX); // newest msg (sorted desc) | ||
| haystackParts.push(text); | ||
| } | ||
| results.push({ ...item, preview }); | ||
| const searchText = haystackParts.join(" ").toLowerCase(); | ||
| results.push({ ...item, preview, searchText }); |
There was a problem hiding this comment.
For very long chat threads (e.g., containing large code blocks, logs, or extensive conversations), concatenating the text of every single message without any limit can lead to a massive searchText string. Since the search filter runs on every keystroke across all indexed threads, performing substring checks on very large strings can block the main UI thread and cause noticeable typing lag.
To prevent memory bloat and search performance degradation, we should defensively cap the maximum accumulated length of the search text per thread (e.g., to 50,000 characters). This is more than enough for search indexing while keeping the search extremely fast and lightweight.
let preview = "";
// Title + every message so search spans the whole conversation.
const haystackParts: string[] = [item.title];
let totalLength = item.title.length;
const MAX_SEARCH_TEXT_LEN = 50000; // Limit to prevent memory bloat and search lag
for (const m of merged) {
const text = extractText(m);
if (!text) continue;
if (!preview) preview = truncate(text, PREVIEW_MAX); // newest msg (sorted desc)
if (totalLength < MAX_SEARCH_TEXT_LEN) {
haystackParts.push(text);
totalLength += text.length + 1;
}
}
const searchText = haystackParts.join(" ").toLowerCase();
results.push({ ...item, preview, searchText });There was a problem hiding this comment.
Thanks for the flag. I looked at this and decided to keep the full text rather than add a hard cap, for a few reasons.
The index already loads every message for each thread (previously only to build the 120 char preview), so this PR does not change how much chat data is fetched or kept in memory. It only adds one lowercased copy used for matching, and the rebuild is debounced and runs off the keystroke path.
I benchmarked the filter on oversized histories. The measured case is the worst one: 200 threads and an absent-token query, so every haystack is scanned in full on a single keystroke:
- 200 x 2k chars (~0.4MB): build 1ms, keystroke 0.4ms
- 200 x 20k chars (~4MB): build 6ms, keystroke 1.4ms
- 200 x 200k chars (~40MB): build 49ms, keystroke 13ms
- 200 x 2M chars (~400MB): build 388ms, keystroke 127ms
Realistic and even heavy histories stay well under a frame. Only a multi hundred MB history gets slow, which is far past normal usage.
A fixed character cap would silently drop later messages from the haystack, so search would return no match for text that is actually in the thread. For a change whose whole point is to search across all messages, that is a worse failure than a few ms of filter cost. If we ever hit real histories large enough to matter, I would rather move to a proper incremental or tokenized index than truncate, so I am leaving this uncapped for now.
There was a problem hiding this comment.
Follow up: I reworked this to search user messages first and expand to the full conversation only when nothing matches on user text alone. User messages are short, so the common keystroke now scans only that small text and never touches the large assistant replies, while the full text stays searchable through the expansion pass so nothing is silently dropped. On 200 threads with roughly 400 char user text and 100k char assistant replies (about 20MB total) a user-tier keystroke is about 0.02ms. This addresses the size concern without a hard cap.
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.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69f905a2cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 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; |
There was a problem hiding this comment.
Include assistant-only matches alongside user hits
When a query matches the title or user text of any thread, this early return stops searching searchText for every other thread. For example, if one chat's user prompt contains cuda and another chat only has cuda in an assistant response, the second chat is hidden even though the new index includes full-message text; common terms will therefore still miss valid conversation matches whenever any user/title hit exists.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Intentional. The dialog searches user messages first and expands to assistant and full text only when no thread matches the query on user text at all (reasoning and benchmarks are in the thread above). In the cuda example the chat whose user prompt contains cuda is treated as the stronger match and shown, while we avoid scanning every assistant transcript on that keystroke, which is the point of the two tier design. So yes, an assistant-only cuda chat stays hidden while a user-side cuda hit exists. I am keeping gating rather than always-on ranking for now because it bounds the common-case cost; if we later want assistant-only matches shown but ranked below user hits, that is a small follow up.
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.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 44b5b880f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (typeof p.toolName === "string") parts.push(p.toolName); | ||
| const args = typeof p.argsText === "string" ? p.argsText : safeStringify(p.args); | ||
| if (args) parts.push(args); | ||
| if (p.result != null) parts.push(safeStringify(p.result)); |
There was a problem hiding this comment.
Skip binary tool payloads from search index
When a conversation contains a completed image-generation tool call, the stored tool result includes image_b64; stringifying p.result here appends the entire base64 image into searchText and then lowercases it for every index rebuild. Opening Cmd/Ctrl+K on histories with generated images can therefore allocate and scan many megabytes of meaningless binary text per image, making search slow or memory-heavy; extract only textual result fields such as prompts/output and omit image payload fields.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch, fixed in 10dab89. extractText no longer stringifies tool results wholesale. A new searchableText keeps the readable fields (tool name, prompt, text, query) but skips binary keys (image_b64, images, audio, *_base64) and strips data URLs, long base64 runs and the IMAGES image suffix, so generated images never enter the haystack. Verified end to end that an image_generation result keeps its prompt searchable while the base64 payload is excluded.
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.
|
@codex review |
|
Codex Review: Didn't find any major issues. 🚀 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
The chat search dialog (Cmd/Ctrl+K) only matched query tokens against each thread's title and a 120 character preview of its newest message. Anything said in the middle of a conversation was unsearchable.
This makes search span the whole conversation, and does it in two tiers so the common case stays cheap: it matches user messages first and expands to the full conversation (including tool calls) only when nothing matches on user text alone. User messages are short while assistant replies can be very long, so a typical keystroke only scans the small user text.
Changes
use-chat-search-index.ts:buildIndexalready fetches every message per thread (it only used them to derive the preview). It now builds two lowercased fields per thread:userSearchText(title plus user messages) andsearchText(title plus every message).extractTextwas extended to also pull reasoning/thinking, tool call name/args/result, and cited source title/url, so tool activity is searchable. Base64 image/audio payloads in tool results (image_b64,images,*_base64, data URLs, the__IMAGES__suffix) are stripped so they never bloat the index. Both fields are lowercased once at build time. The now-unusedpreviewfield is removed.chat-search-dialog.tsx: row filtering moves intoselectVisibleChats, with cmdk runningshouldFilter={false}so the tier choice is deterministic. It matchesuserSearchTextfirst and expands tosearchTextonly when no thread matches on user text. Token semantics are unchanged (every whitespace token must be a substring).Behavior
Efficiency
The common case (query present in some user message) only scans the small user text, and both haystacks are lowercased once per index build (debounced, off the keystroke path), so a keystroke never re-lowercases anything. Measured on 200 threads with roughly 400 char user text and 100k char assistant replies (about 20MB total), a user-tier keystroke is about 0.02ms and the rare full expansion pass over about 20MB stays near 1ms.
Testing