import type { AgentRunNode, AgentRunStatus } from "@umt/contracts"; import { ChevronDown, CornerLeftUp, ExternalLink, GitBranch, LoaderCircle } from "lucide-react"; import { createContext, type ReactNode, useContext, useEffect, useId, useState } from "react"; import { Link } from "react-router"; import { agentMockEnabled, isRunActive, useAgentRun } from "~/api/agent.ts"; import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx"; import { LoadingState } from "~/components/console/empty-state.tsx"; import { formatUsd } from "~/components/schedules/labels.ts"; import { cn } from "~/lib/utils.ts"; import { AgentConversation, RunStatusBadge } from "./agent-conversation.tsx"; import { shortGoalTitle } from "./agent-store.ts"; import { durationLabel, runningText, type SubagentState, subagentElapsed, subagentResultLine, subagentState, } from "./conversation-model.ts"; import { humanizeReport } from "./report-humanize.ts"; import { type SubagentView, useAgentStream } from "./use-agent-stream.ts"; // 子会话(agent.spawn)在父会话里的样子:派出它的位置一张卡片,实时显示任务、状态、当前步骤、时长、调用次数与费用; // 展开后在卡片里看子会话的过程(同一套会话渲染,缩小一档);可在整页 / 抽屉里打开子会话。 // 数据来自父会话事件流里镜像的 subagent_* 事件,快照里的子会话(状态、用量)校正。 const href = (id: string) => `/console/agent/${id}${agentMockEnabled() ? "?mock=1" : ""}`; /** 抽屉里提供:点父 / 子会话在抽屉内打开,不跳页 */ export const AgentRunOpenContext = createContext<((runId: string) => void) | null>(null); /** 会话快照里的子孙会话(按 ID):卡片用它校正状态与用量 */ export const SubagentNodesContext = createContext>(new Map()); const STATE_STATUS: Record = { running: "running", waiting: "waiting", done: "succeeded", failed: "failed", stopped: "cancelled", }; function RunLink({ id, className, children }: { id: string; className?: string; children: ReactNode }) { const open = useContext(AgentRunOpenContext); const cls = cn( "cursor-pointer rounded-tight outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring", className, ); if (open) return ( ); return ( {children} ); } /** 每秒走一次的时钟(只在运行中的卡片上开) */ function useNow(on: boolean): number { const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!on) return; const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, [on]); return now; } /** 子会话展开后的过程:读子会话自己的事件流,用同一套会话渲染(紧凑、不重复目标) */ function SubagentSteps({ childId, active }: { childId: string; active: boolean }) { const run = useAgentRun(childId); const live = useAgentStream(childId, active || !run.data); const nodes = new Map((run.data?.children ?? []).map((c) => [c.id, c] as const)); if (!run.data) return ; return ( ); } /** * 会话流里的子会话卡片。运行中:「正在搜资源… · 12 秒 · 3 次调用」,每秒更新; * 结束后:状态 + 汇报第一句 + 用时、调用次数、费用。展开看子会话的过程,「打开」进子会话。 */ export function SubagentCard({ view }: { view: SubagentView }) { const id = useId(); const node = useContext(SubagentNodesContext).get(view.childRunId); const state = subagentState(view, node?.status ?? null); const live = state === "running" || state === "waiting"; const now = useNow(live); const [open, setOpen] = useState(false); const elapsed = subagentElapsed(view, now, node?.finishedAt ?? null); const calls = Math.max(view.toolCalls, node?.toolCallCount ?? 0); const cost = Math.max(view.usage?.costUsd ?? 0, node?.usage.costUsd ?? 0); const result = subagentResultLine(view.summary ?? node?.summary); const line = state === "running" ? view.currentTool ? `${runningText({ capabilityId: view.currentTool })}…` : "思考中…" : state === "waiting" ? "等待中" : result ? humanizeReport(result).text : state === "done" ? "已完成" : state === "stopped" ? "已停止" : "没有完成"; const meta = [durationLabel(elapsed), `${calls} 次调用`, cost ? formatUsd(cost) : null].filter(Boolean).join(" · "); return (
子会话 {meta}

{shortGoalTitle(view.goal, 120)}

{state === "running" ? ( ) : null} {line}

打开会话
{open ? : null}
); } /** 子会话的运行概况:「2 个子会话运行中」/「3 个子会话」;点一下滚到第一张运行中(没有就第一张)的卡片 */ export function subagentChipText(states: SubagentState[]): string | null { if (!states.length) return null; const running = states.filter((s) => s === "running" || s === "waiting").length; return running ? `${running} 个子会话运行中` : `${states.length} 个子会话`; } export function SubagentChip({ views, className }: { views: SubagentView[]; className?: string }) { const nodes = useContext(SubagentNodesContext); const states = views.map((v) => subagentState(v, nodes.get(v.childRunId)?.status ?? null)); const text = subagentChipText(states); if (!text) return null; const running = states.some((s) => s === "running" || s === "waiting"); const target = views[states.findIndex((s) => s === "running" || s === "waiting")] ?? views[0]; return ( ); } /** 子会话顶部一行:「父会话:xxx」,点了回到父会话 */ export function ParentRunLink({ parent }: { parent: Pick }) { return (

父会话 {shortGoalTitle(parent.goal)} {isRunActive(parent.status) ? : null}

); }