feat(web): subagent side panel with a live call-graph, compact chips in the stream (#78)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei ZhengandClaude Fable 5 authored and GitHub committed 2026-07-28 00:17:18 +08:00
1 parent d3c8f1da7b
commit 08f59baa70
22 files changed
+2330 -175

No files matched your search

@@ -1,11 +1,10 @@
/**
* Tool call card: collapses to a single line by
* default — status icon + tool name + duration (a live-ticking timer while running) + status
* badge; clicking expands full arguments and output; a nested subagent renders below them
* regardless of collapsed state.
* The pending-approval row is always visible regardless of collapsed state; when a pending
* approval appears anywhere in a nested subagent chain, the card auto-expands once (respecting
* the user's choice if they've manually collapsed it since).
* badge; clicking expands full arguments and output; a bound subagent renders as a full-width
* shortcut row below them regardless of collapsed state (the child conversation itself lives
* in the subagents side panel; the row carries its own pending-approval dot, so the card no
* longer needs to auto-expand for nested approvals).
*
* Duration accounting = **argument-generation segment + execution segment** (excludes time
* spent waiting on human approval): the model streaming out arguments token by token is often
@@ -13,7 +12,7 @@
* understate this step's cost. While waiting on approval, the already-settled generation
* segment is shown, with a separate "Waiting for approval" badge attached.
*/
import { useEffect, useRef, useState } from "react";
import { useRef, useState } from "react";
import { S } from "../../lib/strings";
import { humanizeDuration } from "../../lib/format";
import { approvalKey } from "../../lib/omni/stream-model";
@@ -25,7 +24,8 @@ import { StatusIcon } from "../../components/ui/status-icon";
import type { RunState } from "../../components/ui/status-icon";
import { ApprovalButtons } from "./approval-buttons";
import { LiveDuration } from "./live-duration";
import { SubagentCard } from "./subagent-card";
import { agentIdFromRunSubagentArgs } from "./agent-topology";
import { SubagentChip } from "./subagent-chip";
import type { StreamRenderContext } from "./message-stream";
/** Tools that accept the optional model-written `description` argument. */
@@ -172,21 +172,6 @@ export function ToolCallCard({ item, ctx }: { item: ToolCallItem; ctx: StreamRen
// Matched by the current origin chain + toolCallId: prevents parent/child session tool_call_id collisions from lighting each other up.
const pending = ctx.pendingApprovals.get(approvalKey(ctx.origin, item.toolCallId));
// Whether there's a pending approval anywhere in the nested subagent chain (keys are `origin-chain toolCallId`, matched by prefix at any depth).
const nestedPrefix = item.subagentSessionId
? [...ctx.origin, item.subagentSessionId].join("/")
: null;
const hasNestedPending =
nestedPrefix !== null &&
[...ctx.pendingApprovals.keys()].some(
(k) => k.startsWith(`${nestedPrefix} `) || k.startsWith(`${nestedPrefix}/`),
);
// A nested pending approval needs the user's action: auto-expand once (respected if the user manually collapses it afterward).
useEffect(() => {
if (hasNestedPending && !userToggled.current) setOpen(true);
}, [hasNestedPending]);
const preview = previewArguments(item.name, item.argumentsText);
const subtitle = headerSubtitle(item.name, item.argumentsText);
// Executing = the call has finished streaming, output hasn't arrived yet, and it's not waiting on approval (approval wait time doesn't count toward execution).
@@ -357,13 +342,14 @@ export function ToolCallCard({ item, ctx }: { item: ToolCallItem; ctx: StreamRen
</div>
)}
{/* Subagent card: always visible (not hidden after completion, only collapsed by default), unaffected by the tool card's collapsed state. Rendered below the expanded arguments/output so the nested conversation reads after the tool call's own content, not between the header and its details; pt-2 keeps a gap from the tinted args/output blocks above. */}
{/* Subagent row: always visible (unaffected by the tool card's collapsed state) below the expanded arguments/output — a full-width shortcut bar into the subagents panel; the nested conversation no longer renders inline. */}
{item.subagent && (
<div className="px-3 pb-2 pt-2">
<SubagentCard
<SubagentChip
sessionId={item.subagentSessionId ?? ""}
model={item.subagent}
running={!item.outputComplete}
agentId={agentIdFromRunSubagentArgs(item.argumentsText)}
ctx={ctx}
/>
</div>