Skip to content

feat(web): add Ctrl/Cmd+F find-in-chat#3539

Open
hendrillara wants to merge 24 commits into
pingdotgg:mainfrom
hendrillara:feat/find-in-chat
Open

feat(web): add Ctrl/Cmd+F find-in-chat#3539
hendrillara wants to merge 24 commits into
pingdotgg:mainfrom
hendrillara:feat/find-in-chat

Conversation

@hendrillara

@hendrillara hendrillara commented Jun 24, 2026

Copy link
Copy Markdown

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/total counter, 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

  • Search the data model, not the DOM. chatSearch.ts projects each timeline entry to plain text (markdown → text via the same remark pipeline ChatMarkdown uses) and builds an ordered match list. The counter and the highlight offsets share this one text basis, so the count stays honest.
  • Reveal + scroll. Navigating expands a match's turn-fold / work-group / collapsed-long-user-message if hidden, then scrollToItems it into view. Streaming row updates don't re-scroll (the reveal effect keys on the active match and reads rows via refs).
  • Highlight via the CSS Custom Highlight API (CSS.highlights + Highlight ranges over a TreeWalker of content-only nodes) — no <mark> injection, no DOM mutation of message content. A rAF-coalesced MutationObserver re-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.
  • Keybinding 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 check 0 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.toggle keybinding (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 running for 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 global AgentStopNotifications observer 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

  • Adds a find bar (ChatFindBar) triggered by mod+f (gated by !terminalFocus) that searches across timeline entries using buildMatches in chatSearch.ts, with case-sensitive toggle, match count, and Enter/Shift+Enter navigation.
  • Highlights matches in rendered chat using the CSS Custom Highlight API via chatFindHighlight.ts, with a flash fallback for entries that cannot be range-targeted; a MutationObserver keeps highlights live during streaming.
  • Auto-scrolls to the active match in MessagesTimeline.tsx, expanding collapsed turn folds or work groups as needed before scrolling.
  • Adds agent-stop desktop notifications via a new AgentStopNotifications component: shows a native Electron notification and plays a tone (Web Audio) or system beep when a thread transitions from running to idle/ready/error, suppressed when focused on that thread.
  • Adds settings controls (Electron only) for toggling agent-stop popup, sound, and sound source (tone vs system), stored in ClientSettingsSchema with defaults of enabled/tone.

Macroscope summarized 7649e63.

hendrillara and others added 24 commits June 22, 2026 14:29
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>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1ea46a2b-c9f8-4225-9998-cc0a4a4dc49e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jun 24, 2026

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

Cursor Bugbot has reviewed your changes using high effort and found 9 potential issues.

Fix All in Cursor

❌ 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,
),

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.

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.

Fix in Cursor Fix in Web

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

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.

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)
Fix in Cursor Fix in Web

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

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.

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)
Fix in Cursor Fix in Web

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

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.

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 runningidle/ready/error transition may see no previous running and skip the agent-stop notification.

Fix in Cursor Fix in Web

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;

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7649e63. Configure here.

if (entry.kind === "work") return entry.entry.turnId ?? null;
return null;
}
return null;

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7649e63. Configure here.

return next;
});
}
return;

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.

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.

Fix in Cursor Fix in Web

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 }] : [];

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.

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.

Fix in Cursor Fix in Web

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

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.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7649e63. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant