Skip to content

Commit aa56398

Browse files
committed
Merge remote-tracking branch 'origin/main' into chore/release-0.0.52
2 parents 1c2af9c + 31cefc8 commit aa56398

11 files changed

Lines changed: 453 additions & 32 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ jobs:
229229

230230
cockpit-deploy-smoke:
231231
name: Cockpit — deploy smoke dry-run
232+
timeout-minutes: 30 # fail fast instead of blocking the main concurrency group on a hang
232233
needs: ci-scope
233234
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_deploy_smoke == 'true'
234235
runs-on: ubuntu-latest
@@ -595,6 +596,7 @@ jobs:
595596
596597
deploy:
597598
name: Deploy → Vercel
599+
timeout-minutes: 30 # fail fast instead of blocking the main concurrency group on a hang
598600
needs:
599601
[
600602
library,
@@ -783,6 +785,7 @@ jobs:
783785
784786
demo-deploy:
785787
name: Canonical demo → Vercel
788+
timeout-minutes: 30 # fail fast instead of blocking the main concurrency group on a hang
786789
needs: [examples-chat-smoke, examples-chat-e2e]
787790
runs-on: ubuntu-latest
788791
if: ${{ always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' }}
@@ -859,6 +862,7 @@ jobs:
859862
860863
ag-ui-demo-deploy:
861864
name: ag-ui demo → Vercel
865+
timeout-minutes: 30 # fail fast instead of blocking the main concurrency group on a hang
862866
needs: [examples-ag-ui-e2e]
863867
runs-on: ubuntu-latest
864868
if: ${{ always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' }}

cockpit/ag-ui/subagents/angular/e2e/subagents.spec.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ interface SubagentProbe {
1717
}
1818

1919
// Reads the live `agent.subagents()` projection off the cockpit host component
20-
// via Angular's dev-mode global. The chat-subagents primitive renders a
21-
// <chat-subagent-card> only while a subagent is pending/running, so the map IS
22-
// the data the card binds to. Under the aimock harness the run settles
23-
// near-instantly (started → finished within one SSE flush), so the live card
24-
// transits the RUNNING state below a render frame and is filtered out of the
25-
// DOM by the time the assistant turn finalizes — exactly the reason this spec
26-
// asserts on the durable projection rather than the card element. The map
27-
// proves the ACTIVITY snapshot/delta pipeline populated the subagent (name +
28-
// streamed child text) and that it settled to `complete`.
20+
// via Angular's dev-mode global. The spawning `task` tool call now renders
21+
// inline AS a <chat-subagent-card> anchored to its message, and the card
22+
// PERSISTS (collapsed) after the subagent completes — there is no separate
23+
// active-only mount, and the `task` call no longer renders a generic tool-call
24+
// chip. The map read here is the data the card binds to: it proves the ACTIVITY
25+
// snapshot/delta pipeline populated the subagent (name + streamed child text)
26+
// and that it settled to `complete`. The card element and this projection are
27+
// asserted together below (card presence/persistence + projection contents).
2928
async function readSubagents(page: Page): Promise<SubagentProbe> {
3029
return page.evaluate(() => {
3130
const ng = (window as unknown as { ng?: { getComponent?: (el: Element) => unknown } }).ng;
@@ -56,25 +55,25 @@ async function readSubagents(page: Page): Promise<SubagentProbe> {
5655
// `task` tool, the subagent LLM streams a summary, and the ag-ui server
5756
// converts the subagent_activity CUSTOM events into native ACTIVITY_SNAPSHOT/
5857
// ACTIVITY_DELTA. The @threadplane/ag-ui reducer projects the activity to
59-
// agent.subagents() (what chat-subagents renders as a live card) and the
60-
// child's research text must stay OUT of the parent's bubble.
58+
// agent.subagents(), which the inline <chat-subagent-card> (rendered in place of
59+
// the `task` tool call) binds to. The child's research text must stay OUT of the
60+
// parent's bubble.
6161
test('ag-ui subagents: orchestrator dispatches subagent cards that settle complete', async ({
6262
page,
6363
}) => {
6464
const bubble = await submitAndWaitForResponse(page, PROMPT);
6565

66-
// The orchestrator dispatched `task` subagents — the chat-tool-calls
67-
// primitive renders a durable collapsible chip labeled with the tool name,
68-
// unlike the active-only subagent card. Asserting it is in the DOM proves the
69-
// orchestrator emitted real task tool_calls.
70-
const taskChip = page.getByRole('button', { name: /called task|task/i }).first();
71-
await expect(taskChip).toBeVisible({ timeout: 30_000 });
66+
// The spawning `task` tool call renders inline AS a <chat-subagent-card>
67+
// (replacing the generic tool-call chip), anchored to its message. Asserting
68+
// the card is in the DOM proves the orchestrator emitted a real task dispatch
69+
// and that it surfaced as the dedicated subagent card.
70+
await expect(page.locator('chat-subagent-card').first()).toBeVisible({ timeout: 30_000 });
7271

7372
// The live subagent-card data path populated and settled: agent.subagents()
7473
// carries the research subagent with its streamed child summary, now
75-
// `complete`. This is the exact projection chat-subagents binds the card to.
76-
// Poll until the research subagent reaches `complete` to avoid CI micro-races
77-
// where signal propagation hasn't settled at the moment of the first read.
74+
// `complete`. This is the exact projection the inline card binds to. Poll
75+
// until the research subagent reaches `complete` to avoid CI micro-races where
76+
// signal propagation hasn't settled at the moment of the first read.
7877
await expect
7978
.poll(
8079
async () => {
@@ -86,8 +85,18 @@ test('ag-ui subagents: orchestrator dispatches subagent cards that settle comple
8685
)
8786
.toBe('complete');
8887

88+
// The card PERSISTS after completion (collapsed) — there is no active-only
89+
// mount, so it must still be present once the run has settled.
90+
await expect(page.locator('chat-subagent-card').first()).toBeVisible();
91+
8992
const subs = await readSubagents(page);
9093
expect(subs.size).toBeGreaterThan(0);
94+
95+
// No duplicate cards: exactly one <chat-subagent-card> per projected subagent.
96+
// Under aimock the orchestrator may dispatch 1+ subagents; assert equality to
97+
// the projected size rather than a hardcoded count.
98+
expect(await page.locator('chat-subagent-card').count()).toBe(subs.size);
99+
91100
const research = subs.entries.find((e) => e.name === 'research');
92101
expect(research, 'a research subagent should be projected').toBeTruthy();
93102
expect(research?.text).toContain(RESEARCH_SENTENCE);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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

Comments
 (0)