|
| 1 | +# Inline, Persistent Subagent Cards — Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`). |
| 4 | +
|
| 5 | +**Goal:** Render each subagent once, anchored to its spawning `task` tool call, persisting in the transcript (running = expanded/live, done = collapsed summary) — replacing the generic tool chip. Fixes the duplicate-card framework bug + the transient-card weakness, transport-agnostically. |
| 6 | + |
| 7 | +**Architecture:** A subagent's spawning tool call already lands in the assistant message that emitted it (`resolveMessageToolCalls`). Make `chat-tool-calls` render a `chat-subagent-card` (instead of a generic chip) for any tool call whose `id` is a key in `agent.subagents()`, ungrouped. Then remove the per-message `<chat-subagents>` mount from the `<chat>` composition (the source of duplication). `chat-trace` already auto-expands `running`/`error` and collapses `done`/`pending`, so persistence + collapse-on-done come for free. |
| 8 | + |
| 9 | +**Tech Stack:** Angular 21 (signals, `@if`/`@for`, OnPush), vitest, Nx. Spec: `docs/superpowers/specs/2026-06-19-subagent-card-inline-persistent-design.md`. |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Task 1: `chat-tool-calls` renders subagent cards in place of chips |
| 14 | + |
| 15 | +**Files:** |
| 16 | +- Modify: `libs/chat/src/lib/primitives/chat-tool-calls/chat-tool-calls.component.ts` |
| 17 | +- Test: `libs/chat/src/lib/primitives/chat-tool-calls/chat-tool-calls.component.spec.ts` |
| 18 | + |
| 19 | +- [ ] **Step 1: Write the failing test.** Follow the existing spec's `setSignalInput` SIGNAL-symbol pattern and the existing fake-agent shape in this file. Build an agent whose `toolCalls()` returns `[{id:'call_t', name:'task', args:{}, status:'success'}, {id:'call_s', name:'search', args:{}, status:'success'}]`, whose `subagents()` returns a `Map` with one entry keyed `'call_t'` → a `Subagent` (`{toolCallId:'call_t', name:'research', status: signal('running'), messages: signal([{id:'m1', role:'assistant', content:'hello from research'}]), state: signal({})}`), and a message linking both ids via `toolCallIds: ['call_t','call_s']`. Assert: exactly one `chat-subagent-card` renders, it contains "research"; the `search` call still renders a `chat-tool-call-card`; and the `task` call does NOT render a generic `chat-tool-call-card`. Add a second test: two `task` subagent calls (`call_t1`, `call_t2`, both in `subagents()`) render TWO separate `chat-subagent-card`s (not one grouped strip). |
| 20 | + |
| 21 | +- [ ] **Step 2: Run it, verify it fails.** `npx nx test chat --skip-nx-cache -- chat-tool-calls` → FAIL (no `chat-subagent-card` rendered). |
| 22 | + |
| 23 | +- [ ] **Step 3: Implement.** In `chat-tool-calls.component.ts`: |
| 24 | + - Import `ChatSubagentCardComponent` from `../../compositions/chat-subagent-card/chat-subagent-card.component` and add to `imports`. Import `Subagent` type from `../../agent`/`../../agent/subagent`. |
| 25 | + - Add `subagent?: Subagent` to the `Group` interface. |
| 26 | + - In the `groups` computed, read `const subs = this.agent().subagents?.() ?? new Map<string, Subagent>();` (read inside the computed for reactivity). When iterating `calls`, if `subs.has(tc.id)`, push a **standalone** group `{ name: tc.name, calls: [tc], subagent: subs.get(tc.id) }` and never append to/from it (a subagent call neither groups with a previous call nor accepts a following call — treat it like a grouping break). Otherwise keep the existing group/append logic. |
| 27 | + - In the template, add a FIRST branch inside the `@for (group of groups())`: |
| 28 | + ```html |
| 29 | + @if (group.subagent) { |
| 30 | + <chat-subagent-card [subagent]="group.subagent" /> |
| 31 | + } @else if (group.calls.length > 1 && !group.templateRef) { |
| 32 | + ... existing grouped strip ... |
| 33 | + } @else if (group.templateRef) { ... } @else { ... } |
| 34 | + ``` |
| 35 | + - Keep `track $index` on the outer `@for` (already primitive). |
| 36 | + |
| 37 | +- [ ] **Step 4: Run tests.** `npx nx test chat --skip-nx-cache -- chat-tool-calls` → PASS (new + existing). |
| 38 | + |
| 39 | +- [ ] **Step 5: Commit.** |
| 40 | +```bash |
| 41 | +git add libs/chat/src/lib/primitives/chat-tool-calls/chat-tool-calls.component.ts libs/chat/src/lib/primitives/chat-tool-calls/chat-tool-calls.component.spec.ts |
| 42 | +git commit -m "feat(chat): chat-tool-calls renders subagent cards inline (anchored to spawning task call)" |
| 43 | +``` |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## Task 2: Remove the duplicate `<chat-subagents>` mount from the composition |
| 48 | + |
| 49 | +**Files:** |
| 50 | +- Modify: `libs/chat/src/lib/compositions/chat/chat.component.ts` |
| 51 | +- Test: `libs/chat/src/lib/compositions/chat/chat.component.spec.ts` (or the nearest existing composition spec) |
| 52 | + |
| 53 | +- [ ] **Step 1: Write the failing test.** Add a composition-level test (mirror existing composition spec setup) that mounts `<chat [agent]="agent">` with an agent whose `messages()` has TWO assistant messages, only the FIRST of which has `toolCallIds: ['call_t']`, and `subagents()` containing `call_t` (status `running`). Assert the DOM contains exactly ONE `chat-subagent-card` (today it renders twice — once per assistant message — because of the line-221 mount). If a full composition mount is too heavy in this suite, instead assert structurally that the `ai` template no longer contains a standalone `<chat-subagents>` element by rendering and querying — but prefer the behavioral one-card assertion. |
| 54 | + |
| 55 | +- [ ] **Step 2: Run it, verify it fails.** `npx nx test chat --skip-nx-cache -- chat.component` → FAIL (two cards). |
| 56 | + |
| 57 | +- [ ] **Step 3: Implement.** In `chat.component.ts`: delete the `<chat-subagents [agent]="agent()" />` line (≈221) in the `ai` message template. Remove the now-unused `ChatSubagentsComponent` import from this file's `imports` array and import statement. Do NOT remove the lib's public-API export of `ChatSubagentsComponent`. |
| 58 | + |
| 59 | +- [ ] **Step 4: Run tests.** `npx nx test chat --skip-nx-cache -- chat.component` → PASS (one card). Then `npx nx run-many -t test lint build --projects=chat --skip-nx-cache` → green. |
| 60 | + |
| 61 | +- [ ] **Step 5: Commit.** |
| 62 | +```bash |
| 63 | +git add libs/chat/src/lib/compositions/chat/chat.component.ts libs/chat/src/lib/compositions/chat/chat.component.spec.ts |
| 64 | +git commit -m "fix(chat): render subagent cards once via tool-calls; drop duplicate per-message mount" |
| 65 | +``` |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## Task 3: Collapsed summary shows message count (small polish) |
| 70 | + |
| 71 | +**Files:** |
| 72 | +- Modify: `libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts` |
| 73 | +- Test: `libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.spec.ts` |
| 74 | + |
| 75 | +**Context:** `chat-trace` hides default `<ng-content />` when collapsed but always shows `[traceMeta]`. The card's `N message(s)` count is currently in the hidden content. Move it to `[traceMeta]` so a collapsed (done) card still shows `research ✓ · N message(s)`. |
| 76 | + |
| 77 | +- [ ] **Step 1: Write the failing test.** With a `Subagent` whose `status` is `complete` and two messages, mount the card and assert the host (collapsed) still shows text matching `/2 message/`. (Today it's hidden when collapsed.) |
| 78 | + |
| 79 | +- [ ] **Step 2: Run it, verify it fails.** `npx nx test chat --skip-nx-cache -- chat-subagent-card` → FAIL. |
| 80 | + |
| 81 | +- [ ] **Step 3: Implement.** In the card template, move the `<div class="sac__count">{{ subagent().messages().length }} message(s)</div>` to be projected into the trace meta slot: add `ngProjectAs="[traceMeta]"` (or wrap as `<span traceMeta>`), matching how `chat-trace` selects `[traceMeta]`. Keep styling via `.sac__count`. |
| 82 | + |
| 83 | +- [ ] **Step 4: Run tests.** `npx nx test chat --skip-nx-cache -- chat-subagent-card` → PASS. |
| 84 | + |
| 85 | +- [ ] **Step 5: Commit.** |
| 86 | +```bash |
| 87 | +git add libs/chat/src/lib/compositions/chat-subagent-card |
| 88 | +git commit -m "feat(chat): show subagent message count in collapsed card summary" |
| 89 | +``` |
| 90 | + |
| 91 | +--- |
| 92 | + |
| 93 | +## Task 4: Reconcile subagent e2e (chip → card + persistence) |
| 94 | + |
| 95 | +**Files:** |
| 96 | +- Modify: `cockpit/ag-ui/subagents/angular/e2e/subagents.spec.ts` |
| 97 | +- Modify: `cockpit/chat/subagents/angular/e2e/c-subagents.spec.ts` |
| 98 | +- Check (no change expected): `examples/chat/angular/e2e/*.spec.ts`, any `cockpit/langgraph` subagent e2e |
| 99 | + |
| 100 | +- [ ] **Step 1: Re-scan.** `grep -rn "called task\|/task/i\|chat-subagent" cockpit/*/subagents/angular/e2e examples/chat/angular/e2e` to enumerate every chip assertion. Only the two cockpit subagent specs are expected. |
| 101 | +- [ ] **Step 2: Update `cockpit/ag-ui/subagents/angular/e2e/subagents.spec.ts`.** The `task` call now renders as a `chat-subagent-card`, not a `getByRole('button', { name: /called task|task/i })` chip. Replace that chip assertion with one for the card — e.g. `await expect(page.locator('chat-subagent-card').first()).toBeVisible({ timeout: 30_000 })`. Keep the `readSubagents` projection assertions (still valid). Add an assertion that the card **persists after completion** (still present once the research subagent is `complete`) and that there is no duplicate (`expect(page.locator('chat-subagent-card')).toHaveCount(<expected>)` — assert count equals the number of distinct subagents, not 2× per message). Update the stale code comments that describe the active-only/transient behavior. |
| 102 | +- [ ] **Step 3: Update `cockpit/chat/subagents/angular/e2e/c-subagents.spec.ts`** the same way (chip → card; note the comment at line ~16 says it does NOT assert the card "because that primitive only [renders while active]" — now it CAN and SHOULD). |
| 103 | +- [ ] **Step 4: Run the two e2e suites (aimock).** |
| 104 | +```bash |
| 105 | +# free ports first (per repo memory), then: |
| 106 | +npx nx e2e cockpit-ag-ui-subagents-angular --skip-nx-cache |
| 107 | +npx nx e2e cockpit-chat-subagents-angular --skip-nx-cache |
| 108 | +``` |
| 109 | +Expected: green. Reconcile fixtures only if a card assertion needs a longer settle (the aimock run settles fast; assert presence/persistence, not running-state timing). |
| 110 | +- [ ] **Step 5: Commit.** |
| 111 | +```bash |
| 112 | +git add cockpit/ag-ui/subagents/angular/e2e/subagents.spec.ts cockpit/chat/subagents/angular/e2e/c-subagents.spec.ts |
| 113 | +git commit -m "test(cockpit): assert inline persistent subagent card (chip → card)" |
| 114 | +``` |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## Task 5: Verify, live re-smoke, audit-doc close-out, PR |
| 119 | + |
| 120 | +- [ ] **Step 1: Full lib gate + demo builds.** `npx nx run-many -t test lint build --projects=chat --skip-nx-cache`; build `cockpit-ag-ui-subagents-angular`, `cockpit-chat-subagents-angular`, `examples-chat-angular`, and any `cockpit-langgraph` subagent app. All green. |
| 121 | +- [ ] **Step 2: Live re-smoke (AG-UI, real key).** Reuse the F5 harness: `uv run uvicorn src.server:app --port 5326 --env-file <root>/.env` in `cockpit/ag-ui/subagents/python` + `nx serve cockpit-ag-ui-subagents-angular --port 4326`; drive "Plan a trip from LAX to JFK" with the high-frequency Playwright probe (sample `chat-subagent-card` count + statuses). Confirm: ONE card per subagent (no duplicates), card persists after completion, collapses to summary when done, zero NG0956. Capture screenshots. Free ports + stop servers after. |
| 122 | +- [ ] **Step 3: Update the audit findings doc.** In `docs/superpowers/specs/2026-06-11-ag-ui-capability-findings.md`, mark **F5 closed** with the real-defect note (card already rendered over AG-UI; the actual fix was the duplicate per-message mount + persistence). Commit. |
| 123 | +- [ ] **Step 4: Final review** (correctness of the subagent partition in `chat-tool-calls`; no regression to GenUI/view exclusion or grouping; card persistence + collapse; e2e green; live smoke clean). |
| 124 | +- [ ] **Step 5: Open PR**, arm auto-merge (`gh pr merge --squash --auto`), self-healing watcher (Monitor for `BEHIND` → `git merge origin/main`; ignore non-required `review` check). |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +## Self-Review |
| 129 | +- Coverage: subagent-aware rendering (T1), duplicate-mount removal (T2), collapsed summary (T3), e2e reconcile (T4), verify+smoke+doc+PR (T5). ✓ |
| 130 | +- Type consistency: `Subagent` (status/messages are Signals); `agent().subagents?.()` is optional → default `new Map()`. Card uses `subagent` input (required). `track tc.id` / `track $index` primitives — no NG0956 risk. |
| 131 | +- Risk: subagent `task` call must be present in the message's resolved tool calls on BOTH transports — verified by the live smoke (AG-UI) + the cockpit-chat e2e (LangGraph). If a transport surfaces a subagent with no owning message tool call, its card wouldn't render → caught by Step 2/Task 4. |
| 132 | +- No public-API break: `ChatSubagentsComponent` stays exported; only the default composition stops mounting it. |
0 commit comments