import { type AgentPlanStep, CAPABILITY_CATALOG, type KnownCapabilityId, type RiskLevel } from "@umt/contracts"; import { ListChecks, MessageCircleQuestion, Star } from "lucide-react"; import { useId, useState } from "react"; import { toast } from "sonner"; import { useApproval, useChooseOption, useDecideApproval, useIsAdminSession } from "~/api/agent.ts"; import { ApprovalCard, type ApprovalCardStatus } from "~/components/agentui/approval-card.tsx"; import { type TodoItem, type TodoItemStatus, TodoList } from "~/components/agentui/todo-list.tsx"; import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import type { ApprovalView, OptionsView } from "./use-agent-stream.ts"; // 自动化优先的会话交互(AG-2):步骤清单(agent.plan)、方案选择(agent.propose_options)、步骤处的审批卡。 // 事件形状见 packages/contracts/src/agent-events.ts;服务端逻辑见 apps/server/src/modules/agent/interaction.ts。 const STEP_STATUS: Record = { pending: "pending", running: "in-progress", done: "completed", skipped: "skipped", failed: "error", }; export function planTodoItems(steps: AgentPlanStep[]): TodoItem[] { return steps.map((s) => ({ id: s.id, title: s.title, status: STEP_STATUS[s.status], ...(s.note ? { detail: {s.note} } : {}), })); } /** 步骤清单:会话页右侧栏完整列表(实时勾选);compact 压缩成一行进度 */ export function RunPlan({ plan, active, compact = false, fill = false, }: { plan: { steps: AgentPlanStep[]; revision: number } | null; active: boolean; compact?: boolean; /** 侧栏 / 展开的概况条里:不限高、不自动收起 */ fill?: boolean; }) { if (!plan?.steps.length) return null; const done = plan.steps.filter((s) => s.status === "done" || s.status === "skipped").length; const current = plan.steps.find((s) => s.status === "running") ?? plan.steps.find((s) => s.status === "pending"); const failed = plan.steps.some((s) => s.status === "failed"); if (compact) { const pct = Math.round((done / plan.steps.length) * 100); return (
步骤 {done}/{plan.steps.length} {current ? `当前:${current.title}` : failed ? "有步骤失败" : "全部完成"}
); } return ( ); } // ── 方案选择 ── const NONE = "__none__"; /** agent.propose_options 的选择卡:单选一个方案,或"都不要"并写原因;选定后会话继续 */ export function OptionsCard({ runId, view, waiting }: { runId: string; view: OptionsView; waiting: boolean }) { const name = useId(); const choose = useChooseOption(); const [picked, setPicked] = useState(view.recommendedId ?? ""); const [reason, setReason] = useState(""); const [sent, setSent] = useState<{ optionId: string | null; reason: string } | null>(null); const resolved = view.resolved; // 已作答但会话还没恢复(事件里没有 options_resolved):按本地提交结果显示 const closed = !!resolved || !!sent || !waiting; const chosenId = resolved ? resolved.choice : sent ? sent.optionId : null; const submit = () => { const optionId = picked === NONE ? null : picked; if (!picked) return; if (optionId === null && !reason.trim()) { toast.error("写一句不要的原因,Agent 按它调整"); return; } choose.mutate( { runId, decisionId: view.decisionId, optionId, ...(optionId === null ? { reason: reason.trim() } : {}) }, { onSuccess: () => { setSent({ optionId, reason: reason.trim() }); toast.success(optionId ? "已选定,Agent 继续执行" : "已退回,Agent 调整方案"); }, onError: (e) => toast.error(errorMessage(e)), }, ); }; const outcome = resolved ? resolved.outcome === "chosen" ? `${resolved.by === "timeout" ? "超时自动选择" : "已选择"}:${view.options.find((o) => o.id === resolved.choice)?.title ?? resolved.choice}` : resolved.outcome === "stop" ? "超时未选,按设置停止" : `都不要${resolved.reason ? `:${resolved.reason}` : ""}` : sent ? sent.optionId ? `已选择:${view.options.find((o) => o.id === sent.optionId)?.title ?? sent.optionId},等待 Agent 继续` : `都不要:${sent.reason},等待 Agent 调整` : !waiting ? "会话已不在等待这个选择" : null; return (

{view.question}

{view.question} {view.options.map((o) => { const selected = closed ? chosenId === o.id : picked === o.id; return ( ); })} {closed ? null : ( )}
{!closed && picked === NONE ? (