import { useQueryClient } from "@tanstack/react-query"; import { type AgentRunStatus, EventType } from "@umt/contracts"; import { ChevronDown, FlaskConical, ListChecks, RefreshCw, WifiOff } from "lucide-react"; import { useId, useRef, useState } from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; import { type AgentSessionOptions, agentKeys, agentMockEnabled, isRunActive, type RunSnapshot, TRIGGER_LABEL, useAbortAgentRun, useAgentRun, useAppendAgentMessage, useCreateAgentRun, useRunApprovals, useRunInvocationAudit, } from "~/api/agent.ts"; import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx"; import { MessageScroller } from "~/components/agentui/message-scroller.tsx"; import { PromptInput } from "~/components/agentui/prompt-input.tsx"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { TechInfo } from "~/components/console/tech-info.tsx"; import { formatUsd } from "~/components/schedules/labels.ts"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { useSse, useSseStatus } from "~/lib/sse.ts"; import { formatTokens } from "~/lib/tokens.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import { AgentConversation, ApprovalItem, ResultLine, RunAnnouncer, RunStatusBadge, runPlanIds, } from "./agent-conversation.tsx"; import { RunPlan } from "./agent-interaction.tsx"; import { shortGoalTitle } from "./agent-store.ts"; import { SubagentTree } from "./agent-subagent-tree.tsx"; import { callCounts, sessionStatsParts, stepProgress } from "./conversation-model.ts"; import { NEW_RUN_COMMANDS, PROPOSE_FIRST, PROPOSE_FIRST_MESSAGE, parseCommand, RERUN, RUN_COMMANDS, } from "./slash-commands.ts"; import { type AgentStreamState, useAgentStream } from "./use-agent-stream.ts"; /** 新目标的提交内容:`/先给方案` 开头 → proposeFirst(只作用于这一次) */ export function goalFromInput(text: string): { goal: string; opts: AgentSessionOptions } | null { const { command, rest } = parseCommand(text, NEW_RUN_COMMANDS); if (!rest) return null; return { goal: rest, opts: { proposeFirst: command === PROPOSE_FIRST.name } }; } /** * 新目标输入:只有输入框和发送;`/先给方案` 让这一次先给方案。空态下面 3 个快捷目标,点一下填进输入框 * (以空格或冒号结尾的是半句,光标停在末尾等补完)。上下文(当前页面的对象)显示成名称标签,可以去掉。 */ export function GoalComposer({ onSubmit, pending, context, onClearContext, defaultGoal = "", suggestions, }: { onSubmit: (goal: string, opts: AgentSessionOptions) => void; pending: boolean; /** 上下文标签(对象名称) */ context?: string | null; onClearContext?: () => void; defaultGoal?: string; /** 快捷目标:输入框为空时显示 */ suggestions?: string[]; }) { const [goal, setGoal] = useState(defaultGoal); const box = useRef(null); const fill = (text: string) => { setGoal(text); requestAnimationFrame(() => { const ta = box.current?.querySelector("textarea"); if (!ta) return; ta.focus(); ta.setSelectionRange(text.length, text.length); }); }; const submit = (text: string) => { const g = goalFromInput(text); if (!g) { toast.error("在指令后面写上目标"); return; } onSubmit(g.goal, g.opts); }; return (
{suggestions?.length && !goal.trim() ? (
    {suggestions.slice(0, 3).map((s) => (
  • ))}
) : null}
); } /** * 顶部概况条:一行「步骤 4/5 · 当前:核对下载任务」;展开看步骤清单、子会话和技术信息(模型、预算、用量、ID)。 * 不 sticky、不压在会话上;没有步骤和子会话时只在运行中显示状态。 */ /** * 会话统计(AG-4):概况条上方恒定一行小字——工具调用、MCP、已用 token、距上限、费用; * 运行中随事件流实时更新,结束后固定为快照里的最终值。 */ export function RunStatsLine({ snapshot, live, className, }: { snapshot: RunSnapshot; live: AgentStreamState; className?: string; }) { const r = snapshot.run; const u = { ...r.usage, ...(live.usage ?? {}) }; const n = callCounts(live.items.flatMap((i) => (i.kind === "tool" ? [i.call] : []))); const parts = sessionStatsParts( { toolCalls: Math.max(r.toolCallCount - n.mcp, n.tools), mcpCalls: n.mcp, totalTokens: u.totalTokens ?? null, maxTokens: r.budget.maxTokens ?? null, costUsd: u.costUsd ?? null, }, { tokens: formatTokens, usd: formatUsd }, ); return (
{/* 窄屏折行,不截断(费用在最后) */}

{parts.map((t, i) => ( {i ? "· " : ""} {t} ))}

); } export function RunOverviewBar({ snapshot, status, live, active, className, }: { snapshot: RunSnapshot; status: AgentRunStatus; live: AgentStreamState; active: boolean; className?: string; }) { const id = useId(); const [open, setOpen] = useState(false); const r = snapshot.run; const steps = live.plan?.steps ?? []; const progress = stepProgress(steps); const failed = progress.failedStep; const family = (snapshot.children?.length ?? 0) > 0 || !!snapshot.parent; if (!steps.length && !family && !isRunActive(status)) return null; const u = { ...r.usage, ...(live.usage ?? {}) }; return (
{steps.length ? (
) : null}

{TRIGGER_LABEL[r.trigger]} · {formatDateTime(r.createdAt)}

); } /** * 一次运行:顶部概况条 → 会话流(统一最大宽度)→ 底部输入区(只有输入框和发送 / 停止)。 * 输入区的 `/重跑` 用同一个目标新开;`/先给方案` 让智能体先给方案。compact 用于抽屉。 */ export function AgentRunPanel({ runId, compact = false, className, onCreated, }: { runId: string; compact?: boolean; className?: string; /** /重跑 之后:缺省跳到新会话页;抽屉里改成在抽屉内打开 */ onCreated?: (runId: string) => void; }) { const run = useAgentRun(runId); const active = isRunActive(run.data?.run.status); const live = useAgentStream(runId, active || !run.data); const abort = useAbortAgentRun(); const append = useAppendAgentMessage(); const create = useCreateAgentRun(); const navigate = useNavigate(); const [msg, setMsg] = useState(""); const [confirmStop, setConfirmStop] = useState(false); const sseStatus = useSseStatus(); const qc = useQueryClient(); const toolCount = live.items.filter((i) => i.kind === "tool" && i.call.status !== "running").length; const audit = useRunInvocationAudit(runId, toolCount > 0, toolCount); const approvals = useRunApprovals(runPlanIds(live.items, run.data?.report ?? null)); // 会话内审批卡已在对应步骤处显示,这里只补本次运行计划上还在等的其它审批 const inline = new Set(live.items.flatMap((i) => (i.kind === "approval" ? [i.view.approvalId] : []))); // run 状态变化(queued → running → 终态)经 SSE 推送:到达即刷新快照,不等 3 秒轮询 const related = new Set([ runId, ...(run.data?.children ?? []).map((c) => c.id), ...(run.data?.parent ? [run.data.parent.id] : []), ]); useSse( [EventType.agentRunStatus, EventType.agentToolCall], (e) => { const p = e.payload as { runId?: string; capabilityId?: string } | null; if (!p?.runId) return; const hit = e.type === EventType.agentRunStatus ? related.has(p.runId) : p.runId === runId && p.capabilityId === "agent.spawn"; if (hit) void qc.invalidateQueries({ queryKey: agentKeys.run(runId) }); }, !agentMockEnabled(), ); if (run.isPending) return ; if (run.error || !run.data) return void run.refetch()} />; const { run: r, report, messages } = run.data; const finished = !isRunActive(r.status) || live.finished; const streaming = active && !live.finished; const shownStatus: AgentRunStatus = // 事件流已结束但快照还没刷到终态时按运行中显示;waiting(等待下载/入库核验)照实显示 (live.finished && (r.status === "queued" || r.status === "running")) || (r.status === "queued" && live.events.length > 0) ? "running" : r.status; const ended = !isRunActive(r.status); const created = onCreated ?? ((id: string) => navigate(`/console/agent/${id}${agentMockEnabled() ? "?mock=1" : ""}`)); const send = (text: string) => { const { command, rest } = parseCommand(text, RUN_COMMANDS); if (command === RERUN.name) { create.mutate( { goal: r.goal, proposeFirst: r.scope.proposeFirst === true }, { onSuccess: (n) => { setMsg(""); created(n.id); }, onError: (e) => toast.error(errorMessage(e)), }, ); return; } const body = command === PROPOSE_FIRST.name ? `${PROPOSE_FIRST_MESSAGE}${rest ? `\n${rest}` : ""}` : text; append.mutate( { id: r.id, text: body }, { onSuccess: () => { setMsg(""); void run.refetch(); }, onError: (err) => toast.error(errorMessage(err)), }, ); }; const column = "mx-auto w-full max-w-[88ch]"; return (
{agentMockEnabled() && !compact ? (
演示数据
) : null} {streaming && sseStatus === "reconnecting" ? (
实时连接中断,正在重连。
) : null} {live.error ? (
事件读取失败:{live.error}
) : null} [c.id, shortGoalTitle(c.goal, 24)] as [string, string]), ...(run.data.parent ? [[run.data.parent.id, shortGoalTitle(run.data.parent.goal, 24)] as [string, string]] : []), ]} audit={audit.data} userMessages={messages.map((m) => ({ at: m.createdAt, text: m.text, pending: m.status === "pending" }))} after={ <> {approvals.items .filter((a) => !inline.has(a.id) && a.status === "pending") .map((a) => ( ))} {finished ? ( c.parentRunId === r.id).length} showCost={false} /> ) : null} } />
setConfirmStop(true)} stopLabel={abort.isPending ? "停止中…" : "停止"} minRows={1} maxRows={6} maxLength={8000} aria-label="发消息" placeholder={ended ? "接着说,输入 / 看指令" : "补充或纠正"} commands={RUN_COMMANDS} />
abort.mutateAsync(r.id).then( () => toast.success("已请求停止"), (e) => { toast.error(errorMessage(e)); throw e; }, ) } />
); }