fix(chat): inline, persistent subagent cards (F5 close-out + duplicate-card fix)#711
Conversation
…o spawning task call)
…per-message mount Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted; duplicate-mount defect fixed)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude encountered an error after 4m 17s —— View job PR Review in Progress
|
|
Claude encountered an error after 2m 18s —— View job PR Review in Progress
|
| @@ -54,7 +57,9 @@ interface Group { | |||
| `], | |||
| template: ` | |||
| @for (group of groups(); track $index) { | |||
There was a problem hiding this comment.
Pre-existing but now more impactful: track $index can desync expansion state when subagent groups arrive mid-list.
Before this PR, subagent cards lived in a separate <chat-subagents> component and never touched the groups() array. Now they do. When subagents arrive one-by-one (common during streaming), the subagent group replaces the generic chip in-place at the same index — so same-named adjacent calls that were previously merged into one group may split into N groups, shifting all subsequent group indices. Any user-expanded group below the split point will appear collapsed.
In practice this only bites when multiple same-name task calls arrive and subagents populate one at a time. Tracking by a stable key would eliminate the risk:
| @for (group of groups(); track $index) { | |
| @for (group of groups(); track group.calls[0].id) { |
|
Claude finished @blove's task in 3m 8s —— View job PR Review — fix(chat): inline, persistent subagent cards
SummaryThe fix is correct and the design is clean. Moving the subagent card rendering into Issues1. Index-keyed expansion state (pre-existing, made more visible here) — flagged in two related inline comments:
2. Looks good
|
| <chat-subagent-card [subagent]="group.subagent" /> | ||
| } @else if (group.calls.length > 1 && !group.templateRef) { | ||
| <!-- Default grouped strip --> | ||
| @let expanded = expandedGroups().has($index); |
There was a problem hiding this comment.
Deeper side of the track $index issue: expandedGroups is also index-keyed.
The existing review flags the track $index risk for DOM reconciliation; there's a complementary issue here. Even after adopting track group.calls[0].id, this line resolves expanded state by the current render index. When groups() recomputes mid-stream (e.g. a subagent card inserts at index 0, shifting everything below it), a group the user already expanded at index 1 now appears at index 2 — but expandedGroups still holds {1}, so it renders collapsed.
A complete fix pairs the tracking change with an ID-keyed expansion map:
| @let expanded = expandedGroups().has($index); | |
| @let expanded = expandedGroups().has(group.calls[0].id); |
…and in the component class, change the signal + toggle to use the call ID:
private readonly _expandedGroups = signal(new Set<string>());
toggleGroup(groupId: string): void {
this._expandedGroups.update(prev => {
const next = new Set(prev);
next.has(groupId) ? next.delete(groupId) : next.add(groupId);
return next;
});
}The button's (click) binding would pass group.calls[0].id instead of $index. This is a pre-existing issue that this PR makes more visible — happy to treat it as a follow-up if preferred.
| @@ -119,14 +124,23 @@ export class ChatToolCallsComponent { | |||
| readonly groups = computed((): Group[] => { | |||
| const excludeSet = new Set(this.excludeToolNames()); | |||
| const calls = this.toolCalls().filter(tc => !excludeSet.has(tc.name)); | |||
There was a problem hiding this comment.
excludeToolNames silently suppresses subagent cards.
The excludeSet filter runs before the subagent check. If a consumer passes excludeToolNames: ['task'], the spawning task call is removed from calls before we ever look it up in subs, so no chat-subagent-card renders. The subagent is silently swallowed — no chip, no card.
This may be intentional (opt-out of all task rendering), but it's a footgun: the name excludeToolNames reads like "suppress the generic chip", not "also suppress the subagent card". Worth either:
- Documenting this interaction on the
excludeToolNamesJSDoc, or - Checking
subsbefore filtering, so the subagent card is always rendered regardless of exclusion.
… for recent PRs (#719) Documents user-facing surfaces landed since 0.0.49 that had no narrative docs (API reference is bot-generated; these are the hand-written guides): - chat-subagent-card: correct stale "latest message" claim — the card now renders the full subagent transcript (streaming markdown + reasoning + tool-call cards) on <chat-trace>, auto-expanding while running (#692, #711) - changelog: un-freeze from 0.0.49 → 0.0.52; add classified errors + Retry (#693), injectThreadRouting (#697), full-transcript subagent cards, typed DX - new Error Handling guide: AgentError/AgentErrorKind, toAgentError, isAbortError, agent.retry(), the built-in <chat-error> Retry UX (#693) - new Client Tools guide: tools/action/view/ask with typed ViewProps/ToolArgs and typed agent state via createAgentRef (#685) - provideAgent reference: clientOptions + app-wide LANGGRAPH_CLIENT_OPTIONS retry-budget token with precedence (#681); Typed state via AgentRef (#685) - register both new guides in docs-config nav Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Summary
Closes capability gap F5 — and fixes a real, transport-agnostic framework defect a live smoke uncovered along the way.
F5's premise was wrong. A live-LLM smoke of
cockpit/ag-ui/subagentsshowed the AG-UI adapter does render and stream achat-subagent-card(the audit's "renders as a plain tool row, no card" was a timing artifact of the transient active-only card). The real defect:<chat-subagents [agent]>was mounted inside the per-assistant-messageaitemplate of the<chat>composition but bound the whole agent'ssubagents(), so it re-rendered the same active cards once per assistant message → duplicate cards. After completion the only durable trace was a generic "called task" chip.The fix: the spawning
tasktool call now renders as an inlinechat-subagent-cardinchat-tool-calls— anchored to its message, replacing the generic chip. Running = expanded/live, completed = collapsed summary (▸ research ✓ · 1 message). The duplicate per-message<chat-subagents>mount is removed. Keyed ontoolCallId, so it fixes both transports;ChatSubagentsComponentstays exported as a primitive.Changes (all in
libs/chat)chat-tool-calls: renders achat-subagent-card(ungrouped) for any tool call whose id is inagent.subagents(); normal calls unchanged.chatcomposition: dropped the duplicate per-message<chat-subagents>mount.chat-subagent-card: message count moved to the always-visiblechat-tracemeta slot so the collapsed (done) summary shows it.cockpit/ag-ui/subagentsnow asserts the card (presence + persistence + no-duplicate== subs.size) instead of the generic chip.Verification
nx run-many -t test lint build --projects=chat— green (new TDD specs in all three components).cockpit-ag-ui-subagents(card-asserted) +cockpit-chat-subagents— green.Notes
agent.subagents()doesn't populate under aimock replay (atomic replay omits subgraph namespace events), so the inline card can't be e2e-asserted on the chat cockpit — same documented limitation as the transient card; it works live (the canonicalexamples/chatalready renders LangGraph subagent cards live). No regression there: thetaskcall falls back to a generic chip under aimock exactly as before.Spec:
docs/superpowers/specs/2026-06-19-subagent-card-inline-persistent-design.md· Plan:docs/superpowers/plans/2026-06-19-subagent-card-inline-persistent.md🤖 Generated with Claude Code