feat(ui): dedicated chat components for tool search/select/query - #430
feat(ui): dedicated chat components for tool search/select/query#430Zerlight wants to merge 7 commits into
Conversation
…rch results inline
There was a problem hiding this comment.
Pull request overview
This PR improves the chat transcript UI for tool calls by introducing dedicated rendering for “search-like” tool calls (including Claude’s ToolSearch) and by normalizing Codex MCP tool titles to the shared mcp__<server>__<tool> slug so MCP server identity is consistently preserved in the UI and in history replays.
Changes:
- Add ToolSearch-specific parsing + rendering (inline tool-name rows, humanized headers, preserved context across states).
- Humanize generic search tool headers to summarize counts (matches/files) while keeping raw queries in the expandable body only.
- Normalize Codex MCP tool-call titles (live adapter + history replay) to
mcp__<server>__<tool>, including plugin-app namespace unwrapping; update mocks/tests and fix git fixture signing config for integration tests.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/presentation/ui/src/tool-utils.ts | Adds toolCallSearchCounts, adjusts search header/body behavior, and integrates ToolSearch handling into body visibility logic. |
| packages/presentation/ui/src/chat/tool-search.tsx | New ToolSearch result renderer that displays loaded tool names inline with MCP server suffix. |
| packages/presentation/ui/src/chat/tool-result-preview.tsx | Routes ToolSearch calls to the dedicated renderer; updates search preview card icon/behavior for empty-output searches. |
| packages/presentation/ui/src/chat/tool-result-content.ts | Adds toolSearchPresentation parsing to classify ToolSearch output into rows vs. prose message. |
| packages/presentation/ui/src/chat/tool-kind-icons.ts | Updates the search kind icon to TextSearchIcon. |
| packages/presentation/ui/src/chat/tool-call-item.tsx | Humanizes search headers (ToolSearch verbs + generic search counts), suppresses search metadata badges in the body. |
| packages/presentation/ui/src/chat/activity-run.tsx | Swaps activity “explore” icon to TelescopeIcon. |
| packages/presentation/ui/src/chat/tests/tool-search.test.tsx | Adds unit tests covering ToolSearch header/body behavior across states and MCP name splitting. |
| packages/presentation/ui/src/chat/tests/tool-result-content.test.ts | Adds unit tests for toolSearchPresentation parsing behavior. |
| packages/presentation/ui/src/chat/tests/tool-call-metadata.test.tsx | Updates tests to validate search header summarization/count extraction and query placement. |
| packages/presentation/i18n/src/locales/en.ts | Adds localized strings for ToolSearch header verbs and search count summaries. |
| packages/presentation/i18n/src/locales/zh-cn.ts | Adds localized strings for ToolSearch header verbs and search count summaries (zh-CN). |
| packages/host/engine/tests/integration/git-mutations.test.ts | Disables GPG signing in the git fixture to make commits reliable on signing-enabled machines. |
| packages/host/agent-adapter/src/native/codex/history-tools.ts | Normalizes rollout/history MCP function calls into mcp__<server>__<tool> titles. |
| packages/host/agent-adapter/src/native/codex/adapter.ts | Normalizes live Codex MCP tool-call titles to the shared MCP slug and unwraps plugin namespace. |
| packages/host/agent-adapter/src/tests/codex-mcp-tools.test.ts | Adds tests for Codex MCP slug normalization and plugin namespace unwrapping. |
| packages/host/agent-adapter/src/tests/codex-history.test.ts | Adds tests ensuring history replay uses the same MCP slug format as live events. |
| packages/client/workbench/src/mock/data/showcase.ts | Updates mock showcase tool bursts to exercise new search/ToolSearch presentation paths. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /** Rollout MCP rows are `function_call`s whose sibling `namespace` is `mcp__<server>` (observed | ||
| * with a stray trailing `__` on some rows); `name` is the bare tool. Plugin apps namespace as | ||
| * `mcp__codex_apps__<app>` with one leading `_` on the tool name — surface the app as the | ||
| * server, like the live adapter does. Built-ins carry no namespace or a non-`mcp__` one. | ||
| * (Verified against real 0.131–0.146 rollouts, 2026-08.) */ |
| // Emit the shared `mcp__<server>__<tool>` slug — the UI's server/tool join key — instead | ||
| // of codex's raw `server.tool`. Plugin apps all mount under the one `codex_apps` server | ||
| // with the plugin in the tool's first segment; surface the plugin as the server. |
| /** A ToolSearch settle: the loaded tools as one inline line (the humanized header already says | ||
| * what happened, so the body is only the result). MCP slugs shed their `mcp__<server>__` envelope | ||
| * like tool headers do, keeping the server as a muted suffix. */ |
There was a problem hiding this comment.
[!IMPORTANT]
The new
ToolSearchrendering is solid, but thetoolCallHeaderSummarysearch-case deletion strips header context from search tools that don't emit match counts —WebSearchcollapses to a bareWebSearchlabel. Verified by rendering throughToolCallItem.
Reviewed changes
This PR adds dedicated presentation for Claude Code's deferred-tool ToolSearch calls and normalizes Codex MCP tool titles.
- New
ToolSearchResultcomponent (packages/presentation/ui/src/chat/tool-search.tsx) plustoolSearchPresentation()intool-result-content.ts, splittingselect:mode from keyword mode and renderingmcp__server__toolastool (server). tool-call-item.tsxgrows a header branch computingtoolSearch/searchCountsand swaps inToolCaseIcon; search-kind rows now suppress generic metadata.tool-utils.tsaddstoolCallSearchCounts()and changestoolCallHeaderSummary'scase 'search'from returning the query tobreak;SearchRowsdrops its result-count badge.- Codex adapter titles change from
${server}.${tool}tomcp__${server}__${tool}, with a newcodexMcpToolName()handling the rollout-JSONL replay path. - i18n keys added to both
zh-cn.tsanden.tswith matching key sets and valid ICU plurals.
ℹ️ The Codex title change is safe — confirming so it isn't re-litigated
I traced whether mcp__${server}__${tool} could break anything downstream that keys off toolCall.title. It can't: the engine reads title only for approval toolTitle (session-event-processor.ts:150), Codex MCP tools announce as kind: 'other' with no approval path, and deduplication is by toolCallId throughout conversation.ts. codexToolSettle spreads ...existing, so the announce title survives the settle. No action needed — noting it as verified negative evidence.
ℹ️ Nitpicks
The PR bundles three loosely-related concerns — the UI search presentation, the Codex MCP title normalization, and the commit.gpgsign=false git fixture. They're each small, so this isn't worth splitting retroactively, but the title only describes the first.
Separately, the description promises "humanized query + scope" in the header, while the implemented behavior shows counts and removes the query. Worth reconciling the description with what shipped, since the two read as contradictory to a future reader.
Finally, the "I ran the affected surface and observed the change working" checklist item is unchecked — and the search-header path above is exactly the surface where a manual pass would have caught the regression.
Claude Opus | 𝕏
| break; | ||
| } | ||
| // Search queries are raw machine strings (regexes, select: lists) — the localized header | ||
| // composes counts via toolCallSearchCounts instead, and the query stays in the body card. |
There was a problem hiding this comment.
Deleting the label = toolCallSearchQuery(toolCall) assignment removes the only header context for any search tool that doesn't produce numMatches/numFiles. Those rows now render a bare tool name with nothing else.
I verified this empirically by rendering a WebSearch-shaped ToolCall through ToolCallItem:
contextSummary: undefined
searchCounts: undefined
HEADER TEXT: "WebSearch"
Before this PR that header showed the search query. Now the user sees WebSearch with no indication of what was searched for.
The justifying comment — "Search queries are raw machine strings (regexes, select: lists)" — is true for Grep/Glob/ToolSearch but false for WebSearch, whose query is human prose ("linkcode release notes") and whose rawOutput carries durationSeconds, never numMatches/numFiles. So neither the query nor a count reaches the header.
The SearchRows badge removal in tool-result-preview.tsx compounds it: for these tools no count survives in the body either.
Technical details
Two independent paths lead here.
1. WebSearch is classified as kind: 'search'. toolKindFromName() in packages/host/agent-adapter/src/util.ts tests SEARCH_TOOL_NAME_RE = /search|grep|glob|find/ before FETCH_TOOL_NAME_RE = /fetch|web|http|browser/, so WebSearch matches the search branch and never reaches fetch. It arrives at the UI as { title: 'WebSearch', kind: 'search' } and flows through this changed arm.
2. toolCallSearchCounts is record-only. It calls recordValue(toolCall.rawOutput), which rejects both strings and arrays. Adapters whose search rawOutput is a bare string or an array of results (Pi, opencode) return undefined for the same reason, so this isn't limited to WebSearch — it's every search-kind tool outside Claude's Grep/Glob shape.
A targeted fix: keep the query as the fallback label when counts are unavailable, rather than dropping it unconditionally.
case 'search': {
if (toolCallSearchCounts(toolCall)) break;
label = toolCallSearchQuery(toolCall);
break;
}That preserves the intended behavior for Grep/Glob/ToolSearch (counts win, query moves to the body card) while leaving WebSearch and non-record-output adapters with a meaningful header.
lucas77778
left a comment
There was a problem hiding this comment.
Blocking findings:
-
packages/presentation/ui/src/tool-utils.ts:203-206removes the query from every generic search header, whileToolCallItemonly substitutes counts whenrawOutputhas a supported count envelope.WebSearchis classified askind: searchbytoolKindFromName, and Pi/OpenCode-style searches also commonly have nonumMatches/numFiles, so their collapsed rows become a bare tool name (and all in-progress searches are indistinguishable). I reproduced the resulting shape as{kind:"search", headerSummary:undefined, counts:undefined}for aWebSearchcall with querylinkcode release notes. KeeptoolCallSearchQueryas the fallback summary (or append counts to it), while retaining the dedicated ToolSearch branch that hidesselect:queries; add coverage for an output-less generic search and a counted search. -
packages/host/agent-adapter/src/native/codex/adapter.ts:1287-1299embeds the raw Codex MCP server name inmcp__<server>__<tool>, butmcpToolNamesplits on the first__. LinkCode accepts arbitrary non-empty custom MCP names, and the pinned Codex CLI acceptsrepo__prod(verified with an isolatedCODEX_HOME:codex mcp add repo__prod -- /bin/trueandcodex mcp list --jsonboth succeeded). A call from serverrepo__prod, toolsearch_filesis emitted asmcp__repo__prod__search_filesand rendered as serverrepo, toolprod__search_files, defeating the stated identity-preservation goal. Preserve a non-slug fallback for delimiter-containing Codex names (live and history), or introduce an unambiguous representation; add live/history parser cases for such a valid name.

Summary
Search-type tool calls get their own chat presentation (Closes CODE-412):
history-tools.ts) — normalize to the sharedmcp__<server>__<tool>slug so server identity survives into these rows.commit.gpgsign=falseso integration tests pass on signing machines.Verification
pnpm check:ci(0 errors) and fullpnpm exec vitest run(2741 passed) at this tip.tool-search.test.tsx,tool-call-metadata.test.tsx,tool-result-content.test.ts,codex-mcp-tools.test.ts,codex-history.test.ts.dev:mockconversation showcase.Checklist
pnpm check:ciandpnpm testboth pass (pluscargo fmt/clippy/testfor Rust changes)WIRE_PROTOCOL_VERSIONis bumped (no wire change in this PR)