Skip to content

Commit b270c47

Browse files
committed
feat(web): add the Agents panel
Surfaces the subagent data the previous PRs record. A new right-panel tab lists every subagent on the thread with its role, model, status, token usage, recent activity, and the runs it has been activated for. Grouping lives in client-runtime rather than the component so it stays testable: workflow coordinators own their phases, phase status derives from its members, and members orphaned by a missing coordinator fall back to the flat list rather than disappearing. A coordinator is an agent in its own right, so it renders as a card rather than a bare heading — it has a model, usage and activations, and before its members are spawned it is the only row on the thread. It is counted in the tallies for the same reason, but only while it has no members, since afterwards they represent the same work and counting both double-reports it. Without that, a thread running a workflow showed "0 active" with nothing listed and unexplained tokens in the header. Usage follows the same rule: the total keeps whatever the members did not account for. Dropping the coordinator outright erased the whole workflow whenever a provider reported workflow usage but omitted per-agent tokens, and the header read "Usage unavailable" for a workflow that had spent thousands. Read-only: this only renders what the projection already contains.
1 parent 85fe8a2 commit b270c47

8 files changed

Lines changed: 845 additions & 25 deletions

File tree

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
NodeId,
3+
ProviderDriverKind,
4+
ProviderInstanceId,
5+
RunId,
6+
ThreadId,
7+
type OrchestrationV2Subagent,
8+
} from "@t3tools/contracts";
9+
import * as DateTime from "effect/DateTime";
10+
import { renderToStaticMarkup } from "react-dom/server";
11+
import { describe, expect, it } from "vite-plus/test";
12+
13+
import { AgentsPanelV2 } from "./AgentsPanelV2";
14+
import { makeThreadProjectionFixture } from "../test-fixtures";
15+
16+
const now = DateTime.makeUnsafe("2026-07-26T00:00:00.000Z");
17+
const workflowId = NodeId.make("workflow-1");
18+
19+
const agent = (id: string, input: Partial<OrchestrationV2Subagent> = {}): OrchestrationV2Subagent =>
20+
({
21+
id: NodeId.make(id),
22+
threadId: ThreadId.make("thread-test"),
23+
runId: RunId.make("run-test"),
24+
parentNodeId: NodeId.make("root-node"),
25+
origin: "provider_native",
26+
createdBy: "agent",
27+
driver: ProviderDriverKind.make("claudeAgent"),
28+
providerInstanceId: ProviderInstanceId.make("claudeAgent"),
29+
providerThreadId: null,
30+
childThreadId: null,
31+
nativeTaskRef: null,
32+
prompt: "Work",
33+
title: id,
34+
model: "claude-opus-4-1",
35+
kind: "subagent",
36+
role: { name: "general-purpose", source: "provider" },
37+
status: "running",
38+
result: null,
39+
usage: null,
40+
currentActivationId: null,
41+
activationCount: 1,
42+
workflow: null,
43+
workflowMembership: null,
44+
recentActivity: [],
45+
startedAt: now,
46+
completedAt: null,
47+
updatedAt: now,
48+
...input,
49+
}) as OrchestrationV2Subagent;
50+
51+
const renderPanel = (subagents: ReadonlyArray<OrchestrationV2Subagent>) =>
52+
renderToStaticMarkup(
53+
<AgentsPanelV2 projection={{ ...makeThreadProjectionFixture(), subagents }} />,
54+
);
55+
56+
describe("AgentsPanelV2", () => {
57+
it("renders a workflow coordinator as a card before its members exist", () => {
58+
// The Claude-only workflow path. A coordinator arrives before any member
59+
// does, so rendering it as a bare heading left the panel showing a title,
60+
// no rows, and "0 active" while the workflow was running.
61+
const markup = renderPanel([
62+
agent("workflow-1", {
63+
kind: "workflow",
64+
title: "Refactor sweep",
65+
role: { name: "workflow-coordinator", source: "app_default" },
66+
status: "running",
67+
usage: { totalTokens: 5000 },
68+
workflow: { phases: [{ index: 0, title: "Research" }] },
69+
}),
70+
]);
71+
72+
expect(markup).toContain("Refactor sweep");
73+
expect(markup).toContain("workflow-coordinator");
74+
// Its own model and usage only appear if it rendered as a card.
75+
expect(markup).toContain("claude-opus-4-1");
76+
expect(markup).toContain("5k tokens");
77+
expect(markup).toContain("1 active");
78+
expect(markup).toContain("Research");
79+
});
80+
81+
it("renders workflow members under their phase", () => {
82+
const markup = renderPanel([
83+
agent("workflow-1", {
84+
kind: "workflow",
85+
title: "Refactor sweep",
86+
status: "running",
87+
workflow: {
88+
phases: [
89+
{ index: 0, title: "Research" },
90+
{ index: 1, title: "Implement" },
91+
],
92+
},
93+
}),
94+
agent("researcher", {
95+
kind: "workflow_agent",
96+
title: "researcher",
97+
status: "completed",
98+
workflowMembership: {
99+
workflowSubagentId: workflowId,
100+
agentIndex: 0,
101+
phaseIndex: 0,
102+
attempt: 1,
103+
},
104+
}),
105+
agent("implementer", {
106+
kind: "workflow_agent",
107+
title: "implementer",
108+
status: "running",
109+
workflowMembership: {
110+
workflowSubagentId: workflowId,
111+
agentIndex: 0,
112+
phaseIndex: 1,
113+
attempt: 1,
114+
},
115+
}),
116+
]);
117+
118+
expect(markup).toContain("Research");
119+
expect(markup).toContain("Implement");
120+
expect(markup).toContain("researcher");
121+
expect(markup).toContain("implementer");
122+
// Members represent the coordinator's work, so only they are tallied.
123+
expect(markup).toContain("1 active");
124+
expect(markup).toContain("1 settled");
125+
});
126+
127+
it("keeps a member visible when its coordinator is missing", () => {
128+
const markup = renderPanel([
129+
agent("orphan", {
130+
kind: "workflow_agent",
131+
title: "orphaned-worker",
132+
workflowMembership: {
133+
workflowSubagentId: workflowId,
134+
agentIndex: 0,
135+
phaseIndex: 0,
136+
attempt: 1,
137+
},
138+
}),
139+
]);
140+
141+
expect(markup).toContain("orphaned-worker");
142+
});
143+
});

0 commit comments

Comments
 (0)