Skip to content

feat(ui): dedicated chat components for tool search/select/query - #430

Open
Zerlight wants to merge 12 commits into
masterfrom
ruocheng/code-412
Open

feat(ui): dedicated chat components for tool search/select/query#430
Zerlight wants to merge 12 commits into
masterfrom
ruocheng/code-412

Conversation

@Zerlight

@Zerlight Zerlight commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Search-type tool calls get their own chat presentation (Closes CODE-412):

  • Search rows (Grep/Glob and agent-native search tools) render a humanized query + scope instead of raw JSON, with expressive per-kind icons.
  • ToolSearch renders its result rows inline; select-style calls keep neutral wording when no result rows exist.
  • Search context is preserved across pending/running/failed states.
  • Codex MCP tool titles — live app-server events and rollout replay (history-tools.ts) — normalize to the shared mcp__<server>__<tool> slug so server identity survives into these rows.
  • Engine git fixtures set commit.gpgsign=false so integration tests pass on signing machines.

Verification

  • pnpm check:ci (0 errors) and full pnpm exec vitest run (2741 passed) at this tip.
  • New unit coverage: tool-search.test.tsx, tool-call-metadata.test.tsx, tool-result-content.test.ts, codex-mcp-tools.test.ts, codex-history.test.ts.
  • Mock showcase extended with the new search/ToolSearch cases — renderable end-to-end via the webview dev:mock conversation showcase.

Checklist

  • pnpm check:ci and pnpm test both pass (plus cargo fmt / clippy / test for Rust changes)
  • I ran the affected surface and observed the change working
  • If a wire message changed: WIRE_PROTOCOL_VERSION is bumped (no wire change in this PR)
  • New code and assets are my own work, or their origin and license compatibility are noted above
  • Docs and comments are updated where behavior changed

Copilot AI lite review requested due to automatic review settings August 8, 2026 02:35
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CODE-412

Copilot AI 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.

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.

Comment thread packages/host/agent-adapter/src/native/codex/history-tools.ts Outdated
Comment thread packages/host/agent-adapter/src/native/codex/adapter.ts Outdated
Comment thread packages/presentation/ui/src/chat/tool-search.tsx Outdated

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

[!IMPORTANT]

The new ToolSearch rendering is solid, but the toolCallHeaderSummary search-case deletion strips header context from search tools that don't emit match counts — WebSearch collapses to a bare WebSearch label. Verified by rendering through ToolCallItem.

Reviewed changes

This PR adds dedicated presentation for Claude Code's deferred-tool ToolSearch calls and normalizes Codex MCP tool titles.

  • New ToolSearchResult component (packages/presentation/ui/src/chat/tool-search.tsx) plus toolSearchPresentation() in tool-result-content.ts, splitting select: mode from keyword mode and rendering mcp__server__tool as tool (server).
  • tool-call-item.tsx grows a header branch computing toolSearch / searchCounts and swaps in ToolCaseIcon; search-kind rows now suppress generic metadata.
  • tool-utils.ts adds toolCallSearchCounts() and changes toolCallHeaderSummary's case 'search' from returning the query to break; SearchRows drops its result-count badge.
  • Codex adapter titles change from ${server}.${tool} to mcp__${server}__${tool}, with a new codexMcpToolName() handling the rollout-JSONL replay path.
  • i18n keys added to both zh-cn.ts and en.ts with 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread packages/presentation/ui/src/tool-utils.ts Outdated

@lucas77778 lucas77778 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking findings:

  1. packages/presentation/ui/src/tool-utils.ts:203-206 removes the query from every generic search header, while ToolCallItem only substitutes counts when rawOutput has a supported count envelope. WebSearch is classified as kind: search by toolKindFromName, and Pi/OpenCode-style searches also commonly have no numMatches/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 a WebSearch call with query linkcode release notes. Keep toolCallSearchQuery as the fallback summary (or append counts to it), while retaining the dedicated ToolSearch branch that hides select: queries; add coverage for an output-less generic search and a counted search.

  2. packages/host/agent-adapter/src/native/codex/adapter.ts:1287-1299 embeds the raw Codex MCP server name in mcp__<server>__<tool>, but mcpToolName splits on the first __. LinkCode accepts arbitrary non-empty custom MCP names, and the pinned Codex CLI accepts repo__prod (verified with an isolated CODEX_HOME: codex mcp add repo__prod -- /bin/true and codex mcp list --json both succeeded). A call from server repo__prod, tool search_files is emitted as mcp__repo__prod__search_files and rendered as server repo, tool prod__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.

Copilot AI review requested due to automatic review settings August 9, 2026 11:37
@Zerlight
Zerlight force-pushed the ruocheng/code-412 branch from e072cb6 to c4fa4a1 Compare August 9, 2026 11:37

Copilot AI 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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

@Zerlight

Zerlight commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Both addressed:

  1. 9a3fcb9 keeps toolCallSearchQuery as the header fallback whenever toolCallSearchCounts resolves nothing, so WebSearch-style settles and in-progress searches keep their query while counted settles still humanize. The dedicated ToolSearch branch still overrides the summary, so select: strings never surface. Coverage added for an uncounted settle and the in-progress case, alongside the existing counted-search test.

  2. c4fa4a1 routes the live adapter and the rollout replay through one codexMcpSlug() (tool-view.ts): a server name containing __ keeps codex's raw dotted server.tool title instead of emitting a slug that would mis-split in mcpToolName. Live + history parser cases added for repo__prod.

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

ℹ️ No new issues — one unused-export nit inline.

Reviewed changes

Three commits since the last pullfrog review at e072cb6, all of them fixes to round-one feedback. Both the header-summary regression and the codex slug mis-split are addressed, and each fix ships a test that fails without it.

  • Restored the search query as the header fallbacktoolCallHeaderSummary's case 'search' now assigns toolCallSearchQuery(toolCall) whenever toolCallSearchCounts comes back empty, so counted settles still humanize to counts while WebSearch, Pi/opencode-shaped outputs, and in-progress searches keep their query.
  • Extracted codexMcpSlug() into tool-view.ts — the live mcpToolCall case and the rollout-replay path now share one slug builder instead of two copies of the template literal, and CODEX_PLUGIN_APPS_SERVER moved along with it.
  • Kept codex's raw dotted title for __-bearing server namescodexMcpSlug returns ${server}.${tool} when the server would mis-split, matching mcpToolName's first-__ split rule exactly.
  • Added parser coverage for both pathsrepo__prod cases in codex-mcp-tools.test.ts (live) and codex-history.test.ts (replay), plus a WebSearch header test and an updated toolCallHeaderSummary expectation.

I traced live-vs-replay title convergence input by input rather than trusting the tests: ordinary servers, the codex_apps dotted unwrap, the stray-trailing-__ namespace form, __-bearing server names, and codex_apps combined with a __-bearing app name all produce identical titles from both paths. The five touched test files pass locally, 73/73.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread packages/host/agent-adapter/src/native/codex/tool-view.ts Outdated
Copilot AI review requested due to automatic review settings August 9, 2026 11:57

Copilot AI 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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/presentation/ui/src/chat/tool-search.tsx:15

  • When ToolSearch settles with prose (e.g., multi-line error details), rendering it in a plain

    collapses newlines/whitespace, so the message can become hard to read or misleading. Preserve line breaks (similar to how other tool text is rendered) by using whitespace-pre-wrap (and optionally break-words).

  if (names.length === 0) {
    return message ? <p className="text-muted-foreground text-sm">{message}</p> : null;
  }

@pullfrog

pullfrog Bot commented Aug 11, 2026

Copy link
Copy Markdown

Your Claude subscription has hit its usage limit. It resets at 3:10am (UTC). Re-trigger Pullfrog after the reset, or add an ANTHROPIC_API_KEY repo secret — Pullfrog routes around an exhausted subscription automatically when one is present.

Add repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

Copilot AI 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.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/presentation/ui/src/tool-utils.ts:240

  • hasToolBody() treats ToolSearch calls as body-less unless rawOutput.message is set. But ToolSearch failures (and even successful tool lists) are carried in content/toolSearchPresentation().message|names, so the header becomes non-expandable and the ToolSearchResult/message can’t be shown from ToolCallItem.
  if (toolSearchPresentation(toolCall)) {
    return toolCallFailureMessage(toolCall) !== undefined;
  }
  return toolCallMetadata(toolCall).length > 0 || toolCallFailureMessage(toolCall) !== undefined;

@pullfrog

pullfrog Bot commented Aug 11, 2026

Copy link
Copy Markdown

Your Claude subscription has hit its usage limit. It resets at 3:10am (UTC). Re-trigger Pullfrog after the reset, or add an ANTHROPIC_API_KEY repo secret — Pullfrog routes around an exhausted subscription automatically when one is present.

Add repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@lucas77778 lucas77778 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Latest-head review found one blocking history-identity issue; the UI acceptance path itself passes.

  1. Blocking — Codex MCP replay reconstructs raw identity from lossy callable names. codexMcpToolName() treats rollout function_call.namespace/name as the original server/tool and strips a trailing __ (packages/host/agent-adapter/src/native/codex/history-tools.ts:141-152). In Codex 0.144.1 those fields are model-visible callable identifiers: server/tool characters may be sanitized, collisions gain hashes, and the pair may be truncated to the 64-character Responses API limit. A valid custom server named acme__ is a concrete collision: live renders acme__.tool, while replay reads mcp__acme__, strips the suffix, and renders mcp__acme__tool, changing the server to acme.

Codex already persists an authoritative completed mcpToolCall item (paginated history) or legacy McpToolCallEnd, keyed by the same call ID and carrying raw server/tool. Please pre-index those records and use them for replay; keep function_call.namespace/name only as an explicitly lossy fallback without stripping trailing delimiters. Add cases for a trailing-__ server plus sanitized/truncated callable names.

  1. Non-blocking — multiline ToolSearch prose loses line breaks. packages/presentation/ui/src/chat/tool-search.tsx:13-14 renders the preserved message in a normal <p>, collapsing newlines from multi-line failures/notices. Add whitespace-pre-wrap break-words and a two-line component test.

Verified on this head:

  • The prior hasToolBody() concern is a false alarm: normalized result content is checked first, so successful and content-backed failed ToolSearch rows remain expandable.
  • Generic uncounted searches retain the query fallback.
  • Focused suite passes: 6 files / 78 tests.
  • Mock UI acceptance passes for counted search, selected-tool identities, and zero-match messaging.

Copilot AI review requested due to automatic review settings August 12, 2026 13:13
@Zerlight

Copy link
Copy Markdown
Member Author

Resolved both findings from review 4905679467:

  1. Codex MCP replay now pre-indexes the authoritative raw server/tool identity from durable legacy mcp_tool_call_end and paginated item_completed / McpToolCall records before mapping. The function_call namespace/name path remains only a lossy fallback, and trailing __ is no longer stripped. Regression coverage includes a trailing-__ server plus sanitized/truncated callable identifiers. The follow-up in 6c83b89c also preserves legacy codex_apps replay compatibility without overriding modern dotted raw identities.

  2. Multiline ToolSearch prose now renders with whitespace-pre-wrap break-words, with a two-line component regression.

Implemented in 12ad5b6d and 6c83b89c. After rebasing the full stack, the focused suite passes 79/79, pnpm check:ci passes, and the full Vitest suite passes with --maxWorkers=2 --maxConcurrency=2 (335 files / 2,889 tests passed; 1 file / 1 test skipped).

Copilot AI 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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/presentation/ui/src/chat/tool-result-content.ts:89

  • toolSearchPresentation() splits on "\n" without trimming, so CRLF output ("\r\n") will leave a trailing "\r" on each line and fail TOOL_NAME_LINE_RE, causing ToolSearch name-per-line settles to be treated as prose messages.
  const text = toolCallDisplayText(toolCall);
  const lines = [...new Set(text.split('\n').filter((line) => line.length > 0))];

@Zerlight
Zerlight requested a review from lucas77778 August 12, 2026 13:26

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

ℹ️ No blocking issues — one dead-field nit inline.

Reviewed changes

Both of the round-three findings are fixed, and each ships a test that fails without it.

  • Pre-indexed codex's authoritative MCP identitycollectCodexMcpIdentities() (history.ts:557) walks the rollout once, keying call_id → raw {server, tool} from item_completed/McpToolCall items and legacy mcp_tool_call_end.invocation. codexReplayMcpIdentity() now returns persisted ?? callable, so the lossy callable is a genuine fallback rather than the primary source.
  • Removed the trailing-__ strip from codexMcpToolName, exactly as requested — mcp__acme__/lookup now replays as acme__.lookup, matching the live path instead of silently renaming the server to acme.
  • Kept the one legacy Codex Apps case recoverable — a persisted codex_apps/github_create_issue pair defers to the callable only when the namespace, the leading _, and the exact ${server}_${tool} reconstruction all agree; anything else keeps the persisted identity verbatim.
  • Recovered Claude announce snapshots across cursor pagesClaudeTranscriptSupplement gains toolUses, harvested from raw assistant rows, and the settle path falls back to it (announced.get(id) ?? toolUses?.get(id)) when a page begins at a tool_result whose tool_use lived on the previous page. claudeToolCallFromUse() extracts the shared announce shape so both paths cannot drift.
  • Preserved multiline ToolSearch prosewhitespace-pre-wrap break-words on the message paragraph, with a two-line component test asserting both the exact textContent and the classes.

On the 'McpToolCall' PascalCase check — verified correct, noting it so it isn't re-litigated

history.ts:567 tests 'McpToolCall' while the live adapter's item type is 'mcpToolCall', and PascalCase appears nowhere else in the repo except this code and its own fixtures — the shape of an assumption a test was written to agree with. It isn't. There are two codex protocols with different serde: the app-server ThreadItem is internally tagged camelCase (confirmed via codex app-server generate-json-schema), while the rollout's event_msg/item_completed payload carries TurnItem, an internally tagged enum whose variants serialize PascalCase — confirmed by dumping the vendored 0.144.1 binary, which yields …FileChangeMcpToolCallContextCompaction immediately adjacent to internally tagged enum TurnItem. I also checked that McpToolCallEndEvent really nests invocation { server, tool } and that McpToolCallItem exposes single-word id/server/tool, so both read paths are casing-safe.

Two more things I traced rather than trusting the tests: collectCodexMcpIdentities is order-independent by construction (item_completed sets unconditionally, mcp_tool_call_end is guarded by !has, so the authoritative completed item always wins whichever row lands first), and all three Codex Apps branches produce the expected titles by hand — legacy match, server/app mismatch, and modern dotted.

The six touched test files pass locally, 95/95.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

}

function harvestToolUses(toolUses: Map<string, ToolCall>, row: Record<string, unknown>): void {
const parentToolCallId = stringField(row, 'parent_tool_use_id');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

stringField(row, 'parent_tool_use_id') reads a snake_case key off a raw transcript row, but raw rows are camelCase — every sibling read in this function uses isMeta / isSidechain / teamName / parentUuid. snake_case is the SDK's SessionMessage projection shape, not the on-disk one.

I checked a real transcript rather than reasoning about it: zero rows in a ~/.claude/projects/<…>.jsonl carry a top-level parent_tool_use_id. The CLI's own name for the field is parentToolUseID, and its disk→stream projection hard-codes parent_tool_use_id: null — as does the rows.push projection 23 lines up at claude-code.ts:1825. So parentToolCallId here is always undefined in production.

It's harmless: the settle path degrades to parent ?? existing?.parentToolCallId, i.e. exactly the pre-PR behavior. It may even be unreachable by construction — sidechain rows, the only ones that would carry a parent tool id, are excluded on the line directly above. The reason it's worth a line: the new tests fabricate the key (claude-code-compaction.test.ts:473 and :535) and then assert parentToolCallId on the recovered ToolCall, so that one field is validated against a shape the adapter never actually sees.

Either drop the argument and let the settle fall back on its own, or read the camelCase key the transcript actually writes — but the fabricated field should come out of the fixtures either way, since the rest of both tests (the announce-snapshot recovery they exist to prove) is genuinely load-bearing.

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.

3 participants