feat: add in-session search (Ctrl+F)#705
Conversation
Add a search bar that appears on Ctrl/Cmd+F to search within the current conversation. Matches are highlighted inline in user messages (agent markdown is excluded) with navigation between matches. Changes: - ChatSearchBar: new component with input, prev/next arrows, match count (N/M), close button, Escape to dismiss - Chat: add search state, Ctrl+F keyboard handler, match computation via useMemo, searchQuery passed to MessageList - MessageList: forward searchQuery to MessageRow - MessageRow: highlightText helper wraps matches in <mark> tags - main.css: search bar, nav buttons, highlight styling - i18n: 5 new keys across all 10 locales
Greptile SummaryThis PR adds an in-session Ctrl+F search bar that slides into the chat header, highlights query matches inline in message text, and shows a Prev/Next navigation counter. i18n keys are added for all 10 supported locales and CSS uses existing design tokens.
Confidence Score: 3/5The search UI renders and opens correctly, but two of its three core behaviors — an accurate match count and functional navigation — are broken in the current implementation. The counter overcounts by including agent-message matches that are never highlighted, and Prev/Next navigation updates only a display number while doing nothing to the viewport or the visual state of individual highlights. The Chat.tsx — the Important Files Changed
|
| const searchMatches = useMemo(() => { | ||
| if (!searchQuery) return []; | ||
| const q = searchQuery.toLowerCase(); | ||
| const matches: { msgId: string; index: number }[] = []; | ||
| let idx = 0; | ||
| for (const m of messages) { | ||
| const content = | ||
| "content" in m ? ((m as { content?: string }).content || "") : ""; | ||
| let pos = 0; | ||
| while ((pos = content.toLowerCase().indexOf(q, pos)) !== -1) { | ||
| matches.push({ msgId: m.id, index: idx++ }); | ||
| pos += q.length; | ||
| } | ||
| } | ||
| return matches; | ||
| }, [messages, searchQuery]); |
There was a problem hiding this comment.
Match counter includes agent messages that are never highlighted
searchMatches iterates over every message (including role === "agent") and counts all query occurrences. However, in MessageRow, agent messages with content always take the segments.map(...) branch (which renders through AgentMarkdown) — highlightText is only reached for user messages. The result is a counter that claims e.g. "8 matches" while only 3 are actually visible as highlighted spans in the UI, making the counter systematically wrong in any conversation where the agent echoes or discusses the searched term.
| const totalMatches = searchMatches.length; | ||
| const safeMatchIndex = Math.min(searchMatchIndex, Math.max(0, totalMatches - 1)); | ||
|
|
||
| const handleSearchClose = useCallback(() => { | ||
| setSearchOpen(false); | ||
| setSearchQuery(""); | ||
| setSearchMatchIndex(0); | ||
| }, []); | ||
|
|
||
| const handleSearchPrev = useCallback(() => { | ||
| setSearchMatchIndex((i) => (i > 0 ? i - 1 : totalMatches - 1)); | ||
| }, [totalMatches]); | ||
|
|
||
| const handleSearchNext = useCallback(() => { | ||
| setSearchMatchIndex((i) => (i < totalMatches - 1 ? i + 1 : 0)); | ||
| }, [totalMatches]); |
There was a problem hiding this comment.
Navigation changes the counter only — no scroll-to-match behavior
safeMatchIndex is passed to ChatSearchBar purely for the N/total display string. The searchMatches array carries { msgId, index } per occurrence (clearly intended to power scrolling) but it is never read back after construction — searchMatches[safeMatchIndex] is never accessed anywhere. Clicking Prev/Next or pressing Enter/Shift+Enter therefore only increments a number; the viewport does not move and the "active" match is not visually distinguished from other matches. A user with 20 matches across a long conversation has no way to locate the currently-indicated match.
| if (e.key === "Enter") { | ||
| e.preventDefault(); | ||
| if (e.shiftKey) onPrev(); | ||
| else onNext(); | ||
| } |
There was a problem hiding this comment.
When there are no matches and the user presses Shift+Enter,
onPrev() is called (keyboard handlers don't check totalMatches, unlike the nav buttons). handleSearchPrev then sets searchMatchIndex to totalMatches - 1 = -1. If a new message arrives while the search bar is still open, totalMatches becomes non-zero but safeMatchIndex = Math.min(-1, …) = -1, causing the counter to display 0/N instead of 1/N. Guard the keyboard path the same way the buttons are guarded.
| if (e.key === "Enter") { | |
| e.preventDefault(); | |
| if (e.shiftKey) onPrev(); | |
| else onNext(); | |
| } | |
| if (e.key === "Enter" && totalMatches > 0) { | |
| e.preventDefault(); | |
| if (e.shiftKey) onPrev(); | |
| else onNext(); | |
| } |
Feature
Add in-session search triggered by Ctrl+F (Cmd+F on macOS). A search bar slides in at the top of the chat area. Matches are highlighted inline in user message text. Results can be navigated with arrow buttons or Shift+Enter/Enter.
Changes
ChatSearchBar (new component)
Chat.tsx
MessageList / MessageRow
CSS
i18n