Claude agent host: Phase 8.5 — rich tool-call rendering#317184
Merged
Conversation
…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.
Contributor
There was a problem hiding this comment.
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,toolInputformatting) and table-driventoolKindmeta for Claude SDK tools. - Introduce
ClaudeToolCallRegistryand wire it through live streaming + subagent + replay paths; emitSessionToolCallReadyatcontent_block_stopto prevent auto-allowed tools from getting stuck inStreaming. - Expand/adjust tests with snapshots and focused lifecycle coverage; make
onClientToolCallCompletea 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
- 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.
osortega
approved these changes
May 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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.Critical bug fixes bundled
Tests
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.