Skip to content
This repository was archived by the owner on Sep 4, 2026. It is now read-only.

feat(chat): make MCP and tool rows legible end to end (parser + UI, flag-gated) - #387

Merged
ElbertePlinio merged 3 commits into
mainfrom
feat-parser-tool-detail
Jul 26, 2026
Merged

feat(chat): make MCP and tool rows legible end to end (parser + UI, flag-gated)#387
ElbertePlinio merged 3 commits into
mainfrom
feat-parser-tool-detail

Conversation

@ElbertePlinio

@ElbertePlinio ElbertePlinio commented Jul 26, 2026

Copy link
Copy Markdown
Member

The backend and frontend halves of #362 and #365, consolidated. #390 merged into this branch, so it ships as one gated unit rather than leaving the parser emitting events nothing renders.

Nothing non-Bash ever completed

if self.tools.get(item_id) != Some(&RememberedTool::Bash) {
    return None;
}

Every other tool was emitted InProgress and never updated — permanently "working" long after it finished.

Lifting that guard alone would not have helped: RememberedTool was { Bash, Other }, discarding the name, so there was nothing to route a completion to. It now retains identity (Bash, Mcp { server, tool }, Other(name)). Both issues named this type as the thing whichever PR landed first must own.

MCP rows were starved on purpose

The MCP branch emitted detail: None while the non-MCP branch immediately below it attached compact_input_summary(&input). That asymmetry is why an MCP call rendered as one dead grey line with a disabled chevron.

The generic row starved on a four-key allowlist

file_path, notebook_path, pattern, description. AskUserQuestion, Task, Skill, Glob, ToolSearch and Read-with-offset carry none of them. Known keys still win; anything else falls back to a capped rendering of the whole input.

The frontend threw away what did arrive

lib/agentChat.ts has always declared status and detail on both events. The timeline item type did not, so the reducer read them and dropped them, and ChatTimeline rendered <McpCard server tool /> with no detail — which is precisely why canExpand() was false.

Now: both item types carry status and detail (optional, so rows persisted before this still render); a completion carrying no result keeps the arg summary already on the row; expansion moves into the hoisted RowExpansion map so the toggle survives scroll-back; status renders through StatusPill, not new chrome.

reduceMcpToolCall gained the itemId match path — it appended unconditionally, unlike its reduceToolUse sibling. Without it these new completions turn "the row never finishes" into "the row finishes twice". That is why these commits cannot be separated.

Capping

In Rust, not the view, because rows are persisted. Output tails keep the 2000-char tail; argument summaries take the head at 300 — the meaningful part of an argument list is at its front, and it rides on one collapsed row.

Flag

mcpToolDetail, default off. A persisted row's shape changed and the parser only emits completions on newer builds, so main stays releasable while this is dark — the same precedent as changesReview. flags.test.ts enumerates the registry, so the flag is asserted rather than silently appearing.

Tests

Every one verified load-bearing.

Rust (8): MCP arg summary + InProgress → Completed; failed MCP resolves Failed; a generic tool completes under its own name; Bash still maps to CommandDone; an orphan tool_result still ignored; summary falls back to whole input, prefers a known key, is capped, and yields None on empty. Restoring the Bash-only guard fails exactly the three completion tests.

Frontend (5): MCP status/detail carried and resolving in place; a detail: null completion keeping the arg summary; a generic tool reaching failed; no duplicate row; distinct calls still appending separately.

Validation

  • cargo test -p pickforge-core — 738 passed
  • cargo clippy -p pickforge-core --all-targets -- -D warnings — clean
  • bun run test:unit1606 passed
  • bunx tsc --noEmit clean; bun run lint clean

Still open on these two issues

Stated rather than implied — #365's headline complaint is not fixed here:

Refs #362, #365

@ElbertePlinio
ElbertePlinio force-pushed the feat-parser-tool-detail branch from a48e624 to 8eada38 Compare July 26, 2026 01:43
@ElbertePlinio
ElbertePlinio force-pushed the feat-parser-tool-detail branch 2 times, most recently from dcd64df to 53825a9 Compare July 26, 2026 01:57
@ElbertePlinio
ElbertePlinio force-pushed the feat-parser-tool-detail branch from 53825a9 to 866360d Compare July 26, 2026 02:04
@ElbertePlinio ElbertePlinio changed the title fix(claude): let every tool row carry its args and reach a terminal status feat(chat): make MCP and tool rows legible end to end (parser + UI, flag-gated) Jul 26, 2026
…tatus

Shared parser slice for #362 and #365 — both issues named `claude_stream.rs`'s
`RememberedTool` and the Bash-only guard in `map_tool_result` as one surface to
serialize rather than duplicate, so this does that half once.

**Nothing non-Bash ever completed.** `map_tool_result` returned `None` unless
the tool was Bash, so every other tool was emitted `InProgress` and never
updated — permanently "running" long after it finished. Lifting the guard alone
would not have helped: `RememberedTool` collapsed everything non-Bash to a bare
`Other`, discarding the name, so there was nothing left to route a completion
to. It now retains the tool's identity — Bash, an MCP server/tool pair, or a
named tool — and `map_tool_result` dispatches on it.

**MCP rows were starved on purpose.** The MCP branch emitted `detail: None`
while the non-MCP branch immediately below it attached
`compact_input_summary(&input)`. That is why an MCP call rendered as one dead
grey line: no args, no result, no completion, disabled chevron.

**The generic row starved on a four-key allowlist.** `file_path`,
`notebook_path`, `pattern`, `description` — `AskUserQuestion`, `Task`, `Skill`,
`Glob`, `ToolSearch` and `Read`-with-offset carry none of them, so `detail` was
`None` and the chevron was a dead button. Known keys still win (they are the
human-meaningful ones); anything else falls back to a compact rendering of the
whole input.

Both payloads are capped in Rust, not the view, because these rows are
persisted. Argument summaries take the FRONT of the string — the meaningful part
of an argument list is at its head, unlike a command's output tail — and are
capped far shorter, since they ride on a single collapsed row.

Deliberately NOT in this slice, so the two follow-on PRs stay reviewable:
`tool_progress` elapsed time and `system`/`status` compacting/requesting (#365
B1), which add a new event variant, and the frontend half of both issues — the
timeline item's `status`/`detail` fields, the itemId update path, and the card
wiring.

Refs #362, #365
The parser half of this change starts emitting completion events for MCP
calls. `reduceMcpToolCall` appended unconditionally — it had no itemId match
path, unlike its `reduceToolUse` sibling — so a resolving call would visibly
double instead of updating.

That makes the two halves inseparable: shipping the parser without this turns
'the row never finishes' into 'the row finishes twice'.

Refs #362
…ggle (#390)

Frontend half of #362 and #365, behind the new default-off `mcpToolDetail`
flag. Stacked on the parser slice, which is what makes these events exist.

The wire event has always carried `status` and `detail` — `lib/agentChat.ts`
declares both on `mcpToolCall` and `toolUse`. The timeline item type did not, so
the reducer read them and threw them away, and `ChatTimeline` rendered
`<McpCard server tool />` with no `detail` at all. With `detail` undefined
`canExpand()` is false, which is exactly the reported symptom: a disabled
chevron on a dead grey row.

Both item types now carry `status` and `detail`, both optional so a row
persisted before the parser learned to resolve non-Bash tools still renders.
A completion that carries no result keeps the arg summary already on the row —
resolving must never blank what the row was showing.

Expansion moves into the hoisted `RowExpansion` map, the same contract
`CommandCard` and `ThinkingBubble` use, so the toggle survives the virtualizer
disposing the row on scroll-back. That was listed in #372 as a separate
annoyance; it comes free here because these cards had to take `open`/`onToggle`
anyway.

Status renders through `StatusPill`, matching `CommandCard` rather than adding
new chrome — bracket-cornered mono, never a filled chip.

The two new render branches are extracted out of `renderItem` because adding
them inline pushed it past the complexity cap. `flags.test.ts` enumerates the
registry, so the new flag is asserted there rather than silently appearing.

Refs #362, #365
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant