Skip to content

Agent chat: terminal theme tokens + in-conversation search#6006

Open
lawrencecchen wants to merge 6 commits into
feat-agchat-rich-toolsfrom
feat-agchat-theme-search
Open

Agent chat: terminal theme tokens + in-conversation search#6006
lawrencecchen wants to merge 6 commits into
feat-agchat-rich-toolsfrom
feat-agchat-theme-search

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Stacked on #5967 (rich tool rendering), in the #5736 lane.

Terminal theme tokens. Reuses the agent-session theme seam end to end (AgentSessionWebTheme.resolve + applyAgentTheme): the panel appearance resolves to the shared token set, rides the chat.init reply for the first paint, and pushes via cmuxAgentChatBridge.applyTheme on appearance change. styles.css gains scheme-aware token defaults so dev/mock mode keeps the system fallback. User bubbles, tool rows, diff colors, and banners all read the variables.

In-conversation search. Cmd+F (page-level, not a global cmux shortcut) or a header button opens a search bar: case-insensitive substring match over message text, tool titles/names, output, and command input; match counter; Enter/Shift+Enter step with wraparound and scroll the current match into view; Escape closes and clears; a toggle filters the timeline to matches only. Matching rows get an accent ring (current match an outline); plain-text user bubbles get inline marks, markdown bodies keep row-level highlight only (no marks inside sanitized HTML). An active search pauses auto-follow; clearing restores it. All matching/highlighting is bounded (100k chars scanned per field, 20k chars / 100 marks decorated per text) so multi-MB tool outputs cannot freeze the render path. No raw useEffect: the one document-level Cmd+F listener lives in a narrow-contract hook, focus is a re-keyed callback ref, everything else is derived render state.

Worker note: the theme commit and the bounded search module were built by one worker session; the UI wiring, styles, and tests were completed by the manager after that session died on a rate limit.

Verification: bun test 88 pass (search reducer/matcher/highlight bounds suite added), typecheck, lint:ci, verify:tanstack-router, build-webviews-app.sh assets rebuilt and committed.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
UI-only webview bundle changes (theming + client-side search) with bounded scanning and no auth or data-path changes; main risk is render/scroll edge cases on very large transcripts.

Overview
Adds terminal-derived theme tokens and in-conversation search to the Agent Chat webview (agentChatSurface.mjs rebuild).

Theme: chat.init now applies a validated theme on first paint (st(r.theme)); CSS defines --agent-* defaults with prefers-color-scheme and data-theme overrides so bubbles, diffs, and banners stay consistent before/after native theme pushes via cmuxAgentChatBridge.applyTheme.

Search: Cmd/Ctrl+F or a header button opens a search bar with match counter, prev/next (Enter/Shift+Enter), optional “matches only” filter, row highlights and inline marks on plain user text, and scroll-into-view for the current match. Matching scans message/tool fields with caps (100k per field, bounded highlight segments) and a WeakMap cache per item/query; an active query pauses auto-follow to latest. Search input focuses once per open via re-keyed input + WeakSet; IME composition is ignored for Enter/Escape.

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


Summary by cubic

Adds terminal-derived theme tokens to Agent Chat and in-conversation search (Cmd/Ctrl+F) with next/prev, match counter, and optional filtering. First paint is themed via chat.init; search stays fast on large, streaming outputs with an incremental cache and bounded scanning.

  • New Features

    • Terminal theme tokens: reuse the agent-session seam end to end; tokens ride chat.init and update via cmuxAgentChatBridge.applyTheme; scheme-aware CSS fallbacks cover dev/mock; webview background is transparent; rows, diffs, banners, and bubbles read --agent-* vars.
    • In-conversation search: open via Cmd/Ctrl+F or header button; case-insensitive substring match over message text, tool titles/names, output, input command, and visible tool inputs (paths, queries, URLs, patch text). Also matches argv commands, edit pairs (old_string/new_string), file contents, and edits entries (bounded one-pass descent, depth 3, 64 array entries). Counter and Enter/Shift+Enter with wrap + scroll-into-view; Escape closes; toggle to show matches only (filtered view omits turn separators); row-level highlight with current-match outline, inline marks for plain user text; active search pauses auto-follow. Incremental identity cache rescans only changed items; bounds: 100k chars per field scan, 20k chars / 100 marks per text. Tests cover the reducer, matcher/highlighter (incl. argv/edit/content coverage), and theme parsing; webview assets rebuilt.
  • Bug Fixes

    • Search bar focus: input focuses only on first mount per open (stable ref + WeakSet), avoiding focus steal during navigation/toggles/typing and preventing IME issues.
    • Do not intercept Enter/Escape during IME composition in the search field.

Written for commit 41a140c. Summary will update on new commits.

Review in cubic

lawrencecchen and others added 2 commits June 12, 2026 14:04
…heme seam

Reuses AgentSessionWebTheme.resolve + applyAgentTheme end to end: the panel
appearance resolves to the shared token set, rides the chat.init reply for
the initial paint, and pushes via cmuxAgentChatBridge.applyTheme on change.
styles.css gains scheme-aware token defaults so dev/mock mode keeps the
system fallback.
… mode)

Pure search module (bounded matcher + highlighter, reducer-shaped UI
state) + a search bar wired into the surface: Cmd+F or the header
button opens it, Enter/Shift+Enter step with wraparound, Escape closes
and clears, a toggle filters the timeline to matching items only (no
turn separators in the filtered view). Matching rows get an accent
ring, the current match an outline and scrollIntoView, plain-text user
bubbles get inline marks (markdown bodies keep row-level highlight
only). An active search pauses auto-follow; clearing restores it. All
scanning is bounded so multi-MB tool outputs cannot freeze rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 11:02pm
cmux-staging Building Building Preview, Comment Jun 12, 2026 11:02pm

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5aa3f51-1a31-421b-be97-173039e729aa

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
  • Commit unit tests in branch feat-agchat-theme-search

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 and usage tips.

Comment thread webviews/src/agent-chat/react/SearchBar.tsx Outdated
Comment thread webviews/src/agent-chat/search.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds terminal-derived theme tokens and Cmd/Ctrl+F in-conversation search to the Agent Chat webview. The theme seam reuses AgentSessionWebTheme.resolve end-to-end: tokens ride chat.init for a race-free first paint and are pushed on appearance change via the existing applyTheme bridge. The search feature is well-architected: a reducer-owned UI state, a WeakMap identity cache that makes per-render scanning incremental, per-field 100k-char scan caps and 20k-char/100-mark highlight bounds, IME-safe keyboard handling, and a display:contents wrapper that applies match styling without touching row layout.

  • Theme tokens: CSS gains scheme-aware --agent-* defaults; parseAgentChatTheme validates the full token set before calling applyAgentTheme, so partial or missing tokens leave the system-color fallback in place.
  • Search: match counter, Enter/Shift+Enter navigation with wraparound and scrollIntoView, Escape to close, filter-mode toggle, and auto-follow pause while a query is active; inline <mark> highlights for plain user bubbles only (markdown rows stay row-level).
  • Tests: 88-test suite covers reducer edge cases, matcher field coverage (including Codex argv and MultiEdit shapes), highlight bounds, and the incremental cache's zero-rescan behavior on unchanged items.

Confidence Score: 5/5

Safe to merge. The search and theme paths are well-isolated, bounded against large inputs, and covered by 88 tests; no correctness issues were found in the Swift or TypeScript changes.

Both features make additive, well-scoped changes. The search reducer is pure and thoroughly tested; the WeakMap cache correctly handles streaming item replacement; theme validation rejects partial payloads rather than half-applying them. Swift changes mirror the established agent-session coordinator pattern with proper idempotency guards. No correctness or data-integrity issues were found.

No files require special attention. The one style note is in AgentChatApp.tsx (memoizing the computeMatches result to keep matchedIndexes identity-stable during navigation).

Important Files Changed

Filename Overview
webviews/src/agent-chat/search.ts New search engine: reducer-shaped UI state, case-insensitive substring matcher with per-field 100k-char cap, WeakMap identity cache for incremental rescans, and bounded highlight segmenter (20k chars / 100 marks). Well-specified and tested.
webviews/src/agent-chat/react/AgentChatApp.tsx Adds search reducer, derived match list, stepSearch with scroll-into-view, and auto-follow pause when search is active. computeMatches runs inline without useMemo, producing a new array reference every render.
webviews/src/agent-chat/react/SearchBar.tsx New search bar: pure view over SearchUIState, IME-safe key handler, WeakSet-guarded focus-on-first-mount, useSearchHotkey for document-level Cmd/Ctrl+F. ARIA attributes are correct.
webviews/src/agent-chat/react/rows.tsx Wraps each ItemRow in a display:contents sentinel that carries data-search-match/data-search-current attributes; CSS targets matches without threading search props into every row family. Inline <mark> highlights only for plain-text user bubbles.
webviews/src/agent-chat/theme.ts New theme module: validates unknown init-reply payload against the full AgentSessionTheme token set (all 13 string keys + isDark) before delegating to the shared applyAgentTheme seam. Partial/mistyped tokens correctly leave CSS fallbacks untouched.
Sources/AgentChat/AgentChatWebViewController.swift Adds apply(theme:) with an idempotency guard and pushThemeToPage() via evaluateJavaScript; sets drawsBackground = false / underPageBackgroundColor = .clear for transparent backdrop. Mirrors the existing agent-session coordinator pattern.
Sources/AgentChat/AgentChatPanelView.swift Passes AgentSessionWebTheme.resolve(appearance:) into AgentChatWebViewRepresentable; both make and update call controller.apply(theme:), which guards against no-op JS evaluations.
webviews/src/agent-chat/styles.css Adds scheme-aware --agent-* token defaults scoped to html[data-cmux-webview-kind="agent-chat"], light-variant companions for host-delivered themes, and all search-bar/match-highlight CSS. display:contents + child combinator correctly applies match ring without boxing the wrapper.
webviews/src/agent-chat/search.test.ts Thorough unit tests covering reducer edge cases, matcher field coverage, highlight bounds, and the WeakMap identity cache's incremental scan behavior.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[macOS: AgentChatPanelView] -->|AgentSessionWebTheme.resolve| B[AgentChatWebViewRepresentable]
    B -->|apply theme on make/update| C[AgentChatWebViewController]
    C -->|evaluateJavaScript applyTheme JSON| D[WKWebView Page]
    C -->|chat.init reply with theme dict| D
    D -->|applyAgentChatTheme validates + applies| E[CSS --agent-* custom props]

    F[AgentChatApp] -->|useReducer reduceSearchUI| G[SearchUIState]
    G -->|open/query/cursor/filterMode| H[SearchBar UI]
    F -->|computeMatches + WeakMap cache| I[matchedIndexes array]
    I -->|matchedIndexSet + filterMode| J[Timeline rows]
    J -->|data-search-match/current attrs| K[CSS match ring / outline]
    J -->|highlightSegments bounded| L[mark elements in UserMessageRow]
    H -->|stepSearch + scrollIntoView| J
Loading

Reviews (5): Last reviewed commit: "Search: match argv commands, edit pairs,..." | Re-trigger Greptile

Comment on lines +114 to +115
const openSearch = () => dispatchSearch({ type: "open" });
useSearchHotkey(openSearch);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 openSearch is recreated as a new arrow function on every render, so useSearchHotkey's [onOpen] dependency changes every render and the effect runs cleanup+setup on every AgentChatApp render. This directly contradicts the hook's documented invariant ("attaches exactly one keydown listener for the component's lifetime"). During streaming (frequent renders) the listener is re-attached on every incoming event. Wrapping in useCallback with an empty dependency array is safe because dispatchSearch from useReducer is guaranteed stable.

Suggested change
const openSearch = () => dispatchSearch({ type: "open" });
useSearchHotkey(openSearch);
const openSearch = useCallback(() => dispatchSearch({ type: "open" }), []);
useSearchHotkey(openSearch);

Two autoreview findings. computeMatches rescanned every item (up to
100k chars per field) on every render while streaming; items are
immutable snapshots and updates replace only the changed object, so a
WeakMap identity cache makes the scan incremental (cache-hit for
unchanged items, one rescan per changed item or query change; test seam
proves the scan counts). The matcher also missed input strings the tool
rows visibly render (file paths, web queries/URLs, apply_patch text);
those keys are now indexed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 12, 2026 15:01
The inline callback ref was recreated per render, so every search-bar
update (next/prev, filter toggle, typing) refocused the field, stealing
focus from the controls and risking IME composition issues. The ref is
now a stable module-level function guarded by a WeakSet, focusing each
re-keyed input node exactly once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread webviews/src/agent-chat/search.ts
…puts

Codex shell commands arrive as argv arrays, and file-change rows render
old_string/new_string, MultiEdit edits entries, and Write content; the
matcher skipped all of those, so visible diff/command text returned no
match and filter mode hid the row. Bounded one-pass descent (depth 3,
64 array entries per field) covers the rendered shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 and found 1 potential issue.

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 41a140c. Configure here.

"content",
"new_source",
"edits",
] as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Search skips visible input keys

Medium Severity

In-conversation search walks tool input objects via SEARCHED_INPUT_KEYS, but that list omits field names the rich tool rows already use for display— notably cmd (Codex exec_command arguments in fixtures), q (web search), and script (command execution). Text visible in a row can fail to match even though the query appears in the UI.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41a140c. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant