import type { AgentFamilyUsage, AgentRunNode } from "@umt/contracts"; import { ChevronRight, CornerLeftUp, GitBranch } from "lucide-react"; import { createContext, type ReactNode, useContext, useState } from "react"; import { Link } from "react-router"; import { agentMockEnabled, type RunSnapshot } from "~/api/agent.ts"; import { formatTokens } from "~/lib/tokens.ts"; import { cn } from "~/lib/utils.ts"; import { RunStatusBadge } from "./agent-conversation.tsx"; import { shortGoalTitle } from "./agent-store.ts"; // 子代理(agent.spawn):会话页显示父会话链接 + 继承信息,以及子会话树(可展开、可跳转)与会话树合计用量。 // 未派生过子会话、也不是子会话时不渲染(不显示空的多 Agent 看板)。 const tokens = (n: number | undefined) => formatTokens(n); export function usageLine(u: { toolCalls: number; totalTokens?: number; costUsd?: number }) { const parts = [`${u.toolCalls} 次调用`, `${tokens(u.totalTokens)} token`]; if (u.costUsd) parts.push(`$${u.costUsd.toFixed(3)}`); return parts.join(" · "); } const href = (id: string) => `/console/agent/${id}${agentMockEnabled() ? "?mock=1" : ""}`; /** 抽屉里提供:点父/子会话在抽屉内打开,不跳页 */ export const AgentRunOpenContext = createContext<((runId: string) => void) | null>(null); const LINK_CLASS = "line-clamp-2 cursor-pointer rounded-tight text-left text-caption break-words text-foreground outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"; function RunLink({ id, children }: { id: string; children: ReactNode }) { const open = useContext(AgentRunOpenContext); if (open) return ( open(id)} className={LINK_CLASS}> {children} ); return ( {children} ); } function budgetText(b: Record) { const parts: string[] = []; if (b.maxToolCalls !== undefined) parts.push(`${b.maxToolCalls} 次调用`); if (b.maxTokens !== undefined) parts.push(`${tokens(b.maxTokens)} token`); if (b.maxDurationSec !== undefined) parts.push(`${b.maxDurationSec}s`); if (b.maxCostUsd !== undefined) parts.push(`$${b.maxCostUsd}`); return parts.length ? parts.join(" · ") : "沿用设置"; } function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map; depth: number }) { const kids = byParent.get(node.id) ?? []; const [open, setOpen] = useState(true); return ( {kids.length ? ( setOpen(!open)} className="mt-0.5 cursor-pointer rounded-tight text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring" > ) : ( )} {shortGoalTitle(node.goal)} {usageLine({ toolCalls: node.toolCallCount, totalTokens: node.usage.totalTokens, costUsd: node.usage.costUsd, })} {node.spawnReason ? ( 理由:{node.spawnReason} ) : null} {kids.length && open ? ( 2 && "ml-1")}> {kids.map((k) => ( ))} ) : null} ); } /** 子会话树 + 父会话链接;两者都没有时返回 null */ export function SubagentTree({ snapshot, className }: { snapshot: RunSnapshot; className?: string }) { const { run, parent, children = [], familyUsage } = snapshot; if (!parent && children.length === 0) return null; const byParent = new Map(); for (const c of children) { const k = c.parentRunId ?? ""; byParent.set(k, [...(byParent.get(k) ?? []), c]); } const direct = byParent.get(run.id) ?? []; const caps = ((run.scope as { capabilities?: string[] }).capabilities ?? []).length; return ( {parent ? ( 父会话 {shortGoalTitle(parent.goal)} 第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "} {budgetText(run.budget as Record)} {run.spawnReason ? ` · 理由:${run.spawnReason}` : ""} ) : null} {direct.length ? ( 子会话 {children.length} 个{familyUsage ? : null} {direct.map((c) => ( ))} ) : null} ); } function FamilyUsage({ usage }: { usage: AgentFamilyUsage }) { return 会话树合计 {usageLine(usage)}; }
理由:{node.spawnReason}
父会话
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "} {budgetText(run.budget as Record)} {run.spawnReason ? ` · 理由:${run.spawnReason}` : ""}
子会话 {children.length} 个{familyUsage ? : null}