Skip to content

feat(web): add Cmd+F find in thread - #4599

Open
colonelpanic8 wants to merge 6 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/find-in-thread
Open

feat(web): add Cmd+F find in thread#4599
colonelpanic8 wants to merge 6 commits into
pingdotgg:mainfrom
colonelpanic8:t3code/find-in-thread

Conversation

@colonelpanic8

@colonelpanic8 colonelpanic8 commented Jul 27, 2026

Copy link
Copy Markdown

What Changed

Adds in-thread find to the chat timeline, on the new chat.find keybinding command (mod+f, when: !terminalFocus, so it stays out of the terminal's own find). Enter / Shift+Enter step forward and back, Esc closes.

Scope is deliberately narrow: conversation content only — user messages, assistant messages and proposed plans. Work-log/tool rows are not searched.

  • apps/web/src/components/chat/threadFind.ts — pure match model over timeline entries
  • apps/web/src/components/chat/threadFindHighlights.ts — CSS Custom Highlight API painting
  • apps/web/src/components/chat/FindInThreadBar.tsx — the bar
  • apps/web/src/lib/visibleMessageText.ts — the "what does this user message display?" derivation, extracted from MessagesTimeline so the row and find share one source

Why

Requested in #1486. Three earlier attempts (#1501, #3539, #3779) stalled; #1501 was closed with "it should restart as a smaller focused change". This is that smaller change, built to avoid the specific defects reviewers found in the earlier ones:

Highlighting never touches the DOM. Earlier attempts injected <mark> tags into message text before markdown parsing, which rendered literal <mark class=...> inside fenced code blocks and corrupted copied source, and desynchronized the AST offsets that task-list checkbox toggling derives from message text. This uses the CSS Custom Highlight API instead: ranges are painted, the DOM is unmodified. Code blocks keep their syntax highlighting and their exact copy text. Browsers without the API render no highlights but keep working counting and navigation.

Matches are counted against timeline entries, not rendered nodes. LegendList virtualizes the timeline, so DOM-derived counts change as you scroll. Counting from entries keeps the total stable, and navigation uses scrollToIndex on the row model rather than document.querySelector, so a match in an unmounted row is reachable.

Folded turns unfold on navigation. A settled turn hides everything but its terminal assistant message. Those hidden messages are still counted, and stepping to one expands its turn first, then scrolls (screenshot below).

Only one occurrence is ever active. The active style applies to a single range, not to the whole row, so advancing within a long message moves the marker instead of lighting up every match in it.

The counter cannot show an impossible position. The index is clamped against the live match count on every render, so a streaming edit that removes matches cannot leave 5/2 on screen. Occurrence scanning advances by the needle length, so aa in aaa counts once — the count matches what a left-to-right highlighter can actually paint.

Find searches what the timeline shows. User prompts carry appended <terminal_context> / <element_context> / preview-annotation blocks that are stripped before rendering. Searching raw message.text counted invisible text; the display derivation is now shared between the row and the search index, so they cannot drift.

Known limitation, called out rather than hidden: an occurrence split across text nodes by inline markup (**bo**ld) is counted but not painted. The row is authoritative for navigation and the occurrence index within a row is clamped, so this degrades to "scrolls to the right message, paints fewer highlights than it counts" rather than breaking.

UI Changes

Find bar with match count, live highlighting across markdown, inline code and fenced code blocks (light):

find in thread, light

Dark:

find in thread, dark

Stepping onto a match inside a folded turn unfolds the turn and reveals it (the "Worked for 2m" fold above has expanded, and the commentary message it was hiding now shows the active match):

find revealing a folded turn

No results:

no results

Verification

Unit tests:

vp test run apps/web/src/components/chat/threadFind.test.ts apps/web/src/components/chat/threadFindHighlights.test.ts
# 17 passed

vp test run apps/web/src/keybindings.test.ts \
  apps/web/src/components/settings/KeybindingsSettings.logic.test.ts \
  apps/web/src/components/chat/MessagesTimeline.test.tsx \
  apps/web/src/components/chat/MessagesTimeline.logic.test.ts \
  packages/contracts/src/settings.test.ts
# 116 passed

Static checks:

vp check apps/web/src packages/contracts/src packages/shared/src   # 0 errors
pnpm --filter @t3tools/web run typecheck                           # clean
pnpm --filter @t3tools/contracts run typecheck                     # clean
pnpm --filter @t3tools/shared run typecheck                        # clean

Driven in a real browser against an isolated local environment seeded with a 17-turn thread (folded turns, a fenced code block, and enough filler to force virtualization). Confirmed there: Ctrl+F opens and focuses; deploy reports 1/12, matching a hand count that includes the folded commentary and the code block; Enter at 1/12 unfolds the folded turn and mounts the hidden message; wrap-around forward and back (12/121/12); the code block's textContent is unchanged with zero <mark> elements in the DOM; a no-match query reports 0/0 and paints nothing; Esc closes and unregisters both highlights.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Medium Risk
Touches large chat UI (ChatView, MessagesTimeline) with scroll/live-follow and virtualization interactions; highlights degrade gracefully where the Custom Highlight API is missing.

Overview
Adds in-thread find to the chat timeline via a new chat.find command (mod+f when terminal and preview are not focused).

ChatView owns per-thread find state (query, active match, navigation) and renders FindInThreadBar over the timeline. MessagesTimeline builds matches from timeline entries (user/assistant messages and proposed plans—not work logs), reports counts to the bar, unfolds folded turns and scrolls to the active row, and auto-expands collapsed user messages and plan cards while find is active.

Highlights use the CSS Custom Highlight API (threadFindHighlights) on data-thread-find-text regions so markdown and copy text stay untouched. Shared deriveDisplayedUserMessageContent keeps search text aligned with what the UI shows (stripped contexts). Unit tests cover matching, navigation, and highlight partitioning.

Reviewed by Cursor Bugbot for commit ba5a3b3. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Cmd+F find-in-thread search to the chat view

  • Adds a FindInThreadBar overlay triggered by Mod+F (when not in terminal or preview focus) via the new chat.find keybinding command.
  • Searches assistant messages, user messages, and proposed plan content; work-log/tool entries are excluded. Match scanning is case-insensitive with UTF-16-safe offsets.
  • Highlights are painted using the CSS Custom Highlight API with theme-aware colors; the active match is styled distinctly from inactive ones.
  • Collapsed user message bodies and proposed plan cards expand automatically during find and restore collapse controls when find closes.
  • Navigation scrolls the active match into view, unfolding turns as needed, and resets state when switching threads.
  • Risk: CSS Custom Highlight API is not universally supported; the hook falls back silently when unavailable, leaving matches unhighlighted on unsupported browsers.

Macroscope summarized ba5a3b3.

@coderabbitai

coderabbitai Bot commented Jul 27, 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 Plus

Run ID: 0eaf8d10-a018-4f99-b521-5e8e1947775d

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:XL 500-999 changed lines (additions + deletions). labels Jul 27, 2026
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/chat/threadFindHighlights.ts
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/chat/threadFindHighlights.ts
Comment thread apps/web/src/components/chat/threadFindHighlights.ts Outdated
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
Comment thread apps/web/src/components/chat/threadFind.ts
@macroscopeapp

macroscopeapp Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new user-facing feature (Cmd+F find in thread) with new UI components, matching logic, and highlight painting infrastructure. New features with this scope of new behavior and components warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
… and pingdotgg#4599

The published build already carried pingdotgg#4605 and pingdotgg#4599 -- stack-build-info.json on
t3code/stack lists both -- but the manifest edits that admitted them were never
committed here, so the recorded intent had drifted from the artifact and a
rebuild would have dropped two features. Restore them at the OIDs the published
build resolved, then append pingdotgg#4624.

pingdotgg#4624 is closed upstream without merging; carried because the replay cost is
real on this phone. Its head carries a fix past the PR: the 200ms code-highlight
debounces could never fire, because a growing code block's React key encodes its
source offsets and the block remounts on every delta.

Also refresh two pins that the artifact had moved past (the thread-picker group
and local/nix-flake), and re-audit the exception set: the group rebuild at
c5a4a73 shrank the CommandPalette losses on pingdotgg#4263/pingdotgg#4258/pingdotgg#4426 from 29/28/19
to 3/3/4. pingdotgg#4590's waiver is rewritten -- its 32 missing lines are an unbuilt
branch revision, not a resolution loss, because the group is still pinned at the
older member head.
@colonelpanic8
colonelpanic8 force-pushed the t3code/find-in-thread branch from 7d951aa to 6a69b32 Compare July 27, 2026 11:15
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/chat/threadFind.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 27, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
@colonelpanic8
colonelpanic8 force-pushed the t3code/find-in-thread branch from 6a69b32 to 5ec0c9f Compare July 28, 2026 02:08
Comment thread apps/web/src/components/chat/FindInThreadBar.tsx
Comment thread apps/web/src/lib/visibleMessageText.ts
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread packages/shared/src/keybindings.ts Outdated
colonelpanic8 and others added 4 commits July 27, 2026 19:47
Adds an in-thread find bar to the chat timeline, bound to the new
`chat.find` keybinding command (mod+f outside terminal focus).

Matches are modeled against timeline entries rather than rendered DOM,
so counts stay stable while the virtualized list mounts and unmounts
rows, and a match inside a folded turn unfolds that turn on navigation.
Highlights are painted with the CSS Custom Highlight API, which leaves
the DOM untouched — markdown output, syntax-highlighted code blocks and
copyable text are unaffected, and browsers without the API still get
working counting and navigation.

Refs pingdotgg#1486

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Length-preserving case fold: lowercasing that changes UTF-16 length
  ("İ" → "i" + combining dot) no longer drifts offsets into text nodes,
  which could throw IndexSizeError from Range.setEnd. Code points whose
  lowercase form is longer simply don't case-fold.
- Highlight painting is scoped to data-thread-find-text regions (user
  message body, assistant markdown, plan markdown) so row chrome, fold
  labels and work-log output can no longer light up for text the counter
  never counted, or shift the active-occurrence index within a row.
- Find navigation calls onManualNavigation() so live follow cannot
  scroll the timeline back to the streaming edge right after revealing
  a match.
- The navigation identity now includes the query and a step nonce:
  changing the query re-navigates even when the target row/occurrence
  is unchanged, and Enter on a lone match re-reveals it.
- The post-scroll occurrence nudge retries across frames (and survives
  streaming re-renders) instead of giving up if the row hasn't mounted
  by the first requestAnimationFrame.
- Match count is reported via useLayoutEffect so the find bar can never
  paint a total the step handlers aren't already using.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@colonelpanic8
colonelpanic8 force-pushed the t3code/find-in-thread branch from 70d719b to f625827 Compare July 28, 2026 02:51

@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 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f625827. Configure here.

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
colonelpanic8 added a commit to colonelpanic8/t3code that referenced this pull request Jul 28, 2026
# Conflicts:
#	apps/web/src/components/ChatView.tsx
#	apps/web/src/components/chat/MessagesTimeline.tsx
#	packages/contracts/src/keybindings.ts
#	packages/shared/src/keybindings.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 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