Skip to content

feat: add in-session search (Ctrl+F)#705

Open
hankkyy wants to merge 1 commit into
fathah:mainfrom
hankkyy:feat/chat-search
Open

feat: add in-session search (Ctrl+F)#705
hankkyy wants to merge 1 commit into
fathah:mainfrom
hankkyy:feat/chat-search

Conversation

@hankkyy

@hankkyy hankkyy commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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)

  • Search input with magnifying glass icon
  • Prev/Next arrow buttons for match navigation
  • Match counter (e.g. '3/15') or 'No matches'
  • Close button + Escape key to dismiss

Chat.tsx

  • Ctrl/Cmd+F keyboard handler opens search
  • Match positions computed across all message content via useMemo
  • searchQuery passed to MessageList

MessageList / MessageRow

  • searchQuery prop forwarded down
  • highlightText() helper splits text on search query and wraps matches in tags with .chat-search-highlight class

CSS

  • .chat-search-bar — thin bar with border-bottom
  • .chat-search-highlight — accent-colored mark

i18n

  • searchPlaceholder, searchPrev, searchNext, searchClose, searchNoMatch in all 10 locales

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-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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.

  • The match counter in Chat.tsx iterates over all messages (user and agent) but highlightText is only applied to user messages — agent content always routes through AgentMarkdown which doesn't receive the query — so the displayed match count is systematically higher than the number of visually highlighted occurrences.
  • searchMatches builds { msgId, index } per occurrence, clearly intended to power scroll-to-match, but searchMatches[safeMatchIndex] is never read back; Prev/Next only changes the counter number while the viewport stays still and no match is visually distinguished as "current".

Confidence Score: 3/5

The 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 searchMatches data structure built to drive scrolling is entirely unused. These are present, reproducible gaps in the feature's stated behaviour rather than hypothetical edge cases.

Chat.tsx — the searchMatches useMemo and navigation handlers need the most attention; the computed match data must be scoped to highlightable (user) messages and the active index must be wired to a scroll-into-view call.

Important Files Changed

Filename Overview
src/renderer/src/screens/Chat/Chat.tsx Core orchestrator for the search feature: adds state, the Ctrl/Cmd+F handler, and useMemo-based match counting. Has two significant logic bugs — the match counter includes unhighlighted agent messages, and the computed searchMatches[safeMatchIndex] data is never used for scrolling, making navigation a cosmetic-only counter change.
src/renderer/src/screens/Chat/ChatSearchBar.tsx New search bar component — well-structured with focus-on-mount, Escape/Enter keyboard handling, and i18n. Minor issue: keyboard Enter/Shift+Enter bypasses the totalMatches === 0 guard that the nav buttons have.
src/renderer/src/screens/Chat/MessageRow.tsx Adds highlightText helper and searchQuery prop; applies highlighting only to user messages (agent messages route through AgentMarkdown). Also removes several inline comments and has a missing trailing newline.
src/renderer/src/screens/Chat/MessageList.tsx Straightforward prop forwarding of searchQuery down to MessageRow; no logic changes.
src/renderer/src/assets/main.css Adds CSS for the search bar and highlight mark; uses existing CSS variables consistently, no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant Chat as Chat.tsx
    participant SearchBar as ChatSearchBar
    participant MessageList
    participant MessageRow

    User->>Chat: Ctrl/Cmd+F keydown
    Chat->>Chat: setSearchOpen(true)
    Chat-->>SearchBar: "render (query="", matchIndex=0, totalMatches=0)"
    SearchBar->>SearchBar: "focus & select input (useEffect)"

    User->>SearchBar: types query text
    SearchBar->>Chat: onQueryChange(query)
    Chat->>Chat: setSearchQuery(query), setSearchMatchIndex(0)
    Chat->>Chat: useMemo recomputes searchMatches (iterates ALL messages incl. agent)
    Chat-->>SearchBar: totalMatches (may overcount agent msgs)
    Chat-->>MessageList: searchQuery prop
    MessageList-->>MessageRow: searchQuery prop (each row)
    MessageRow->>MessageRow: highlightText() — user msgs only, agent msgs skip to AgentMarkdown

    User->>SearchBar: clicks Next / presses Enter
    SearchBar->>Chat: onNext()
    Chat->>Chat: setSearchMatchIndex(i+1)
    Chat-->>SearchBar: matchIndex updated (counter shows N/total)
    Note over Chat,MessageRow: searchMatches[safeMatchIndex].msgId is never read — no scroll occurs
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant Chat as Chat.tsx
    participant SearchBar as ChatSearchBar
    participant MessageList
    participant MessageRow

    User->>Chat: Ctrl/Cmd+F keydown
    Chat->>Chat: setSearchOpen(true)
    Chat-->>SearchBar: "render (query="", matchIndex=0, totalMatches=0)"
    SearchBar->>SearchBar: "focus & select input (useEffect)"

    User->>SearchBar: types query text
    SearchBar->>Chat: onQueryChange(query)
    Chat->>Chat: setSearchQuery(query), setSearchMatchIndex(0)
    Chat->>Chat: useMemo recomputes searchMatches (iterates ALL messages incl. agent)
    Chat-->>SearchBar: totalMatches (may overcount agent msgs)
    Chat-->>MessageList: searchQuery prop
    MessageList-->>MessageRow: searchQuery prop (each row)
    MessageRow->>MessageRow: highlightText() — user msgs only, agent msgs skip to AgentMarkdown

    User->>SearchBar: clicks Next / presses Enter
    SearchBar->>Chat: onNext()
    Chat->>Chat: setSearchMatchIndex(i+1)
    Chat-->>SearchBar: matchIndex updated (counter shows N/total)
    Note over Chat,MessageRow: searchMatches[safeMatchIndex].msgId is never read — no scroll occurs
Loading

Comments Outside Diff (1)

  1. src/renderer/src/screens/Chat/MessageRow.tsx, line 163-167 (link)

    P2 Missing newline at end of file — many editors and linters flag this, and it shows as a non-newline-terminated line in git diff.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "feat: add in-session search with Ctrl+F ..." | Re-trigger Greptile

Comment on lines +483 to +498
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]);

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.

P1 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.

Comment on lines +500 to +515
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]);

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.

P1 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.

Comment on lines +37 to +41
if (e.key === "Enter") {
e.preventDefault();
if (e.shiftKey) onPrev();
else onNext();
}

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.

P2 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.

Suggested change
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();
}

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