Stream Claude Code subagent activity inline in the transcript

This commit is contained in:
Ian Gulliver
2026-10-15 11:08:53 -07:00
parent 0e43a99115
commit d83212d46b
11 changed files with 325 additions and 35 deletions
+73 -10
View File
@@ -153,6 +153,7 @@ export function mountChat(root, src, makeHead) {
}, { root: transcript, threshold: 0.5 });
const newCtx = () => ({
tools: new Map(),
subagents: new Map(),
run: undefined,
anchors: [],
unplaced: [],
@@ -255,10 +256,11 @@ export function mountChat(root, src, makeHead) {
releaseImages(entry.imgs);
pending = pending.filter((p) => p !== entry);
};
const addToRun = (el, call) => {
const runHead = (run) => [subagentTag(run.parent, ctx), runSummary(run.calls), snapMark(run.snaps)];
const addToRun = (el, call, parent) => {
const run = ctx.run;
if (!run) {
ctx.run = { first: el, group: undefined, calls: call ? [call] : [], snaps: [] };
if (!run || run.parent !== parent) {
ctx.run = { first: el, group: undefined, calls: call ? [call] : [], snaps: [], parent };
put(el);
return;
}
@@ -274,7 +276,7 @@ export function mountChat(root, src, makeHead) {
run.group = { el: group, summary, body };
}
run.group.body.append(el);
replace(run.group.summary, runSummary(run.calls), snapMark(run.snaps));
replace(run.group.summary, ...runHead(run));
};
const addAnchor = (seq, el, turnEnd) => {
const anchor = { seq, turnEnd, el, snaps: [], mark: undefined, run: turnEnd ? undefined : ctx.run };
@@ -301,7 +303,7 @@ export function mountChat(root, src, makeHead) {
}
run.snaps.push(n);
if (run.group) {
replace(run.group.summary, runSummary(run.calls), snapMark(run.snaps));
replace(run.group.summary, ...runHead(run));
}
};
const placeSnapshot = (snap) => {
@@ -380,7 +382,12 @@ export function mountChat(root, src, makeHead) {
headImpl.onEvents?.(events);
for (const ev of events) {
const p = ev.payload;
if (ev.kind === "agent.message" || ev.kind === "turn.ended" || ev.kind === "user.message") {
const parent = parentOf(p);
if (ev.kind === "agent.vendor") {
claudeTaskStatus(p, ctx);
continue;
}
if ((ev.kind === "agent.message" && !parent) || ev.kind === "turn.ended" || ev.kind === "user.message") {
dropStream();
}
if (ev.kind === "user.message" && ev.actor !== "byre") {
@@ -397,9 +404,16 @@ export function mountChat(root, src, makeHead) {
placeSnapshot(p);
continue;
}
if (ev.kind === "agent.tool_call" && isSubagentCall(p)) {
ctx.run = undefined;
const el = renderSubagent(p, ctx);
put(el);
addAnchor(ev.seq, el.querySelector("summary"), false);
continue;
}
if (ev.kind === "agent.tool_call") {
const el = renderToolCall(p, ctx);
addToRun(el, { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) });
addToRun(el, { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) }, parent);
addAnchor(ev.seq, el.querySelector("summary"), false);
continue;
}
@@ -408,7 +422,7 @@ export function mountChat(root, src, makeHead) {
continue;
}
if (ev.kind === "agent.thinking") {
addToRun(el, undefined);
addToRun(el, undefined, parent);
continue;
}
ctx.run = undefined;
@@ -701,10 +715,56 @@ export function compactButton(sessionId, sessionState, c) {
};
return b;
}
function parentOf(p) {
return typeof p["parent_tool_use_id"] === "string" ? p["parent_tool_use_id"] : "";
}
function subagentTag(parent, ctx) {
if (!parent) {
return null;
}
const sub = ctx.subagents.get(parent);
return h("span", { class: "subagent-tag", title: sub?.description ?? "" }, `↳ ${sub?.type ?? "subagent"}`);
}
function renderToolCall(p, ctx) {
const result = h("div", { class: "tool-result pending" }, "running…");
ctx.tools.set(String(p["id"]), result);
return h("details", { class: "tool" }, h("summary", {}, h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root))), h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)), result);
return h("details", { class: "tool" }, h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root))), h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)), result);
}
function isSubagentCall(p) {
return p["name"] === "Agent" || p["name"] === "Task";
}
function renderSubagent(p, ctx) {
const input = (typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {});
const id = String(p["id"]);
const type = String(input["subagent_type"] ?? "subagent");
const description = String(input["description"] ?? "");
const status = h("span", { class: "subagent-status" }, "starting…");
const result = h("div", { class: "tool-result pending" }, "running…");
const tag = subagentTag(parentOf(p), ctx);
ctx.tools.set(id, result);
ctx.subagents.set(id, { type, description, status });
return h("details", { class: "tool subagent" }, h("summary", {}, tag, h("span", { class: "tool-name" }, `${p["name"]} ${type}`), " ", h("span", { class: "tool-brief" }, description), " ", status), markdown(String(input["prompt"] ?? ""), "text md subagent-prompt", { taskId: ctx.taskId }), result);
}
function claudeTaskStatus(p, ctx) {
if (p["type"] !== "system") {
return;
}
const sub = ctx.subagents.get(String(p["tool_use_id"]));
if (!sub) {
return;
}
switch (p["subtype"]) {
case "task_started":
replace(sub.status, p["is_backgrounded"] === true ? "running in the background" : "running");
return;
case "task_progress":
replace(sub.status, String(p["description"] ?? "running"));
return;
case "task_notification":
sub.status.className = `subagent-status ${p["status"] === "completed" ? "done" : "error"}`;
replace(sub.status, String(p["status"]));
return;
}
}
function snapMark(ns) {
if (ns.length === 0) {
@@ -761,6 +821,9 @@ function renderEvent(ev, ctx) {
}
return h("div", { class: "msg user" }, h("div", { class: "who" }, ev.actor, " ", when), messageImages(p["images"]), String(p["text"] ?? "").trim() ? markdown(String(p["text"]), "text md", md) : null);
case "agent.message":
if (parentOf(p)) {
return h("div", { class: "msg agent subagent" }, h("div", { class: "who" }, subagentTag(parentOf(p), ctx), " ", when), markdown(String(p["text"]), "text md", md));
}
if (String(p["text"]).trim() === quietReply) {
ctx.quiet = true;
return null;
@@ -769,7 +832,7 @@ function renderEvent(ev, ctx) {
ctx.lastMessage = h("div", { class: "msg agent" }, markdown(String(p["text"]), "text md", md));
return ctx.lastMessage;
case "agent.thinking":
return h("details", { class: "thinking" }, h("summary", {}, "thinking"), markdown(String(p["text"]), "text md", md));
return h("details", { class: "thinking" }, h("summary", {}, subagentTag(parentOf(p), ctx), "thinking"), markdown(String(p["text"]), "text md", md));
case "agent.tool_result": {
const target = ctx.tools.get(String(p["tool_use_id"]));
const body = h("pre", {}, resultText(p["content"]));
+1 -1
View File
File diff suppressed because one or more lines are too long
+93 -10
View File
@@ -39,6 +39,13 @@ type Run = {
group: { el: HTMLDetailsElement; summary: HTMLElement; body: HTMLElement } | undefined;
calls: Call[];
snaps: number[];
parent: string;
};
type Subagent = {
type: string;
description: string;
status: HTMLElement;
};
type Anchor = {
@@ -52,6 +59,7 @@ type Anchor = {
type Ctx = {
tools: Map<string, HTMLElement>;
subagents: Map<string, Subagent>;
run: Run | undefined;
anchors: Anchor[];
unplaced: Snapshot[];
@@ -223,6 +231,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
const newCtx = (): Ctx => ({
tools: new Map(),
subagents: new Map(),
run: undefined,
anchors: [],
unplaced: [],
@@ -333,10 +342,12 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
pending = pending.filter((p) => p !== entry);
};
const addToRun = (el: HTMLElement, call: Call | undefined) => {
const runHead = (run: Run) => [subagentTag(run.parent, ctx), runSummary(run.calls), snapMark(run.snaps)];
const addToRun = (el: HTMLElement, call: Call | undefined, parent: string) => {
const run = ctx.run;
if (!run) {
ctx.run = { first: el, group: undefined, calls: call ? [call] : [], snaps: [] };
if (!run || run.parent !== parent) {
ctx.run = { first: el, group: undefined, calls: call ? [call] : [], snaps: [], parent };
put(el);
return;
}
@@ -352,7 +363,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
run.group = { el: group, summary, body };
}
run.group.body.append(el);
replace(run.group.summary, runSummary(run.calls), snapMark(run.snaps));
replace(run.group.summary, ...runHead(run));
};
const addAnchor = (seq: number, el: HTMLElement, turnEnd: boolean) => {
@@ -381,7 +392,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
}
run.snaps.push(n);
if (run.group) {
replace(run.group.summary, runSummary(run.calls), snapMark(run.snaps));
replace(run.group.summary, ...runHead(run));
}
};
@@ -463,7 +474,12 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
headImpl.onEvents?.(events);
for (const ev of events) {
const p = ev.payload as Record<string, unknown>;
if (ev.kind === "agent.message" || ev.kind === "turn.ended" || ev.kind === "user.message") {
const parent = parentOf(p);
if (ev.kind === "agent.vendor") {
claudeTaskStatus(p, ctx);
continue;
}
if ((ev.kind === "agent.message" && !parent) || ev.kind === "turn.ended" || ev.kind === "user.message") {
dropStream();
}
if (ev.kind === "user.message" && ev.actor !== "byre") {
@@ -480,9 +496,16 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
placeSnapshot(p as unknown as Snapshot);
continue;
}
if (ev.kind === "agent.tool_call" && isSubagentCall(p)) {
ctx.run = undefined;
const el = renderSubagent(p, ctx);
put(el);
addAnchor(ev.seq, el.querySelector("summary")!, false);
continue;
}
if (ev.kind === "agent.tool_call") {
const el = renderToolCall(p, ctx);
addToRun(el, { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) });
addToRun(el, { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) }, parent);
addAnchor(ev.seq, el.querySelector("summary")!, false);
continue;
}
@@ -491,7 +514,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
continue;
}
if (ev.kind === "agent.thinking") {
addToRun(el, undefined);
addToRun(el, undefined, parent);
continue;
}
ctx.run = undefined;
@@ -818,18 +841,75 @@ export function compactButton(sessionId: string | undefined, sessionState: strin
return b;
}
function parentOf(p: Record<string, unknown>): string {
return typeof p["parent_tool_use_id"] === "string" ? p["parent_tool_use_id"] : "";
}
function subagentTag(parent: string, ctx: Ctx): HTMLElement | null {
if (!parent) {
return null;
}
const sub = ctx.subagents.get(parent);
return h("span", { class: "subagent-tag", title: sub?.description ?? "" }, `↳ ${sub?.type ?? "subagent"}`);
}
function renderToolCall(p: Record<string, unknown>, ctx: Ctx): HTMLElement {
const result = h("div", { class: "tool-result pending" }, "running…");
ctx.tools.set(String(p["id"]), result);
return h(
"details",
{ class: "tool" },
h("summary", {}, h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root))),
h("summary", {}, subagentTag(parentOf(p), ctx), h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root))),
h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)),
result,
);
}
function isSubagentCall(p: Record<string, unknown>): boolean {
return p["name"] === "Agent" || p["name"] === "Task";
}
function renderSubagent(p: Record<string, unknown>, ctx: Ctx): HTMLElement {
const input = (typeof p["input"] === "object" && p["input"] !== null ? p["input"] : {}) as Record<string, unknown>;
const id = String(p["id"]);
const type = String(input["subagent_type"] ?? "subagent");
const description = String(input["description"] ?? "");
const status = h("span", { class: "subagent-status" }, "starting…");
const result = h("div", { class: "tool-result pending" }, "running…");
const tag = subagentTag(parentOf(p), ctx);
ctx.tools.set(id, result);
ctx.subagents.set(id, { type, description, status });
return h(
"details",
{ class: "tool subagent" },
h("summary", {}, tag, h("span", { class: "tool-name" }, `${p["name"]} ${type}`), " ", h("span", { class: "tool-brief" }, description), " ", status),
markdown(String(input["prompt"] ?? ""), "text md subagent-prompt", { taskId: ctx.taskId }),
result,
);
}
function claudeTaskStatus(p: Record<string, unknown>, ctx: Ctx): void {
if (p["type"] !== "system") {
return;
}
const sub = ctx.subagents.get(String(p["tool_use_id"]));
if (!sub) {
return;
}
switch (p["subtype"]) {
case "task_started":
replace(sub.status, p["is_backgrounded"] === true ? "running in the background" : "running");
return;
case "task_progress":
replace(sub.status, String(p["description"] ?? "running"));
return;
case "task_notification":
sub.status.className = `subagent-status ${p["status"] === "completed" ? "done" : "error"}`;
replace(sub.status, String(p["status"]));
return;
}
}
function snapMark(ns: number[]): HTMLElement | null {
if (ns.length === 0) {
return null;
@@ -901,6 +981,9 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
);
case "agent.message":
if (parentOf(p)) {
return h("div", { class: "msg agent subagent" }, h("div", { class: "who" }, subagentTag(parentOf(p), ctx), " ", when), markdown(String(p["text"]), "text md", md));
}
if (String(p["text"]).trim() === quietReply) {
ctx.quiet = true;
return null;
@@ -910,7 +993,7 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
return ctx.lastMessage;
case "agent.thinking":
return h("details", { class: "thinking" }, h("summary", {}, "thinking"), markdown(String(p["text"]), "text md", md));
return h("details", { class: "thinking" }, h("summary", {}, subagentTag(parentOf(p), ctx), "thinking"), markdown(String(p["text"]), "text md", md));
case "agent.tool_result": {
const target = ctx.tools.get(String(p["tool_use_id"]));
+29
View File
@@ -735,6 +735,35 @@ button.compact.danger {
color: var(--muted);
}
.subagent-status {
float: right;
margin-left: 12px;
color: var(--accent);
}
.subagent-status.done {
color: var(--muted);
}
.subagent-status.error {
color: var(--danger);
}
.subagent-tag {
margin-right: 8px;
color: var(--accent);
font-size: 11px;
}
.msg.agent.subagent {
padding-left: 10px;
border-left: 1px solid var(--accent);
}
.subagent-prompt {
padding: 6px 0;
}
.tool-input,
.tool-result pre {
background: var(--raised);