Skip to content

Claude agent host: Phase 8.5 — rich tool-call rendering#317184

Merged
TylerLeonhardt merged 4 commits into
mainfrom
tyleonha/claude-phase8.5-impl
May 18, 2026
Merged

Claude agent host: Phase 8.5 — rich tool-call rendering#317184
TylerLeonhardt merged 4 commits into
mainfrom
tyleonha/claude-phase8.5-impl

Conversation

@TylerLeonhardt
Copy link
Copy Markdown
Member

Phase 8.5 of the Claude agent host. Brings tool-call rendering parity with Copilot — rich invocation/past-tense messages, `_meta.toolKind` for workbench renderer dispatch (terminal/search/subagent), and the supporting `ClaudeToolCallRegistry` collaborator class.

Plan: `src/vs/platform/agentHost/node/claude/phase8.5-plan.md`.

Highlights

  • Rich messages per tool. `Bash` reads Running \`git status\` Ran \`git status\` ; `Read` reads `Reading README.md` → `Read README.md`; `Grep`/`Glob` surface the pattern; `Task`/`Agent` use the model-authored description.
  • `_meta.toolKind` stamped at the tool-open seam drives the workbench's terminal / search / subagent renderers. Single write, reducer carries it through every state transition (D6).
  • New `ClaudeToolCallRegistry` encapsulates per-session attribution + input accumulation + computed start-info, mirroring Copilot's stash on start, reuse on complete pattern.

Critical bug fixes bundled

  • Auto-allowed tools rendered empty. Claude SDK auto-allows tools without invoking `canUseTool`, so `sessionPermissions` never fired `SessionToolCallReady`. The state stayed in `Streaming` and the subsequent `SessionToolCallComplete` was dropped by the reducer. Fixed by emitting Ready at `content_block_stop` with the parsed input + `_meta`.
  • Live vs replay past-tense parity for inner subagent tools. `claudeSubagentSignals` now calls `registry.seedParsedInput()` for inner `tool_use` blocks (which arrive pre-parsed on synthesized assistant messages, not via `input_json_delta`). Without this, the live mapper fell back to `"{displayName} finished"` while replay rendered rich text — D6 violation.
  • `ClaudeAgent.onClientToolCallComplete` benign no-op. `AgentSideEffects` fires this hook for EVERY server-dispatched `SessionToolCallComplete` envelope — including normal SDK tool completions. The previous `throw new Error('TODO: Phase 10')` corrupted every tool flow. `setClientTools` still throws since Phase 10 (client/MCP tool registration) hasn't landed.

Tests

  • NEW `claudeToolCallRegistry.test.ts` — full lifecycle + `seedParsedInput` coverage.
  • Snapshot `claudeToolDisplay.test.ts` — every tool row × all helpers.
  • Mapper `claudeMapSessionEvents.test.ts` — Test 9.5 asserts the new `content_block_stop` Ready emission shape.
  • Subagent `claudeSubagentSignals.test.ts` — extended to assert rich past-tense for inner-tool completion (D6 parity).
  • Agent `claudeAgent.test.ts` — `onClientToolCallComplete` no-op regression guard.

Verification

Per the plan's E2E Step 8 (added in this PR): NEW Claude `[Local]` chat in the Agents window, sent "Run `git status` and then read README.md". Both tool widgets render in the expanded thinking block with command + output visible. Zero `[Claude]` warns in `agentHost.log`.

Scope

`src/vs/platform/agentHost/` only — no workbench / chat renderer changes. The pre-existing `finalizeToolInvocation` terminal branch in `stateToProgressAdapter.ts` already handles command + output rendering once `tc.toolInput` is set, which the new Ready emission now ensures.

…ndering)

- Mark Phases 5, 6, 7, 8 as DONE (implementations have shipped; just
  catching the headings up to reality).
- Insert Phase 8.5 — Rich tool-call rendering parity with Copilot.
  Today the Claude permission card for Bash reads 'Run shell command'
  with no command shown; Bash/Grep/Glob rows render in the generic
  renderer instead of the dedicated terminal/search renderers. This
  phase ports Copilot's getInvocationMessage / getPastTenseMessage /
  getToolKind / getShellLanguage / getToolInputString shape into
  claudeToolDisplay.ts and wires them through the permission, mapper,
  and replay paths.

Phase 6.5 (Fork) intentionally stays Deferred.
Adds phase8.5-plan.md alongside roadmap.md's Phase 8.5 section.

Synthesized from a 3-model council (GPT-5.5, Claude Opus 4.6,
GPT-5.3-Codex) and refined through a grill-with-docs session.

Locked decisions:
- D1: getClaudeToolKind is a TOOL_ROWS column (single source of truth).
- D2: add Agent row to TOOL_ROWS; delete SUBAGENT_TOOL_NAMES.
- D3: defensive Record<string, unknown> access; no per-tool exported
  types.
- D4: pastTenseMessage is success-aware (tool_result.is_error).
- D5: live mapper mirrors Copilot's stash-on-start/reuse-on-complete
  pattern, with state encapsulated in a new ClaudeToolCallRegistry
  class (replaces today's bare maps on ClaudeMapperState).
- D6: _meta single-write on Start; reducer carries to Complete; replay
  emits on its single terminal action (asymmetry by design).
- D7: MCP tools get toolKind: undefined.
- D8: one big per-tool snapshot table covering all 5 helpers.
- D9: getClaudeInvocationMessage('Task', ...) owns the Task description
  fallback; Phase 12's site reduces to a plain helper call.
- D10: _meta stays flat (no per-kind namespacing).

Steps cover claudeToolDisplay (helpers + columns), claudeCanUseTool
(permission card rich invocation + _meta), sessionPermissions (forward
_meta through pending->ready), claudeMapSessionEvents (registry
migration + _meta on Start + success-aware past tense),
claudeReplayMapper (parity), claudeSubagentSignals (inner tool
parity), and the snapshot + behavior tests.
Brings tool-call rendering parity with Copilot:

- Rich invocation/past-tense messages per tool (Bash 'Running `git status`'
  → 'Ran `git status`', Read 'Reading [README.md](...)' → 'Read [README.md](...)',
  Grep/Glob with patterns, file links for path-bearing tools, subagent
  descriptions for Task/Agent).
- `_meta.toolKind` ('terminal'/'search'/'subagent') stamped at the tool-open
  seam to drive the workbench's specialized renderers; reducer carries it
  forward through every state transition (D6 in plan).
- New `ClaudeToolCallRegistry` encapsulates per-session tool-call attribution
  + input accumulation + computed start-info, mirroring Copilot's
  'stash on start, reuse on complete' pattern.

Critical bug fix bundled in:

- Emit `SessionToolCallReady` at `content_block_stop` so auto-allowed
  tools (which the Claude SDK runs without invoking `canUseTool`) transition
  Streaming → Running and the subsequent `SessionToolCallComplete` is
  accepted by the reducer instead of dropped. Without this, every
  auto-allowed tool widget rendered empty after completion.

D6 parity fix for inner subagent tools:

- `claudeSubagentSignals` now calls `registry.seedParsedInput()` for inner
  tool_use blocks (which arrive pre-parsed on synthesized assistant
  messages rather than via input_json_delta), so the live tool_result
  handler emits rich past-tense text matching the replay path instead of
  falling back to '{displayName} finished'.

Also fixes a pre-existing Phase 10 stub:

- `ClaudeAgent.onClientToolCallComplete` is now a benign no-op. The
  AgentSideEffects autorun fires this hook for EVERY server-dispatched
  SessionToolCallComplete envelope (including normal SDK tool completions),
  so the previous `throw new Error('TODO: Phase 10')` corrupted every
  tool flow. Client (MCP) tool registration via `setClientTools` still
  throws since Phase 10 hasn't landed.

Tests:
- New: `claudeToolCallRegistry.test.ts` (lifecycle + seedParsedInput coverage).
- Snapshot: `claudeToolDisplay.test.ts` covers every tool row × all helpers.
- Mapper: `claudeMapSessionEvents.test.ts` Test 9.5 asserts the new
  content_block_stop Ready emission.
- Subagent: `claudeSubagentSignals.test.ts` extended to assert rich
  past-tense for inner-tool completion (D6 parity).
- Agent: `claudeAgent.test.ts` asserts onClientToolCallComplete is a no-op.

Verified live in the Agents window: NEW Claude [Local] chat with
'Run `git status` and then read README.md' produces both tool widgets
in the expanded thinking block with command + output visible.
Copilot AI review requested due to automatic review settings May 18, 2026 21:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements Phase 8.5 for the Claude agent host by bringing Claude tool-call rendering up to Copilot parity: rich invocation/past-tense messages, _meta.toolKind stamping for renderer dispatch (terminal/search/subagent), and a per-session ClaudeToolCallRegistry to accumulate streamed tool inputs and reuse computed display info at completion time.

Changes:

  • Add rich tool display helpers (invocationMessage, pastTenseMessage, toolInput formatting) and table-driven toolKind meta for Claude SDK tools.
  • Introduce ClaudeToolCallRegistry and wire it through live streaming + subagent + replay paths; emit SessionToolCallReady at content_block_stop to prevent auto-allowed tools from getting stuck in Streaming.
  • Expand/adjust tests with snapshots and focused lifecycle coverage; make onClientToolCallComplete a no-op (until Phase 10).
Show a summary per file
File Description
src/vs/platform/agentHost/node/claude/claudeToolDisplay.ts Adds rich invocation/past-tense/tool-input helpers and table-driven toolKind + _meta builder for Claude tools.
src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts New registry to accumulate input_json_delta and cache computed start info for later completion rendering.
src/vs/platform/agentHost/node/claude/claudeMapSessionEvents.ts Wires registry into live mapper; stamps _meta; emits Ready at content_block_stop; uses rich past-tense on complete.
src/vs/platform/agentHost/node/claude/claudeReplayMapper.ts Replay path now produces rich invocation/past-tense and reuses _meta via buildClaudeToolMeta.
src/vs/platform/agentHost/node/claude/claudeSubagentSignals.ts Ensures inner subagent tool calls are seeded for rich past-tense parity; stamps _meta on inner tool starts.
src/vs/platform/agentHost/node/claude/claudeCanUseTool.ts Permission path now uses rich invocation/toolInput and stamps _meta for tool-kind rendering.
src/vs/platform/agentHost/node/claude/claudeAgent.ts Makes onClientToolCallComplete a benign no-op to avoid breaking normal tool flows.
src/vs/platform/agentHost/node/sessionPermissions.ts Attempts to forward _meta through pending-confirmation → ready transition.
src/vs/platform/agentHost/test/node/claudeToolDisplay.test.ts Adds snapshot + defensive-input tests for all rich helper outputs and MCP/Agent cases.
src/vs/platform/agentHost/test/node/claudeToolCallRegistry.test.ts New unit tests covering registry lifecycle, malformed JSON, and seedParsedInput.
src/vs/platform/agentHost/test/node/claudeSubagentSignals.test.ts Extends assertions to validate rich past-tense for inner-tool completion (live/replay parity).
src/vs/platform/agentHost/test/node/claudeMapSessionEvents.test.ts Adds coverage ensuring Ready is emitted on content_block_stop for auto-allowed tools.
src/vs/platform/agentHost/test/node/claudeAgent.test.ts Adds regression test for onClientToolCallComplete no-op; updates expectations for rich invocation/toolInput formatting.
src/vs/platform/agentHost/node/claude/roadmap.md Marks prior phases done and documents Phase 8.5 scope/approach.
src/vs/platform/agentHost/node/claude/phase8.5-plan.md Adds detailed Phase 8.5 plan and implementation notes for traceability.

Copilot's findings

  • Files reviewed: 15/15 changed files
  • Comments generated: 4

Comment thread src/vs/platform/agentHost/node/claude/claudeCanUseTool.ts
Comment thread src/vs/platform/agentHost/node/sessionPermissions.ts
Comment thread src/vs/platform/agentHost/node/claude/claudeToolCallRegistry.ts
Comment thread src/vs/platform/agentHost/node/claude/claudeToolDisplay.ts
- claudeCanUseTool: drop the redundant '?? JSON.stringify(input)' fallback.
  getClaudeToolInputString already wraps stringify in try/catch and
  returns undefined on failure; the outer call was re-running the same
  stringify that just failed (would throw on non-serializable input).

- sessionPermissions.createToolReadyAction: drop the state._meta
  forwarding. The reducer's SessionToolCallReady branch derives _meta
  via tcBase(tc) from the prior state, so the action-level _meta was
  never read.

- claudeToolCallRegistry.finalize: preserve the raw inputBuffer as
  toolInput when JSON.parse fails or yields a non-object. Without this,
  malformed payloads rendered an empty input section in the UI.
  Test updated to assert raw-buffer preservation.

- claudeToolDisplay.formatPathAsMarkdownLink + WebFetch link: escape
  the link label via escapeMarkdownLinkLabel. File names containing
  ']' or '\\' would otherwise break out of the [...] label and could
  cause malformed rendering / injection.

- claudeAgent.integrationTest (Phase 7 §5.3 Read tool round-trip):
  expected signal sequence updated for the Phase 8.5 mapper's new
  SessionToolCallReady emission at content_block_stop. The mapper-side
  Ready (auto-allow path) now lands between toolCallDelta and the
  permission card's pending_confirmation.
@TylerLeonhardt TylerLeonhardt marked this pull request as ready for review May 18, 2026 22:41
@TylerLeonhardt TylerLeonhardt enabled auto-merge (squash) May 18, 2026 22:41
@TylerLeonhardt TylerLeonhardt merged commit 1e4d8bd into main May 18, 2026
25 checks passed
@TylerLeonhardt TylerLeonhardt deleted the tyleonha/claude-phase8.5-impl branch May 18, 2026 23:19
@vs-code-engineering vs-code-engineering Bot added this to the 1.122.0 milestone May 18, 2026
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