apps/web: add in-thread find/search for chat conversations#1501
apps/web: add in-thread find/search for chat conversations#1501leonardoxr wants to merge 12 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72d67ca4fb
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7828e85e7f
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fd077b092
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 340789c4b9
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 776dfcfe8d
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50affe963c
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a652fd5ee6
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9bfd72303
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd11e1f59d
ℹ️ 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".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e8e4642e0b
ℹ️ 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".
|
I haven't taken the time to review any changes yet, but I wanted to bring attention to this PR. This is a really important and useful feature that I find myself reaching for all the time. |
|
This feature would be great to have |
yeah, if my implementation is ok, i'd update it to make it up to date and ready for any requested changes |
e8e4642 to
633d906
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 633d906aab
ℹ️ 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 (!isThreadSearchInputTarget(activeElement)) { | ||
| threadSearchRestoreFocusRef.current = activeElement; |
There was a problem hiding this comment.
Preserve focus when find is opened from editable inputs
openThreadSearch only remembers the previously focused element when it is not an input/textarea/select/contentEditable node. If a user triggers Cmd/Ctrl+F while typing in the composer, closeThreadSearch has no saved focus target and focus falls back to the document after closing, so keyboard typing cannot resume immediately. This breaks the intended focus-restore behavior for a common workflow.
Useful? React with 👍 / 👎.
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. This PR adds a new in-thread find/search feature with ~2300 lines of new code including UI components, search indexing, and highlight rendering. New features of this scope require human review. Additionally, unresolved comments identify several bugs including highlight rendering issues and focus restoration problems. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: efbd61f2e0
ℹ️ 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".
| visibleMessageState?.visibleText ?? "", | ||
| visibleMessageState?.contexts ?? [], | ||
| ) | ||
| : markdownToPlainText(visibleAssistantText), |
There was a problem hiding this comment.
Index rendered file-link labels for assistant markdown search
Build the assistant search text from the same file-link labels that ChatMarkdown renders, not from raw markdown alone. collectRowSearchText currently indexes markdownToPlainText(visibleAssistantText), but assistant links that resolve to local files are rendered as MarkdownFileLink labels derived from path metadata (basename/parent/line), not the original markdown link text. In threads containing [custom label](./path/to/file.ts), searching the original label can produce an active row with no visible <mark>, while searching the visible file-link label can miss entirely.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
When a markdown link is rendered as MarkdownFileLink, the visible label text is produced from path metadata (e.g. file.ts · src/app · L12), but this label is rendered as plain text with no search highlighting. Since thread indexing now includes these rendered labels, queries that match only this visible label can make the row active without any inline <mark>, so users still cannot localize the hit. This is reproducible with links like [custom text](./src/file.ts) where the rendered label differs from the original markdown text.
ℹ️ 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".
| markdownToPlainText(visibleAssistantText), | ||
| ...collectMarkdownFileLinkLabels(visibleAssistantText, options?.markdownCwd), | ||
| ] |
There was a problem hiding this comment.
Index rendered skill-chip labels for searchable message text
Message-row indexing uses raw text (markdownToPlainText(...) for assistant content and buildRenderedUserMessageText(...) for user content) but does not add the human-readable skill chip labels rendered by SkillInlineText/renderSkillInlineMarkdownChildren. In threads containing known skill tokens (for example $web_search rendered as Web Search), searching the visible chip label text will miss the row entirely because only token-form text is indexed.
Useful? React with 👍 / 👎.
| </div> | ||
| )} | ||
| {/* Messages — LegendList handles virtualization and scrolling internally */} | ||
| <MessagesTimeline |
There was a problem hiding this comment.
🟠 High components/ChatView.tsx:3761
The renderItem callback in MessagesTimeline depends on search state (activeSearchRowId, matchedSearchRowIds, searchQuery), but LegendList only re-renders items when itemKey, data, or extraData changes. Since no extraData prop is passed, items already on screen never re-render when search state updates — visible rows don't gain/lose search highlights as the user types or navigates results.
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/components/ChatView.tsx around line 3761:
The `renderItem` callback in `MessagesTimeline` depends on search state (`activeSearchRowId`, `matchedSearchRowIds`, `searchQuery`), but `LegendList` only re-renders items when `itemKey`, `data`, or `extraData` changes. Since no `extraData` prop is passed, items already on screen never re-render when search state updates — visible rows don't gain/lose search highlights as the user types or navigates results.
Evidence trail:
- apps/web/src/components/chat/MessagesTimeline.tsx lines 270-291: `renderItem` useCallback with deps `[activeSearchRowId, matchedSearchRowIds, searchQuery]`
- apps/web/src/components/chat/MessagesTimeline.tsx lines 307-324: `<LegendList>` with no `extraData` prop
- LegendApp/legend-list src/components/Container.tsx line 165: `useMemo(() => getRenderedItem(itemKey), [itemKey, data, extraData])` — `getRenderedItem` not in deps
- LegendApp/legend-list src/utils/getRenderedItem.ts: reads `renderItem` from `state.props` and calls it, but the memo in Container only reacts to `itemKey`, `data`, `extraData`
- apps/web/package.json line 23: `@legendapp/list: 3.0.0-beta.44`
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0db8951448
ℹ️ 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 normalizedText = text.toLocaleLowerCase(); | ||
| const ranges: TextMatchRange[] = []; | ||
| let searchStart = 0; | ||
|
|
||
| while (searchStart <= normalizedText.length - normalizedQuery.length) { | ||
| const matchIndex = normalizedText.indexOf(normalizedQuery, searchStart); |
There was a problem hiding this comment.
Compute highlight ranges against original string indices
findMatchRanges lowercases the full text before searching, then reuses those indices to slice the original string during rendering. Locale-aware lowercasing can change string length (for example İ becomes i̇), so any match after such characters gets shifted and the <mark> can wrap the wrong substring (or an empty one). In those cases search navigation reports a hit but the visible inline highlight is misplaced/non-localizable in internationalized content.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0db8951. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 896b034ca9
ℹ️ 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".
| row.message.role === "assistant" | ||
| ? [ | ||
| markdownToPlainText(visibleAssistantText), | ||
| ...collectMarkdownFileLinkLabels(visibleAssistantText, options?.markdownCwd), |
There was a problem hiding this comment.
Ignore file-link labels extracted from literal markdown code
collectRowSearchText adds collectMarkdownFileLinkLabels(visibleAssistantText, ...) from the raw assistant markdown, so [](...) patterns inside fenced/inline code are indexed as file-link labels even though markdown treats those regions as literal text and ChatMarkdown never renders a MarkdownFileLink there. In that case, searching for the synthesized label (for example file.ts · L10) can mark the row as a hit with no visible highlight in the rendered message.
Useful? React with 👍 / 👎.
| ? [ | ||
| markdownToPlainText(visibleAssistantText), | ||
| ...collectMarkdownFileLinkLabels(visibleAssistantText, options?.markdownCwd), | ||
| ...collectSkillInlineTextLabels(visibleAssistantText, options?.skills ?? []), |
There was a problem hiding this comment.
Index skill labels only where chips are actually rendered
collectSkillInlineTextLabels(visibleAssistantText, ...) scans the full markdown source, but the renderer only transforms $skill tokens into chips in normal text and explicitly skips code and a nodes (renderSkillInlineMarkdownChildren). Tokens inside links or code therefore stay as raw $... on screen, yet their display names are still indexed; searching by display name can produce an active result without any visible <mark> in that row.
Useful? React with 👍 / 👎.

Summary
Adds a Codex-style in-thread search flow to the chat view in
apps/web.Cmd+F/Ctrl+FEnter,Shift+Enter, and the next/previous buttonsWhy
Closes #1486.
The issue asks for a
Cmd+F/Ctrl+Fstyle find-in-thread flow because it is hard to locate earlier content in long conversations by scrolling, especially during active generation.Scope
This PR is intentionally limited to the web thread-search experience.
It does not include unrelated server/runtime compatibility changes or local dev seeding helpers.
Before / After
Before:
After:
Validation
bun fmtbun lintbun typecheckbun run test src/components/chat/threadSearch.test.tsbun run test src/components/ChatMarkdown.test.tsx src/components/chat/MessagesTimeline.test.tsx src/components/chat/threadSearchHighlight.test.tsx src/components/chat/ProposedPlanCard.test.tsxbun run test:browser src/components/ChatView.threadSearch.browser.tsxNotes
This is still a fairly broad UI change for a single feature, but all included edits are directly tied to making thread search usable across the actual thread surfaces that can render searchable content.
Note
Medium Risk
Moderate UI/UX and rendering changes across the chat timeline, including altered markdown/code-block rendering during search and new indexing logic that could impact performance on large threads.
Overview
Adds an in-thread find UX to
ChatView(Cmd/Ctrl+F) with an overlaidThreadSearchBar, keyboard navigation (Enter/Shift+Enter/Escape), and auto-scroll to the active match in the virtualizedMessagesTimeline.Introduces a reusable thread search index (
threadSearch.ts) that normalizes visible text across assistant markdown (via newmarkdownToPlainText), user messages (including rendered terminal-chip labels), work-log rows (rendered headings/previews/visible file chips), proposed plans, skill chips, and markdown file-link labels; lookup incrementally narrows using prior results.Implements match highlighting across surfaces using
renderHighlightedTextplus a markdownrehypeplugin (threadSearchHighlight.tsx); during search, collapsed user messages/work groups/plans auto-expand and code fences with matches render plain text with<mark>highlights instead of syntax highlighting. Also refactors markdown link parsing/label building intomarkdown-links.tsand addsdecode-named-character-referencefor entity decoding, with comprehensive new unit/browser tests.Reviewed by Cursor Bugbot for commit 896b034. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add in-thread find/search for chat conversations
ThreadSearchBarUI triggered by Cmd/Ctrl+F inChatView, with prev/next navigation, match counts, and keyboard shortcuts (Enter/Shift+Enter/Escape).buildThreadSearchIndex) over timeline rows covering assistant markdown, user messages with terminal context chips and skill chips, work log entries, and proposed plans.findThreadSearchLookupState) and case-insensitive, locale-aware highlight rendering via a rehype plugin andrenderHighlightedText.data-search-match-stateattribute; the active row is scrolled into view; collapsed sections (work groups, proposed plans, long user messages) auto-expand during search and hide their expand controls.markdownToPlainTextfor plain-text extraction from markdown andcollectMarkdownFileLinkLabelsfor indexing file link labels.SuspenseShikiCodeBlockto a plain<pre><code>with<mark>nodes.Macroscope summarized 896b034.