feat(web): add Ctrl/Cmd+F find-in-chat#3539
Conversation
Also fixes DesktopClientSettings.test.ts to include the notifyOnAgentStop* fields added to ClientSettings by a prior task (typecheck was already failing before this task touched the file).
…, share schema, readonly types, logging, tests) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fallback Implements Task 8 of the find-in-chat feature: - index.css: --find-match / --find-match-current tokens (light + dark), ::highlight(t3-find-match) / ::highlight(t3-find-current) pseudo rules, .t3-find-flash + @Keyframes t3-find-flash fallback - chatFindHighlight.ts: clearFindHighlights, collectTextNodes (TreeWalker skipping [data-find-skip]), rangeFor, applyFindHighlights (DOM scan → CSS Custom Highlight API), rowIdHoldsEntry, reveal-flash fallback when active match produces no current range; runtime feature-detects CSS.highlights / Highlight before use - ChatView.tsx: timelineContainerRef on the 4733 relative wrapper, highlight effect (rAF-coalesced MutationObserver; clears on close/cleanup), thread-switch close effect - useChatFind.ts + ChatFindBar.tsx: openNonce counter so repeat Cmd+F re-focuses and re-selects the input Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…range, debounce flash
|
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)
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.
Cursor Bugbot has reviewed your changes using high effort and found 9 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| { caseSensitive: chatFind.caseSensitive }, | ||
| chatFind.matches, | ||
| chatFind.activeMatch, | ||
| ), |
There was a problem hiding this comment.
Find highlights lag match counter
Medium Severity
The chat find feature uses a deferred query for its match list and match count, but an immediate query for applying highlights to the DOM. This can cause the displayed highlights, match counts, and active match selection to become out of sync, particularly when typing quickly.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| pendingScrollEntryRef.current = null; | ||
| const item = rowsRef.current[located]; | ||
| if (item) void listRef.current?.scrollToItem?.({ item, animated: true, viewPosition: 0.3 }); | ||
| }, [activeFindMatch]); // eslint-disable-line react-hooks/exhaustive-deps -- read rows/timelineEntries via refs so streaming row updates don't re-scroll |
There was a problem hiding this comment.
Find scroll jumps while streaming
High Severity
The find-navigation effect depends on the whole activeFindMatch object, but useChatFind returns a fresh Match instance whenever buildMatches runs. While the assistant streams, timelineEntries updates rebuild matches even when the selected matchId is unchanged, so scrollToItem fires again and the list can jump away from where the user is reading.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| const rowId = row.getAttribute("data-timeline-row-id"); | ||
| let occurrence = 0; | ||
| for (let at = hay.indexOf(needle); at !== -1; at = hay.indexOf(needle, at + needle.length)) { | ||
| const range = rangeFor(nodes, lengths, at, at + query.length); |
There was a problem hiding this comment.
Case fold mismatch find highlights
Medium Severity
Case-insensitive matches are built with an uppercased shadow in scanOccurrences, but live highlighting lowercases DOM text and the query. For text where toUpperCase and toLowerCase disagree (e.g. ß / SS), the counter can list matches that the highlight pass never finds in the rendered nodes.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| for (const thread of input.threads) { | ||
| const status = thread.session?.status; | ||
| if (status === undefined) continue; // no session -> not tracked | ||
| nextStatuses.set(thread.id, status); |
There was a problem hiding this comment.
Null session drops stop tracking
Medium Severity
When thread.session is temporarily null, the loop continues before recording status in nextStatuses, erasing any prior running baseline. A later running → idle/ready/error transition may see no previous running and skip the agent-stop notification.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
|
|
||
| const prev = input.prevStatuses.get(thread.id); | ||
| const transitioned = prev === "running" && STOP_STATUSES.has(status); | ||
| if (!transitioned) continue; |
There was a problem hiding this comment.
Missed notify starting to ready
Medium Severity
Agent-stop notifications only fire on prev === "running". If snapshots skip running (e.g. starting then ready on a fast finish), the transition never qualifies and no notification is emitted though work completed.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| if (entry.kind === "work") return entry.entry.turnId ?? null; | ||
| return null; | ||
| } | ||
| return null; |
There was a problem hiding this comment.
Folded plan matches won't expand
High Severity
findTurnIdForEntry returns a turn id for messages and work entries but not for proposed-plan entries, even though match building uses proposedPlan.turnId. A find hit inside a collapsed turn fold won't expand that turn or scroll to the plan row.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| return next; | ||
| }); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Stale pending scroll target
Medium Severity
When the active find match has no rendered row and findTurnIdForEntry returns null, the reveal effect returns without clearing pendingScrollEntryRef. A later SECONDARY effect can scroll to an older pending entry after the user moved to a different match.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| switch (entry.kind) { | ||
| case "message": { | ||
| const text = projectMarkdown(entry.message.text); | ||
| return text.length > 0 ? [{ field: "text", text }] : []; |
There was a problem hiding this comment.
User message search wrong text
Medium Severity
Find indexes the full stored user message.text, but the timeline renders promptText after stripping preview annotations and element contexts. Matches can appear in the count for text that never shows in the bubble.
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
| push("label", entry.label); | ||
| push("detail", entry.detail); | ||
| push("command", entry.command); | ||
| push("toolTitle", entry.toolTitle); |
There was a problem hiding this comment.
Hidden tool body still indexed
Medium Severity
Find indexes separate work fields such as command and detail, but those values often live only in the collapsible expandedBody, which stays hidden until the user expands the row. Navigation can target matches that are not visible or highlightable.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 7649e63. Configure here.
ApprovabilityVerdict: Needs human review Diff is too large for automated approval analysis. A human reviewer should evaluate this PR. You can customize Macroscope's approvability policy. Learn more. |


What
Adds a browser-style Ctrl/Cmd+F find-in-chat to the conversation view. The shortcut opens a floating find bar that searches the current thread — user/assistant messages, tool/work entries, and proposed plans, including collapsed content — shows a live
N/totalcounter, navigates with Enter/Shift+Enter (or ↑/↓), and highlights matches.Why
The transcript is virtualized (
LegendList), so off-screen messages aren't in the DOM and native browser/Electron find misses most of them.How
chatSearch.tsprojects each timeline entry to plain text (markdown → text via the same remark pipelineChatMarkdownuses) and builds an ordered match list. The counter and the highlight offsets share this one text basis, so the count stays honest.scrollToItems it into view. Streaming row updates don't re-scroll (the reveal effect keys on the active match and reads rows via refs).CSS.highlights+Highlightranges over aTreeWalkerof content-only nodes) — no<mark>injection, no DOM mutation of message content. A rAF-coalescedMutationObserverre-applies as virtualized rows materialize and as a message streams. Where projected text and rendered DOM diverge (raw HTML, custom chips), it falls back to revealing + flashing the row.find.toggle(mod+f, gated!terminalFocus) registered in@t3tools/contracts+@t3tools/shared. New translucent tokens--find-match/--find-match-current(light + dark).Tests
Pure logic is unit-tested (
vite-plus/test): projection, match-building/ordering/case-fold (incl. length-changing folds like ß), active-match reconcile, row-location across grouped work entries, offset→range mapping, and keybinding gating. Repo-wide: typecheck clean,vp check0 errors, all suites green (web/contracts/shared/server).Not yet verified / scope
Visual behavior (highlight painting, scroll landing, fold/work-group/long-message reveal, streaming stability) has not been exercised in a running GUI — please try it manually. v1 leaves out regex/whole-word, terminal/preview search, and cross-thread search. In a multi-field tool entry where the same term appears in more than one field, the "current" highlight may land on a different occurrence within that row (the counter stays correct).
🤖 Generated with Claude Code
Note
Medium Risk
Find ties together virtualization, streaming DOM, and highlight/reveal logic; agent-stop detection has a documented false-positive on background turn interrupts. Desktop notification IPC is localized and low blast radius.
Overview
Adds Ctrl/Cmd+F find-in-chat for the virtualized conversation timeline: a floating find bar,
find.togglekeybinding (blocked while the terminal is focused), and search over projected timeline text (messages, plans, work logs) rather than whatever happens to be in the DOM.Match navigation expands collapsed turns, work groups, and long user messages, scrolls the active hit into view, and paints highlights via the CSS Custom Highlight API (with a MutationObserver during streaming and a row flash when DOM text diverges from the projection).
Also ships desktop agent-stop notifications: when a thread leaves
runningfor idle/ready/error, the app can show a native notification and play a generated tone or system beep (Electron IPC + new client settings), with click-to-navigate. A globalAgentStopNotificationsobserver drives this; notifications are suppressed when that thread is focused.Reviewed by Cursor Bugbot for commit 7649e63. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Ctrl/Cmd+F find-in-chat and agent-stop desktop notifications
ChatFindBar) triggered bymod+f(gated by!terminalFocus) that searches across timeline entries usingbuildMatchesin chatSearch.ts, with case-sensitive toggle, match count, and Enter/Shift+Enter navigation.MutationObserverkeeps highlights live during streaming.AgentStopNotificationscomponent: shows a native Electron notification and plays atone(Web Audio) or system beep when a thread transitions from running to idle/ready/error, suppressed when focused on that thread.tonevssystem), stored inClientSettingsSchemawith defaults of enabled/tone.Macroscope summarized 7649e63.