Skip to content

Chat search: search all messages, user messages first#6350

Merged
danielhanchen merged 4 commits into
mainfrom
chat-search-all-messages
Jun 16, 2026
Merged

Chat search: search all messages, user messages first#6350
danielhanchen merged 4 commits into
mainfrom
chat-search-all-messages

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Jun 16, 2026

Copy link
Copy Markdown
Member

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: buildIndex already 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) and searchText (title plus every message). extractText was 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-unused preview field is removed.
  • chat-search-dialog.tsx: row filtering moves into selectVisibleChats, with cmdk running shouldFilter={false} so the tier choice is deterministic. It matches userSearchText first and expands to searchText only when no thread matches on user text. Token semantics are unchanged (every whitespace token must be a substring).

Behavior

  • Title and newest-message matches still work.
  • A keyword in any user message matches in the first tier.
  • A keyword that only appears in an assistant message, a tool call, or a cited source is still found via the expansion pass, as long as no thread matched it on user text.
  • When a query matches a user message in one thread, assistant-only matches in other threads are not shown. This is intentional: user messages are short and are usually how people recall a conversation, and it keeps the common case from scanning large assistant text.

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

  • tsc -b typecheck passes and vite build succeeds.
  • Verified end to end with Playwright against a live Studio instance with seeded chat history:
    • keyword in a user message matches the right thread in the first tier
    • keyword only in an assistant message is found via expansion
    • keyword only inside a tool-call (args and textual result fields) is found via expansion
    • a base64 image payload in a tool result is excluded from the index (no false match)
    • a user-message hit in one thread suppresses an assistant-only hit in another
    • title match, empty query, and non-match (empty state) behave as expected

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +131 to +141
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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@danielhanchen danielhanchen changed the title Chat search: match keywords across all messages Chat search: search all messages, user messages first Jun 16, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 10dab89ebc

ℹ️ 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".

@danielhanchen danielhanchen merged commit f3991ac into main Jun 16, 2026
28 of 33 checks passed
@danielhanchen danielhanchen deleted the chat-search-all-messages branch June 16, 2026 06:48
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.

1 participant