You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Make MCP tool calls in agent chat legible: an expandable row carrying arguments, result/error, and a real terminal status — and for pickforge-lanes calls, render the pi-kit lane cards inline instead of a bare row.
Why
An MCP call currently renders as one dead grey line — MCP pickforge-lanes / lanes_wait — with no arguments, no result, no completion, and a disabled chevron. When an agent dispatches lanes and waits, the chat shows nothing about which lanes exist or how they are doing, while the Settings pi-kit panel has that exact information a screen away.
Verified against main at 9b852be. Not a stale build.
Where it breaks (three independent layers)
1. Rust parser drops args and results (Claude).crates/pickforge-core/src/agents/claude_stream.rs:273-281 special-cases the mcp__server__tool shape (claude_stream.rs:979-981) and emits AgentEvent::McpToolCall { status: InProgress, detail: None } — the non-MCP branch right below it (claude_stream.rs:283-287) does attach compact_input_summary(&input), so the MCP branch deliberately drops the summary its sibling keeps. And map_tool_result (claude_stream.rs:333-336) returns None unless the tool was Bash, so no MCP result is ever parsed and no completion event is ever emitted — the row never resolves. Codex app-server is the only provider that captures a result today (codex_app.rs:1059-1069, detail: string_field(item, &["error", "result"])); codex_exec.rs:373-379 also sends detail: None.
2. Frontend model drops what does arrive. The wire event carries status and detail (src/lib/agentChat.ts:73-79), but the timeline item type does not — src/stores/agentChat.ts:87 is { type: "mcpToolCall"; seq; itemId; server; tool }, no status, no detail, unlike the sibling toolUse on line 86. The reducer (agentChat.ts:976-986) appends and discards both, and has no itemId match/update path (contrast reduceToolUse, agentChat.ts:794-818) — so a completion event would append a duplicate row rather than update the existing one. Fixing layer 1 without this produces double rows.
3. The card is starved, not missing.McpCard.tsx:6-49 already implements the chevron/expand/<pre> body and accepts detail?: string | null. ChatTimeline.tsx:181-182 never passes it:
With detail undefined, canExpand() is false → spacer instead of chevron, disabled button, no body. Exactly the observed symptom.
Note plans/002-agent-chat-gui.md:43 always intended McpToolCall to map to tool_use mcp__* + result for the Claude column. That half was never implemented.
Scope
A. Generic MCP row (all servers)
Emit an arg summary and a completion event with status + result/error from claude_stream.rs; make codex_exec match.
Carry status and detail on the timeline item; add the itemId update path so completion mutates the row instead of appending.
Pass detail through ChatTimeline.tsx, hoist expansion state into RowExpansion (same contract as CommandCard.tsx:22-27 / ThinkingBubble.tsx:5-38) so it survives virtualization.
Update the virtualizer height estimate — chatTimelineVirtual.ts:90-92 gives mcpToolCall a flat 68 while toolUse measures its detail.
Truncation policy for large MCP payloads (they can be far bigger than a Bash output_tail) — cap in Rust, not in the view, since these rows are persisted through manager.rs:2676-2688.
B. pickforge-lanes renders lane cards
For server === "pickforge-lanes" (lanes_spawn / lanes_wait / lanes_status), the expanded body should show the run's lanes the way Settings does — the PiKitLanesPanel idiom (src/components/pikit/PiKitLanesPanel.tsx, display helpers in pikitLaneDisplay.ts, store src/stores/pikitLanes.ts reading <run>.status.json).
Yes, this is possible: the panel is already a self-contained run/lane card set with its own store and polling, and it was itself modelled on SwarmRunCard from chat. The work is extraction + correlation, not new rendering:
extract the run/lane card out of PiKitLanesPanel so both Settings and chat render the same component (avoid a second divergent lane card);
correlate the MCP call to a run id — from the lanes_spawn result, or the run argument on lanes_wait/lanes_status;
decide polling ownership: startPiKitLanesPolling is currently panel-scoped (onMount/onCleanup); a chat row needs it live while the call is in flight and static afterwards.
Open question for A/B split: whether the chat body shows live lanes (polls, matches Settings) or a snapshot of the lane state at result time. Live is what makes lanes_wait useful; snapshot is honest for replayed history. Likely: live while in-progress, frozen snapshot once the call completes.
Flag
Behind a flag — this changes a persisted timeline row shape and adds polling into chat. Proposed name: mcpToolDetail. Nearest analogue is changesReview gating the richer card at ChatTimeline.tsx:108-110.
merged behind flag mcpToolDetail
tested on main (flag on)
enabled in vX.Y.Z
flag removed
PR plan
PR 1 — Rust: MCP arg summary + completion event with result/error, with a payload cap
Depends on: none
Touches: claude_stream.rs, codex_exec.rs, event.rs if the payload shape grows
Validation: Rust unit tests over recorded stream-json fixtures (in-progress → completed → failed)
Goal
Make MCP tool calls in agent chat legible: an expandable row carrying arguments, result/error, and a real terminal status — and for
pickforge-lanescalls, render the pi-kit lane cards inline instead of a bare row.Why
An MCP call currently renders as one dead grey line —
MCP pickforge-lanes / lanes_wait— with no arguments, no result, no completion, and a disabled chevron. When an agent dispatches lanes and waits, the chat shows nothing about which lanes exist or how they are doing, while the Settings pi-kit panel has that exact information a screen away.Verified against
mainat 9b852be. Not a stale build.Where it breaks (three independent layers)
1. Rust parser drops args and results (Claude).
crates/pickforge-core/src/agents/claude_stream.rs:273-281special-cases themcp__server__toolshape (claude_stream.rs:979-981) and emitsAgentEvent::McpToolCall { status: InProgress, detail: None }— the non-MCP branch right below it (claude_stream.rs:283-287) does attachcompact_input_summary(&input), so the MCP branch deliberately drops the summary its sibling keeps. Andmap_tool_result(claude_stream.rs:333-336) returnsNoneunless the tool wasBash, so no MCP result is ever parsed and no completion event is ever emitted — the row never resolves. Codex app-server is the only provider that captures a result today (codex_app.rs:1059-1069,detail: string_field(item, &["error", "result"]));codex_exec.rs:373-379also sendsdetail: None.2. Frontend model drops what does arrive. The wire event carries
statusanddetail(src/lib/agentChat.ts:73-79), but the timeline item type does not —src/stores/agentChat.ts:87is{ type: "mcpToolCall"; seq; itemId; server; tool }, nostatus, nodetail, unlike the siblingtoolUseon line 86. The reducer (agentChat.ts:976-986) appends and discards both, and has no itemId match/update path (contrastreduceToolUse,agentChat.ts:794-818) — so a completion event would append a duplicate row rather than update the existing one. Fixing layer 1 without this produces double rows.3. The card is starved, not missing.
McpCard.tsx:6-49already implements the chevron/expand/<pre>body and acceptsdetail?: string | null.ChatTimeline.tsx:181-182never passes it:With
detailundefined,canExpand()is false → spacer instead of chevron, disabled button, no body. Exactly the observed symptom.Note
plans/002-agent-chat-gui.md:43always intendedMcpToolCallto map totool_use mcp__* + resultfor the Claude column. That half was never implemented.Scope
A. Generic MCP row (all servers)
claude_stream.rs; makecodex_execmatch.statusanddetailon the timeline item; add the itemId update path so completion mutates the row instead of appending.detailthroughChatTimeline.tsx, hoist expansion state intoRowExpansion(same contract asCommandCard.tsx:22-27/ThinkingBubble.tsx:5-38) so it survives virtualization.chatTimelineVirtual.ts:90-92givesmcpToolCalla flat68whiletoolUsemeasures its detail.output_tail) — cap in Rust, not in the view, since these rows are persisted throughmanager.rs:2676-2688.B.
pickforge-lanesrenders lane cardsFor
server === "pickforge-lanes"(lanes_spawn/lanes_wait/lanes_status), the expanded body should show the run's lanes the way Settings does — thePiKitLanesPanelidiom (src/components/pikit/PiKitLanesPanel.tsx, display helpers inpikitLaneDisplay.ts, storesrc/stores/pikitLanes.tsreading<run>.status.json).Yes, this is possible: the panel is already a self-contained run/lane card set with its own store and polling, and it was itself modelled on
SwarmRunCardfrom chat. The work is extraction + correlation, not new rendering:PiKitLanesPanelso both Settings and chat render the same component (avoid a second divergent lane card);lanes_spawnresult, or therunargument onlanes_wait/lanes_status;startPiKitLanesPollingis currently panel-scoped (onMount/onCleanup); a chat row needs it live while the call is in flight and static afterwards.Open question for A/B split: whether the chat body shows live lanes (polls, matches Settings) or a snapshot of the lane state at result time. Live is what makes
lanes_waituseful; snapshot is honest for replayed history. Likely: live while in-progress, frozen snapshot once the call completes.Flag
Behind a flag — this changes a persisted timeline row shape and adds polling into chat. Proposed name:
mcpToolDetail. Nearest analogue ischangesReviewgating the richer card atChatTimeline.tsx:108-110.mcpToolDetailPR plan
claude_stream.rs,codex_exec.rs,event.rsif the payload shape growsstatus/detail, itemId update path, passdetailtoMcpCard, hoist expansion, fix height estimatestores/agentChat.ts,lib/agentChat.ts,ChatTimeline.tsx,McpCard.tsx,chatTimelineVirtual.tsbun run test:unitreducer tests (no duplicate row on completion); VRT of an expanded MCP rowpickforge-laneslane cards inline: extract the run/lane card, correlate run id, polling lifecyclecomponents/pikit/*,ChatTimeline.tsx,stores/pikitLanes.tslanes_waitrow with lanes; confirm Settings panel is unchangedTraceability
pickforge-lanescalls show lane cards matching SettingsOpen decisions
Refs #274