import { useQueryClient } from "@tanstack/react-query"; import { EventType } from "@umt/contracts"; import { FlaskConical, Play, RefreshCw, Sparkles, WifiOff } from "lucide-react"; import { useRef, useState } from "react"; import { toast } from "sonner"; import { type AgentMode, agentKeys, agentMockEnabled, isRunActive, useAbortAgentRun, useAgentRun, useAppendAgentMessage, useCreateAgentRun, useRunApprovals, useRunInvocationAudit, } from "~/api/agent.ts"; 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 { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { useSse, useSseStatus } from "~/lib/sse.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import { AgentConversation, ApprovalItem, BudgetBar, FinalReport, PlanSteps, RunAnnouncer, RunStatusBadge, runPlanIds, } from "./agent-conversation.tsx"; import { SubagentTree } from "./agent-subagent-tree.tsx"; import { useAgentStream } from "./use-agent-stream.ts"; const TRIGGER_LABEL = { user: "用户发起", event: "事件触发", schedule: "定时", subagent: "子会话" } as const; export const MODE_HELP: Record = { execute: { label: "授权内执行", help: "未授权的动作会被服务端拒绝并在对话里标出。", }, plan_only: { label: "只要方案", help: "只调查、出方案,不执行写操作。之后可按方案执行。", }, }; /** 运行模式分段切换 */ export function ModeSwitch({ mode, onChange }: { mode: AgentMode; onChange: (m: AgentMode) => void }) { return (
{(["execute", "plan_only"] as const).map((v) => ( ))}
); } /** 新目标输入:execute / plan_only 切换 + 自增高输入框(Enter 发送,Shift+Enter 换行) */ export function GoalComposer({ onSubmit, pending, prefix, defaultGoal = "", compact = false, suggestions, }: { onSubmit: (goal: string, mode: AgentMode) => void; pending: boolean; /** 上下文前缀(只读展示,提交时拼在目标前) */ prefix?: string; defaultGoal?: string; compact?: boolean; /** 快捷目标:点一下填入输入框(不直接提交),光标停在末尾 */ suggestions?: string[]; }) { const [goal, setGoal] = useState(defaultGoal); const [mode, setMode] = useState("execute"); 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); }); }; return (
onSubmit(g, mode)} pending={pending} minRows={compact ? 2 : 3} maxRows={10} maxLength={4000} aria-label="目标" placeholder="例如:把 沙丘:第二部 的 4K 收进来" submitLabel={pending ? "提交中" : "开始"} header={ prefix ? (

{prefix}

) : null } toolbar={} />

{MODE_HELP[mode].help} Enter 发送,Shift+Enter 换行。

{suggestions?.length ? (

常用目标

    {suggestions.map((s) => (
  • ))}
) : null}
); } /** 一次运行:头部状态 + 预算条 + (只要方案的步骤清单)+ 流式对话 + 审批 + 最终汇报 + 输入框 */ export function AgentRunPanel({ runId, compact = false, className, onRunCreated, }: { runId: string; compact?: boolean; className?: string; /** 从本会话派生了新运行(按方案执行)时 */ onRunCreated?: (id: 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 [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)); // run 状态变化(queued → running → 终态)经 SSE 推送:到达即刷新快照,不等 3 秒轮询 // 子会话:本会话收到子会话提示(agent.tool_call / agent.spawn)或任一子会话 / 父会话状态变化时刷新会话树 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 mode: AgentMode = (r.mode ?? (r.scope.mode as AgentMode | undefined)) || "execute"; const shownStatus = // 事件流已结束但快照还没刷到终态时按运行中显示;waiting(等待下载/入库核验)照实显示 (live.finished && (r.status === "queued" || r.status === "running")) || (r.status === "queued" && live.events.length > 0) ? "running" : r.status; const send = (text: string) => append.mutate( { id: r.id, text }, { onSuccess: () => { setMsg(""); void run.refetch(); }, onError: (err) => toast.error(errorMessage(err)), }, ); const executePlan = () => create.mutate( { goal: `${r.goal}\n\n(按会话 ${r.id.slice(0, 8)} 的方案执行)`, mode: "execute" }, { onSuccess: (n) => onRunCreated?.(n.id), onError: (e) => toast.error(errorMessage(e)), }, ); return (
{MODE_HELP[mode].label} {TRIGGER_LABEL[r.trigger]} · {formatDateTime(r.createdAt)} {r.model ? ` · ${r.provider ?? ""}/${r.model}` : ""} {agentMockEnabled() ? ( mock 数据 ) : null}
{mode === "plan_only" ? : null} {streaming && sseStatus === "reconnecting" ? (
实时连接中断,正在重连。期间每 2.5 秒轮询一次。
) : null} {live.error ? (
事件读取失败:{live.error}
) : null} ({ at: m.createdAt, text: m.text, pending: m.status === "pending" }))} after={ <> {approvals.items.map((a) => ( ))} {finished ? : null} } />
setConfirmStop(true)} stopLabel={abort.isPending ? "停止中…" : "停止"} minRows={1} maxRows={6} maxLength={8000} aria-label="追加消息" placeholder={streaming ? "补充或纠正(运行中也可追加)" : "追加消息"} toolbar={ mode === "plan_only" && finished ? ( ) : ( 本会话:{MODE_HELP[mode].label} ) } />

{mode === "plan_only" && finished ? "「按方案执行」会新开一次授权内执行的运行,仍受服务端授权校验。" : MODE_HELP[mode].help}{" "} Enter 发送,Shift+Enter 换行{streaming ? ",停止 = 中止本次运行" : ""}。

abort.mutateAsync(r.id).then( () => toast.success("已请求停止"), (e) => { toast.error(errorMessage(e)); throw e; }, ) } />
); }