feat(web): add in-thread find#7434

closed19
main
GitHub fork: codex/find-in-thread
openedAug 18, 2026byBbj97301
View on GitHubTwo-way sync enabledPR last imported 3d ago

Users currently cannot find text in the open thread from the desktop app because Cmd/Ctrl+F has no in-app target.

This was sloppified by GPT-5.6 Sol Medium. I (a real human) also built and used the desktop and web apps, then tested the UI to make sure it worked well.

This adds a compact, responsive find bar directly to the existing virtualized message timeline. It stays anchored to the top right within narrow chat columns by shrinking the text field while preserving every control. It searches every user prompt and assistant response, including inline and fenced code snippets, and expands a collapsed long prompt when navigating to a match inside it. Search is case-insensitive by default, with a compact Aa toggle and a clear primary-color active state for exact-case matching. It counts each occurrence, scrolls to matching virtualized rows, highlights every rendered match, and gives the active match a stronger highlight and underline.

The bar supports Cmd/Ctrl+F, Enter, Shift+Enter, Escape, previous/next controls, and Cmd/Ctrl+G or Shift+Cmd/Ctrl+G while focus is outside the search field. Cmd/Ctrl+G reopens a closed bar with its retained query and immediately navigates to the next match; adding Shift navigates to the previous match. Escape closes the bar even after focus has moved elsewhere in the chat. Pressing Cmd/Ctrl+F again refocuses an open bar; in a regular browser, pressing it again while that input is already focused falls through to the browser's native find. Terminal focus keeps ownership of its shortcuts.

The implementation remains client-only, with no contract or server changes and no state outside the timeline.

Web dependencies

These three additions are kept because search counts need to be derived from visible Markdown text rather than raw Markdown syntax:

  • unified: creates and reuses the Markdown processing pipeline.
  • remark-parse: parses message Markdown into an mdast tree, so syntax and hidden link destinations are not counted as visible matches.
  • mdast-util-to-string: extracts the visible text from that tree, including inline/fenced code while excluding image alt text, so occurrence counts line up with rendered highlights.

remark-gfm was already a web dependency; the search parser reuses it so GitHub-flavored Markdown is interpreted consistently with ChatMarkdown.

Validation:

  • vp test run apps/web/src/components/chat/MessagesTimeline.logic.test.ts (35 tests, including exact-case matching)
  • pnpm --filter @t3tools/web typecheck
  • targeted vp lint on the changed web files
  • real browser pass: all matches remain highlighted; Enter changes 1/2 β†’ 2/2 with a stronger active result; Cmd+G and Shift+Cmd+G navigate; Cmd+F refocuses the open custom input; a second Cmd+F while it is focused opens Chrome's native find

Screenshots:

Before:

Current chat without in-thread search

After β€” the active browser result is highlighted:

Cmd+F find bar with browser highlighted

Context: #1486, #4599

Built with Codex (GPT-5).

Synced from GitHub

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: 075e3a4d-3493-4882-9551-0da408d4ecd9

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

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.

apps/web/src/components/chat/MessagesTimeline.tsxline 453Outdated Β· fa38104Open thread
……@@ -420,6 +423,65 @@ export const MessagesTimeline = memo(function MessagesTimeline({
450+ setFindMatchIndex(next);
451+ onManualNavigation();
452+ void listRef.current?.scrollToIndex({
453+ index: findMatches[next]!,
MmacroscopeappBoton line 453Aug 18, 2026

🟑 Medium chat/MessagesTimeline.tsx:453

goToFindMatch advances the counter for each occurrence but scrolls only to the matched message row, so later matches within a long message remain off-screen while the UI reports them as selected. Preserve the occurrence offset and reveal or highlight that specific occurrence instead of calling scrollToIndex for the row alone.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 453:
2
3`goToFindMatch` advances the counter for each occurrence but scrolls only to the matched message row, so later matches within a long message remain off-screen while the UI reports them as selected. Preserve the occurrence offset and reveal or highlight that specific occurrence instead of calling `scrollToIndex` for the row alone.

Addressed in 409b73bc. Navigation now tracks individual occurrences and scrolls the selected DOM range into view, including later matches inside a long message.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Added 1 commit

  • feat(web): add in-thread find4ab4c6f
apps/web/src/components/chat/MessagesTimeline.tsxline 432Outdated Β· 4ab4c6fOpen thread
……@@ -420,6 +424,103 @@ export const MessagesTimeline = memo(function MessagesTimeline({
429+ const [findMatchIndex, setFindMatchIndex] = useState(0);
430+ const findInputRef = useRef<HTMLInputElement>(null);
431+ const findTexts = useMemo(
432+ () => rows.map((row) => (row.kind === "message" ? row.message.text : null)),
MmacroscopeappBoton line 432Aug 18, 2026

🟠 High chat/MessagesTimeline.tsx:432

Cmd/Ctrl+F cannot find messages inside collapsed turn folds, so text in older turns is omitted from the search results and cannot be navigated to. findTexts is derived from rows, while deriveMessagesTimelineRows removes folded messages; build the search corpus from the underlying timeline messages and expand the containing fold before navigating to a match.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 432:
2
3Cmd/Ctrl+F cannot find messages inside collapsed turn folds, so text in older turns is omitted from the search results and cannot be navigated to. `findTexts` is derived from `rows`, while `deriveMessagesTimelineRows` removes folded messages; build the search corpus from the underlying timeline messages and expand the containing fold before navigating to a match.

Addressed in 409b73bc. The match corpus now comes from the underlying timeline messages; selecting a hidden match expands its turn fold before scrolling and highlighting.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

MmacroscopeappBotreviewed Β· 3 inline threadsAug 18, 2026
Synced from GitHub

UI consistency review found three issues in the new find-in-thread bar: a locally rebuilt glass surface, a raw input that recreates the shared Input/InputGroup primitive, and a global capture-phase Cmd/Ctrl+F handler that hijacks the keystroke outside the timeline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 462–476Outdated Β· 4ab4c6fOpen thread
……@@ -420,6 +424,103 @@ export const MessagesTimeline = memo(function MessagesTimeline({
473+ event.preventDefault();
474+ event.stopPropagation();
475+ setFindOpen(true);
476+ };
MmacroscopeappBoton lines 462–476Aug 18, 2026

The capture-phase window listener claims Cmd/Ctrl+F for the whole app and stopPropagation()s it, with a carve-out only for [data-terminal-owner]. The timeline is mounted side-by-side with the right panel (PR code tab, file preview, browser preview, and the URL input in PreviewChromeRow), and when the right panel is maximized the chat column collapses to w-0 while staying mounted β€” so the shortcut is swallowed and the find bar opens where the user cannot see or use it.

Consider gating the handler on the timeline actually being the active surface (e.g. timelineViewportElement?.contains(event.target as Node) or an equivalent visibility check) and, for discoverability/rebindability, routing it through the shared keybindings registry (packages/shared/src/keybindings.ts / resolveShortcutCommand) like the other app shortcuts rather than a hardcoded key comparison.

Posted via Macroscope β€” UI Consistency

Addressed in 409b73bc. Cmd/Ctrl+F is now captured only while the chat column is visible and the event target belongs to that chat column; terminal and right-panel controls retain their own behavior.

apps/web/src/components/chat/MessagesTimeline.tsxfileOutdated Β· 4ab4c6fOpen thread

This raw <input> reconstructs the core input primitive in ordinary product UI: it loses the shared control chrome owned by Input (focus-visible ring/border, placeholder:text-placeholder token, autofill background handling, h-8.5/sm:h-7.5 size ramp) and re-implements the leading-icon + trailing-action layout that InputGroup / InputGroupAddon / InputGroupInput already own β€” see PreviewChromeRow.tsx for the established composition with size="icon-xs" addon buttons.

Consider composing the bar as InputGroup (variant ghost or default) + InputGroupAddon align="inline-start" for SearchIcon + InputGroupInput size="sm" + InputGroupAddon align="inline-end" for the counter and prev/next/close buttons, keeping only positioning and width (w-48) at the call site.

Posted via Macroscope β€” UI Consistency

Addressed in 409b73bc. The find control now uses InputGroup, InputGroupAddon, and InputGroupInput.

apps/web/src/components/chat/MessagesTimeline.tsxfileOutdated Β· 4ab4c6fOpen thread

This rebuilds the floating glass surface locally (bg-background/95 + backdrop-blur) instead of using the shared surface-glass utility, so it ignores the runtime theme tokens (--glass-opacity, --glass-blur, --glass-saturation) and the @supports opaque fallback that other floating panels rely on (PullRequestCodeTab, UsageProviderChart). Consider using the shared utility with the same border/shadow treatment.

suggestion
1 <div className="surface-glass absolute right-4 top-3 z-30 flex h-9 items-center gap-1 rounded-lg border border-border/60 px-2 shadow-lg">

Posted via Macroscope β€” UI Consistency

MmacroscopeappBotreviewed Β· 2 inline threadsAug 18, 2026
Synced from GitHub

UI consistency review found two issues in the new thread find bar. Details inline.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 678–691Outdated Β· 4ab4c6fOpen thread
……@@ -573,6 +674,57 @@ export const MessagesTimeline = memo(function MessagesTimeline({
688+ aria-label="Find in thread"
689+ placeholder="Find in thread"
690+ className="w-48 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
691+ />
MmacroscopeappBoton lines 678–691Aug 18, 2026

The find bar rebuilds the shared input shell by hand: the wrapper owns the border/background/shadow and the field is a raw <input>. That skips InputGroup's focus treatment (has-focus-visible:border-ring + ring-[3px]), the text-placeholder token, and the responsive text-base sm:text-sm sizing that every other search field uses (PullRequestListFilters, FileBrowserPanel), so this control focuses and reads differently from the rest of the app. Consider composing the primitives, and using the shared surface-glass utility for the floating shell instead of the one-off bg-background/95 ... backdrop-blur:

tsx
1<InputGroup className="surface-glass absolute top-3 right-4 z-30 w-auto shadow-lg">
2 <InputGroupAddon>
3 <SearchIcon aria-hidden="true" />
4 </InputGroupAddon>
5 <InputGroupInput
6 ref={findInputRef}
7 value={findQuery}
8 onChange={...}
9 onKeyDown={...}
10 aria-label="Find in thread"
11 placeholder="Find in thread"
12 className="w-48"
13 />
14 <InputGroupAddon align="inline-end">{/* count + icon-xs buttons */}</InputGroupAddon>
15</InputGroup>

The addon also sizes the leading icon consistently with the neighbouring icon-xs buttons, replacing the one-off size-3.5.

Posted via Macroscope β€” UI Consistency

Addressed in 409b73bc. The find bar now uses the shared input-group primitives and surface-glass styling.

apps/web/src/components/chat/MessagesTimeline.tsxlines 447–460Outdated Β· 4ab4c6fOpen thread
……@@ -420,6 +424,103 @@ export const MessagesTimeline = memo(function MessagesTimeline({
457+ });
458+ },
459+ [findMatchIndex, findMatches, listRef, onManualNavigation],
460+ );
MmacroscopeappBoton lines 447–460Aug 18, 2026

findMatches repeats a row index once per occurrence in that row (findTextMatches(["One one", ...], "one") -> [0, 0, ...]), but scrolling and the highlight effect are keyed on the row only. When a message contains two hits, Next/Previous bumps the n/total counter while the scroll position and the highlighted ranges stay identical, so the button reads as dead. Consider navigating over de-duplicated row indices (keeping the occurrence total for the label), or carrying the per-occurrence offset so each step targets a distinct range.

Posted via Macroscope β€” UI Consistency

Addressed in 409b73bc. Matches carry an occurrence index, and only that exact range is highlighted, so next/previous visibly advances between repeated matches in one message.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Added 1 commit

  • fix(web): address in-thread find feedback409b73b
apps/web/src/components/chat/MessagesTimeline.tsxline 535Outdated Β· 409b73bOpen thread
……@@ -420,10 +425,148 @@ export const MessagesTimeline = memo(function MessagesTimeline({
532+ ranges.push(range);
533+ }
534+ }
535+ const activeRange = ranges[match.occurrenceIndex];
MmacroscopeappBoton line 535Aug 18, 2026

🟑 Medium chat/MessagesTimeline.tsx:535

Searching text that exists only in Markdown source, such as the URL in [label](https://example.com), counts a match and scrolls to the row but produces no highlight because findMatches uses raw message.text while ranges scans rendered DOM text. Use the same text representation, or map source offsets to rendered nodes, so match counts and match.occurrenceIndex correspond to the text being highlighted.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 535:
2
3Searching text that exists only in Markdown source, such as the URL in `[label](https://example.com)`, counts a match and scrolls to the row but produces no highlight because `findMatches` uses raw `message.text` while `ranges` scans rendered DOM text. Use the same text representation, or map source offsets to rendered nodes, so match counts and `match.occurrenceIndex` correspond to the text being highlighted.

Addressed in 378b338a. Search text is now produced by parsing Markdown and extracting its rendered text, so hidden link destinations no longer affect counts or occurrence indexes.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

apps/web/src/components/chat/MessagesTimeline.tsxline 531Outdated Β· 409b73bOpen thread
……@@ -420,10 +425,148 @@ export const MessagesTimeline = memo(function MessagesTimeline({
528+ ) {
529+ const range = new Range();
530+ range.setStart(node, offset);
531+ range.setEnd(node, offset + needle.length);
MmacroscopeappBoton line 531Aug 18, 2026

🟑 Medium chat/MessagesTimeline.tsx:531

Searching text containing Unicode characters such as İ throws an uncaught IndexSizeError, so the active match is not highlighted. The offsets come from text.toLowerCase() ("İ" becomes two UTF-16 code units, "i̇") but are applied to the original text node; map normalized offsets back to the original string or use a length-preserving comparison before calling range.setEnd.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 531:
2
3Searching text containing Unicode characters such as `İ` throws an uncaught `IndexSizeError`, so the active match is not highlighted. The offsets come from `text.toLowerCase()` (`"İ"` becomes two UTF-16 code units, `"i̇"`) but are applied to the original text node; map normalized offsets back to the original string or use a length-preserving comparison before calling `range.setEnd`.

Addressed in 378b338a. Case-insensitive matching now records original UTF-16 boundaries while normalizing each code point, so expanding folds such as Δ° cannot produce an out-of-bounds DOM range.

MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One sizing/ownership finding on the new find bar. The rest of the migration (InputGroup composition, surface-glass, size="icon-xs" addon buttons, per-occurrence match navigation) looks consistent with the shared system.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 720–720Outdated Β· 409b73bOpen thread
……@@ -573,6 +716,61 @@ export const MessagesTimeline = memo(function MessagesTimeline({
574717 <TimelineRowActivityCtx value={activityState}>
575718 <div ref={setTimelineViewportElement} className="relative h-full min-h-0">
719+ {findOpen ? (
720+ <InputGroup className="surface-glass absolute right-4 top-3 z-30 h-9 w-auto shadow-lg">
MmacroscopeappBoton lines 720–720Aug 18, 2026

The field geometry is applied to elements that can't render it. InputGroup's base sets *:[[data-slot=input-control]]:contents, and InputGroupInput's className lands on exactly that data-slot="input-control" wrapper β€” so w-48 (line 734) generates no box and the bar falls back to the input's default intrinsic width. Separately, h-9 pins the shell to 36px at every breakpoint, so this control skips the sm: compaction the shared input ramp (h-8.5 sm:h-7.5) gives every other search field, and leaves the 30px input floating inside a 36px shell on desktop.

The established pattern is to size the inner input from the group (PullRequestListFilters uses **:[input]:h-9 sm:**:[input]:h-8):

diff
1- <InputGroup className="surface-glass absolute right-4 top-3 z-30 h-9 w-auto shadow-lg">
2+ <InputGroup className="surface-glass absolute right-4 top-3 z-30 w-auto shadow-lg **:[input]:w-48">

and drop className="w-48" from the InputGroupInput so the primitive keeps ownership of the height ramp.

Posted via Macroscope β€” UI Consistency

Addressed in 378b338a. Width is now applied from InputGroup with **:[input]:w-48, and the fixed shell height was removed so the shared responsive height ramp remains in control.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Added 1 commit

  • fix(web): align find matches with rendered text378b338

Added 1 commit

  • Merge branch 'main' into codex/find-in-threadb1ffb46
apps/web/src/components/chat/MessagesTimeline.logic.tsline 262Outdated Β· b1ffb46Open thread
……@@ -242,6 +246,67 @@ export function normalizeCompactToolLabel(value: string): string {
259+const markdownSearchTextProcessor = unified().use(remarkParse).use(remarkGfm);
260+
261+export function renderMarkdownSearchText(markdown: string): string {
262+ return toString(markdownSearchTextProcessor.parse(markdown), { includeImageAlt: false });
MmacroscopeappBoton line 262Aug 18, 2026

🟑 Medium chat/MessagesTimeline.logic.ts:262

renderMarkdownSearchText creates matches across separate Markdown text nodes, so findTextMatches reports occurrences such as oob in **foo**bar that the highlighting path cannot map to any DOM Range. This produces navigation counts like 1/1 without a visible highlight; preserve node boundaries during extraction or make highlighting support ranges spanning nodes.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.logic.ts around line 262:
2
3`renderMarkdownSearchText` creates matches across separate Markdown text nodes, so `findTextMatches` reports occurrences such as `oob` in `**foo**bar` that the highlighting path cannot map to any DOM `Range`. This produces navigation counts like `1/1` without a visible highlight; preserve node boundaries during extraction or make highlighting support ranges spanning nodes.

Added 1 commit

  • fix(web): highlight matches across markdown nodes89d82cb
MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding on the find bar's surface ownership. Everything else from the earlier passes (InputGroup composition, per-occurrence navigation, shortcut gating, input sizing via **:[input]:*) now matches the shared contracts.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxfileOutdated Β· 89d82cbOpen thread

surface-glass can't own the background on this element: InputGroup's default variant already paints the control surface (bg-background plus dark:bg-input/32), and the dark rule compiles to .dark\:bg-input\/32:is(.dark, .dark *) (0-2-0), which outranks the surface-glass utility (0-1-0). So in dark themes the bar is tinted with --input at 32% instead of --background at --glass-opacity, and in light themes the winner is decided purely by utility emission order rather than by a contract. The runtime glass tokens the earlier composition was meant to pick up therefore still don't apply. Every other glass surface (PullRequestCodeTab, UsageProviderChart, Button variant="glass") puts surface-glass on an element that owns no bg-*.

Smallest fix: let the input primitive own its surface and drop the utility here β€” the floating elevation still comes from shadow-lg. If the glass look is wanted, move surface-glass (and the border/shadow) to a wrapper element and keep the group background-free.

suggestion
1 <InputGroup className="absolute right-4 top-3 z-30 w-auto shadow-lg **:[input]:w-48">

Posted via Macroscope β€” UI Consistency

Addressed in 89460214. Removed surface-glass from the default InputGroup so the primitive has unambiguous ownership of its background while the floating elevation remains on shadow-lg.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding on the find bar's surface ownership. Everything else from the earlier passes (InputGroup composition, per-occurrence navigation, shortcut gating, input sizing via **:[input]:*) now matches the shared contracts.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxfileOutdated Β· 89d82cbOpen thread

surface-glass can't own the background on this element: InputGroup's default variant already paints the control surface (bg-background plus dark:bg-input/32), and the dark rule compiles to .dark\:bg-input\/32:is(.dark, .dark *) (0-2-0), which outranks the surface-glass utility (0-1-0). So in dark themes the bar is tinted with --input at 32% instead of --background at --glass-opacity, and in light themes the winner is decided purely by utility emission order rather than by a contract. The runtime glass tokens the earlier composition was meant to pick up therefore still don't apply. Every other glass surface (PullRequestCodeTab, UsageProviderChart, Button variant="glass") puts surface-glass on an element that owns no bg-*.

Smallest fix: let the input primitive own its surface and drop the utility here β€” the floating elevation still comes from shadow-lg. If the glass look is wanted, move surface-glass (and the border/shadow) to a wrapper element and keep the group background-free.

suggestion
1 <InputGroup className="absolute right-4 top-3 z-30 w-auto shadow-lg **:[input]:w-48">

Posted via Macroscope β€” UI Consistency

Addressed in 89460214. Removed surface-glass from the default InputGroup so the primitive has unambiguous ownership of its background while the floating elevation remains on shadow-lg.

Added 1 commit

  • fix(web): let find input own its surface8946021
MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding on the find bar's floating treatment: the surface-glass + shadow-lg panel classes are applied to an InputGroup whose own base/variant rules outrank them, so the glass background is dropped in dark mode and the elevation disappears while the field is focused.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 729–729Outdated Β· 8946021Open thread
……@@ -573,6 +725,60 @@ export const MessagesTimeline = memo(function MessagesTimeline({
574726 <TimelineRowActivityCtx value={activityState}>
575727 <div ref={setTimelineViewportElement} className="relative h-full min-h-0">
728+ {findOpen ? (
729+ <InputGroup className="absolute right-4 top-3 z-30 w-auto shadow-lg **:[input]:w-48">
MmacroscopeappBoton lines 729–729Aug 18, 2026

The floating-panel treatment is applied at the call site but loses to InputGroup's own rules by specificity, so it doesn't render as intended:

  • surface-glass sets background at specificity (0,1,0), while the default variant's dark:bg-input/32 compiles to .dark\:bg-input\/32:is(.dark, .dark *) at (0,2,0). In dark mode the panel therefore paints --input at 32% alpha instead of --background at --glass-opacity (80%), i.e. a far more transparent surface than every other glass panel (PullRequestCodeTab, UsageProviderChart, Button glass), with the message text scrolling visibly behind the field. In light mode bg-background and surface-glass tie on specificity, so which one wins depends on utility emit order.
  • shadow-lg is nulled whenever the input has :focus-visible β€” the base class carries has-[...,input:focus-visible,...]:shadow-none at (0,2,1) β€” which is the normal state here since the bar auto-focuses on open. The panel loses its elevation exactly while it is in use.

The smallest durable fix is to give the primitive the treatment rather than stacking it at the call site: add a glass variant to inputGroupVariants that owns the translucent surface and elevation (no bg-*/dark:bg-*, no focus shadow-none), mirroring the existing glass variant in button.tsx, and use variant="glass" here while keeping position/width local. Wrapping the group in a surface-glass div instead won't help, since the default variant's opaque bg-background would still cover the wrapper.

Posted via Macroscope β€” UI Consistency

This was generated against the previous head and is superseded by 89460214: the current call site no longer applies surface-glass, so neither background-specificity issue remains and there is no glass elevation contract to preserve. The control intentionally uses the standard InputGroup focus treatment; adding a new global glass variant would expand shared API without a current caller need.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Added 1 commit

  • Merge branch 'main' into codex/find-in-threada4dc729
Synced from GitHub

Hot damn, you have a lot of automated checks. I like the ui Consistency one. I might have to implement that in my own projects too.

Synced from GitHub

One finding on the find bar surface: in dark themes the floating panel is effectively transparent, so timeline text shows through it. Details inline.

Posted via Macroscope β€” UI Consistency

Added 1 commit

  • fix(web): make find overlay opaque in dark mode6fec1d2
MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding: the find bar's elevation class is cancelled by InputGroup's own focus-state contract, so the floating overlay renders flat in its default (autofocused) state.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 729–729Outdated Β· 6fec1d2Open thread
……@@ -573,6 +725,60 @@ export const MessagesTimeline = memo(function MessagesTimeline({
574726 <TimelineRowActivityCtx value={activityState}>
575727 <div ref={setTimelineViewportElement} className="relative h-full min-h-0">
728+ {findOpen ? (
729+ <InputGroup className="absolute right-4 top-3 z-30 w-auto shadow-lg dark:bg-background **:[input]:w-48">
MmacroscopeappBoton lines 729–729Aug 18, 2026

shadow-lg is cancelled exactly when this bar is in use. InputGroup's base contract includes has-[input:disabled,textarea:disabled,input:focus-visible,…]:shadow-none, which compiles to .has-\[…\]\:shadow-none:has(input:focus-visible, …) (0-2-1) and outranks the plain .shadow-lg utility (0-1-0). The bar focuses its input on open (the requestAnimationFrame focus effect), so the floating panel paints with no elevation in its normal state and only gains the drop shadow once focus leaves β€” the opposite of the intent, and it leaves the overlay visually unseparated from the messages scrolling underneath it.

That state rule is deliberate for an in-flow control (ring replaces shadow on focus), so the elevation belongs on a wrapper surface rather than on the control itself β€” the same composition the review overlay in PullRequestCodeTab.tsx uses (surface-glass … rounded-xl border border-border/60 shadow-lg wrapping its controls).

diff
1- <InputGroup className="absolute right-4 top-3 z-30 w-auto shadow-lg **:[input]:w-48">
2+ <div className="absolute right-4 top-3 z-30 rounded-[var(--control-radius)] shadow-lg">
3+ <InputGroup className="w-auto **:[input]:w-48">

…with the matching close (indent the existing children one level):

diff
1- </InputGroup>
2+ </InputGroup>
3+ </div>

Posted via Macroscope β€” UI Consistency

Addressed in 673fbf46. Positioning, radius, and shadow-lg now live on a wrapper, while InputGroup retains ownership of its focus ring and inline-field shadow states, so elevation remains stable as focus moves within the find widget.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding on the new find-in-thread bar: the floating elevation applied at the call site is neutralized by InputGroup's own focus rule, so the panel renders flat in its default state.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 729–729Outdated Β· 6fec1d2Open thread
……@@ -573,6 +725,60 @@ export const MessagesTimeline = memo(function MessagesTimeline({
574726 <TimelineRowActivityCtx value={activityState}>
575727 <div ref={setTimelineViewportElement} className="relative h-full min-h-0">
728+ {findOpen ? (
729+ <InputGroup className="absolute right-4 top-3 z-30 w-auto shadow-lg dark:bg-background **:[input]:w-48">
MmacroscopeappBoton lines 729–729Aug 18, 2026

shadow-lg is inert while the field is focused. InputGroup's base contains has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,...]:shadow-none, which emits a :has(input:focus-visible) selector at specificity (0,2,1) and outranks the call-site .shadow-lg (0,1,0); tailwind-merge keeps both because the modifiers differ. The bar auto-focuses its input on open, so the floating panel shows no elevation in its normal state and only pops the shadow back when focus moves to the chevron/close buttons β€” elevation flickers as focus moves inside one widget.

That shadow-none is the inline-field contract (focus ring replaces the shadow), so panel elevation shouldn't be owned by the field. Consider putting the floating treatment on a positioning wrapper and leaving InputGroup its own shadow states:

tsx
1<div className="absolute right-4 top-3 z-30 rounded-[var(--control-radius)] shadow-lg">
2 <InputGroup className="w-auto dark:bg-background **:[input]:w-48">
3 ...
4 </InputGroup>
5</div>

Posted via Macroscope β€” UI Consistency

Addressed in 673fbf46. Positioning, radius, and shadow-lg now live on a wrapper, while InputGroup retains ownership of its focus ring and inline-field shadow states, so elevation remains stable as focus moves within the find widget.

Added 1 commit

  • fix(web): keep find overlay elevation stable673fbf4
apps/web/src/components/chat/MessagesTimeline.tsxline 445Outdated Β· 673fbf4Open thread
……@@ -420,10 +427,155 @@ export const MessagesTimeline = memo(function MessagesTimeline({
442+ () => findMessageEntries.map((entry) => renderMarkdownSearchText(entry.message.text)),
443+ [findMessageEntries],
444+ );
445+ const findMatches = useMemo(() => findTextMatches(findTexts, findQuery), [findQuery, findTexts]);
MmacroscopeappBoton line 445Aug 18, 2026

🟑 Medium chat/MessagesTimeline.tsx:445

When messages are removed, findMatchIndex can remain past the end of findMatches, so the UI shows invalid counts such as 3/1 and the effect finds no active match to highlight or navigate to. Clamp the selected index to the current match count whenever findMatches changes.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 445:
2
3When messages are removed, `findMatchIndex` can remain past the end of `findMatches`, so the UI shows invalid counts such as `3/1` and the effect finds no active match to highlight or navigate to. Clamp the selected index to the current match count whenever `findMatches` changes.

Addressed in d6df6773. The effective match index is now synchronously clamped to the current result count, so the counter, active highlight, and navigation never observe an out-of-range cursor after messages are removed.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Added 1 commit

  • fix(web): clamp active find matchd6df677

Added 4 commits

  • fix(web): refine in-thread find behavior3c7ee6e
  • feat(web): add match-case thread search2c808cb
  • fix(web): refine thread find controls0b0266b
  • fix(web): emphasize match case state4838691
MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding on the new find bar: the "Match case" control reconstructs the Toggle primitive on top of Button and overrides its pressed colors with !important.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 822–838Outdated Β· 4838691Open thread
……@@ -573,6 +802,78 @@ export const MessagesTimeline = memo(function MessagesTimeline({
835+ }}
836+ >
837+ <span className="text-[11px] font-semibold leading-none">Aa</span>
838+ </Button>
MmacroscopeappBoton lines 822–838Aug 18, 2026

chat/MessagesTimeline.tsx:838

This rebuilds the Toggle primitive on top of Button: data-pressed is set by hand (findCaseSensitive || undefined), aria-pressed is mirrored manually, and the ghost variant's pressed state ([:hover,[data-pressed]]:bg-accent) is then overridden with three !important utilities to repaint it as bg-primary/text-primary-foreground. Two consequences: the pressed state no longer matches any other pressed control in the app (Toggle ghost is data-pressed:bg-accent data-pressed:text-accent-foreground), and because data-pressed also drives hover selectors, the un-pressed hover/pressed distinction is now decided entirely by call-site !important rather than the primitive contract.

The app already owns this exact control β€” ProjectContentSearchDialog's SearchOptionButton renders the "Aa" match-case option as <Toggle pressed … variant="ghost">, and PanelLayoutControls uses the same pressed/onPressedChange shape. Consider composing Toggle here (Base UI emits aria-pressed and data-pressed for you) and dropping the ! overrides, keeping only contextual classes:

tsx
1<Toggle
2 size="xs"
3 variant="ghost"
4 pressed={findCaseSensitive}
5 onPressedChange={(pressed) => {
6 setFindCaseSensitive(pressed);
7 setFindMatchIndex(0);
8 onManualNavigation();
9 }}
10 aria-label="Match case"
11 title="Match case"
12>
13 <span className="text-[11px] font-semibold leading-none">Aa</span>
14</Toggle>

(needs import { Toggle } from "../ui/toggle";). If the accent pressed tint really is too subtle inside the input group, extending the shared toggle contract is preferable to an !important override at one call site.

No longer relevant as of 8753a9bd9f7b0e798f99897084f8a5c014eedd7e

MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One consistency finding in the new find bar: the "Match case" control recreates the shared Toggle primitive with a hand-managed pressed state.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 822–837Outdated Β· 4838691Open thread
……@@ -573,6 +802,78 @@ export const MessagesTimeline = memo(function MessagesTimeline({
834+ onManualNavigation();
835+ }}
836+ >
837+ <span className="text-[11px] font-semibold leading-none">Aa</span>
MmacroscopeappBoton lines 822–837Aug 18, 2026

chat/MessagesTimeline.tsx:837

This rebuilds Toggle out of Button: the pressed state is expressed by hand via aria-pressed plus a manually written data-pressed attribute, so the call site now owns toggle semantics that the primitive already owns. data-pressed is a Base UI-emitted attribute everywhere else in the app (only variant class strings reference it) β€” this is the first place it is set as a literal prop, and the app already has this exact "Aa / Match case" control built on the shared primitive in ProjectContentSearchDialog's SearchOptionButton (<Toggle pressed={…} variant="ghost" size="compact">).

Consider using Toggle with pressed/onPressedChange; size="xs" matches the icon-xs geometry (size-7 sm:size-6) used by the sibling addon buttons.

diff
1- <Button
2- type="button"
3- size="icon-xs"
4- variant="ghost"
5- data-pressed={findCaseSensitive || undefined}
6- aria-label="Match case"
7- aria-pressed={findCaseSensitive}
8- title="Match case"
9- onClick={() => {
10- setFindCaseSensitive(!findCaseSensitive);
11- setFindMatchIndex(0);
12- onManualNavigation();
13- }}
14- >
15+ <Toggle
16+ size="xs"
17+ variant="ghost"
18+ aria-label="Match case"
19+ pressed={findCaseSensitive}
20+ title="Match case"
21+ onPressedChange={(pressed) => {
22+ setFindCaseSensitive(pressed);
23+ setFindMatchIndex(0);
24+ onManualNavigation();
25+ }}
26+ >
27 <span className="text-[11px] font-semibold leading-none">Aa</span>
28- </Button>
29+ </Toggle>

(requires import { Toggle } from "../ui/toggle";)

No longer relevant as of 8753a9bd9f7b0e798f99897084f8a5c014eedd7e

Added 3 commits

  • fix(web): use shared toggle for match casecce4f3b
  • fix(web): keep thread find bar responsived5cf92e
  • fix(web): right-anchor responsive find bar8753a9b
MmacroscopeappBotreviewed Β· 2 inline threadsAug 18, 2026
Synced from GitHub

Reviewed the find-in-thread UI against the shared component system. Two findings: the floating panel's elevation is applied to a transparent full-width wrapper, and the new Toggle primary variant duplicates ghost for a single consumer while diverging from the app's existing "Match case" control.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/ui/toggle.tsxlines 29–29Outdated Β· 8753a9bOpen thread
……@@ -25,6 +25,8 @@ const toggleVariants = cva(
2626 ghost:
2727 "border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-accent data-pressed:text-accent-foreground disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",
28+ primary:
29+ "border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-primary data-pressed:text-primary-foreground disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",
MmacroscopeappBoton lines 29–29Aug 18, 2026

This new primary variant is a verbatim copy of ghost apart from the two data-pressed: colors, and it has exactly one consumer (the find bar's Aa toggle). The app already renders the same "Match case" control in ProjectContentSearchDialog (SearchOptionButton) with variant="ghost", so the identical control now has two different pressed treatments (bg-accent vs a solid bg-primary fill), and any future change to ghost's shared contract has to be duplicated here to stay in sync.

Consider dropping this variant and rendering the find bar toggle with variant="ghost" so the pressed state matches the existing match-case control; if a solid pressed fill is genuinely wanted, it's worth applying it to both call sites so the variant has a shared contract.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 807–808Outdated Β· 8753a9bOpen thread
……@@ -573,6 +803,76 @@ export const MessagesTimeline = memo(function MessagesTimeline({
575805 <div ref={setTimelineViewportElement} className="relative h-full min-h-0">
806+ {findOpen ? (
807+ <div className="absolute right-4 top-3 z-30 w-[calc(100%-2rem)] max-w-sm rounded-[var(--control-radius)] shadow-lg">
808+ <InputGroup className="w-full dark:bg-background">
MmacroscopeappBoton lines 807–808Aug 18, 2026

chat/MessagesTimeline.tsx:808

shadow-lg is on a transparent full-width wrapper (inset-x-4), but the visible panel is the max-w-sm InputGroup that ml-auto pushes to the right. An outer box shadow is painted around the wrapper's border box, so the elevation renders as a band across the whole timeline width β€” including the empty region to the left of the field β€” rather than hugging the find bar.

Moving the width constraint onto the shadowed wrapper keeps the shadow on the panel, and still avoids InputGroup's has-[input:disabled,textarea:disabled,input:focus-visible,…]:shadow-none, which would cancel the shadow if it were applied to the group itself.

suggestion
1 <div className="absolute inset-x-4 top-3 z-30 ml-auto w-full max-w-sm rounded-[var(--control-radius)] shadow-lg">
2 <InputGroup className="dark:bg-background">

No longer relevant as of 8753a9bd9f7b0e798f99897084f8a5c014eedd7e

MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

One finding on the find bar's "Aa" toggle: the call-site font-size override is defeated by the primitive's responsive text ramp at sm and above.

Posted via Macroscope β€” UI Consistency

apps/web/src/components/chat/MessagesTimeline.tsxlines 827–827Outdated Β· 8753a9bOpen thread
……@@ -573,6 +803,76 @@ export const MessagesTimeline = memo(function MessagesTimeline({
824+ pressed={findCaseSensitive}
825+ size="xs"
826+ variant="primary"
827+ className="font-mono text-[11px]"
MmacroscopeappBoton lines 827–827Aug 18, 2026

chat/MessagesTimeline.tsx:827

text-[11px] only takes effect below the sm breakpoint. Toggle's base string is ... text-base ... sm:text-sm ..., and tailwind-merge keys utilities by modifier, so the unprefixed text-[11px] only replaces text-base while sm:text-sm survives the merge and wins at >=640px (responsive utilities are emitted after base ones at equal specificity). On desktop the "Aa" label therefore renders at 14px inside a 24px-tall size="xs" toggle, larger than the 12px match counter beside it. Consider carrying the override through the sm variant as well.

suggestion
1 className="font-mono text-[11px] sm:text-[11px]"

No longer relevant as of 666bcb9cb859963a9889fcd092686b589ca73cde

apps/web/src/components/chat/MessagesTimeline.tsxline 615Outdated Β· 8753a9bOpen thread
……@@ -420,10 +430,230 @@ export const MessagesTimeline = memo(function MessagesTimeline({
612+ observer.observe(timelineViewportElement, { childList: true, subtree: true });
613+ const mountFrame = requestAnimationFrame(() => {
614+ paintFrame = requestAnimationFrame(() => {
615+ const activeRange = paintHighlights();
MmacroscopeappBoton line 615Aug 18, 2026

🟑 Medium chat/MessagesTimeline.tsx:615

Find navigation reports and selects matches inside collapsed CollapsibleUserMessageBody content without revealing them, so matches beyond the max-h-44 overflow-hidden cutoff remain invisible even after the outer list scrolls. Expand the matched user message (or otherwise reveal the range) before painting and scrolling to it.

πŸ€– Copy this AI Prompt to have your agent fix this:
text
1In file @apps/web/src/components/chat/MessagesTimeline.tsx around line 615:
2
3Find navigation reports and selects matches inside collapsed `CollapsibleUserMessageBody` content without revealing them, so matches beyond the `max-h-44 overflow-hidden` cutoff remain invisible even after the outer list scrolls. Expand the matched user message (or otherwise reveal the range) before painting and scrolling to it.
MmacroscopeappBotreviewed Β· 1 inline threadAug 18, 2026
Synced from GitHub

UI consistency review: one finding on the new Toggle primary variant (pressed fill is overridden by the base hover rule). The rest of the find bar now composes the shared primitives (InputGroup, Toggle, Button) and the ::highlight() rules match the existing search-match tone (bg-primary/25 in HighlightedSearchLine).

Posted via Macroscope β€” UI Consistency

apps/web/src/components/ui/toggle.tsxlines 28–29Outdated Β· 8753a9bOpen thread
……@@ -25,6 +25,8 @@ const toggleVariants = cva(
2626 ghost:
2727 "border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-accent data-pressed:text-accent-foreground disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",
28+ primary:
29+ "border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-primary data-pressed:text-primary-foreground disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",
MmacroscopeappBoton lines 28–29Aug 18, 2026

ui/toggle.tsx:29

The new primary variant leaves the base hover:bg-accent in charge of the pressed state. .hover\:bg-accent:hover and .data-pressed\:bg-primary[data-pressed] tie on specificity (0,2,0) and Tailwind emits the hover: utility later, so hovering a pressed toggle repaints the fill to --accent while data-pressed:text-primary-foreground keeps the label white β€” in light themes that is white on zinc-100, so the "Aa" label vanishes exactly while the pointer is over it (in dark themes the on-state just disappears).

Every other filled/pressed surface in the app pins both states together instead of relying on emit order: Button uses [:hover,[data-pressed]]:bg-primary/90, and AddProviderInstanceDialog re-asserts data-checked:hover:bg-primary/8. Consider re-asserting the pressed fill under hover here too.

suggestion
1 primary:
2 "border-transparent text-foreground shadow-none [:disabled,:active,[data-pressed]]:shadow-none before:shadow-none data-pressed:bg-primary data-pressed:text-primary-foreground data-pressed:hover:bg-primary/90 disabled:opacity-100 disabled:text-muted-foreground disabled:[&_svg]:opacity-100",

No longer relevant as of 666bcb9cb859963a9889fcd092686b589ca73cde

Added 2 commits

  • fix(web): align find toggle and reveal matches666bcb9
  • fix(web): reopen thread find with match shortcuts1e856b4