import { useState } from "react"; import { useAppStore } from "../../store/useAppStore"; import type { AgentInfo, AgentStatus, FlowGraph } from "../../lib/types"; import { Notice, PageHeader } from "../components"; import { Button, EmptyState, Surface, Tag, type TagTone } from "../../components/primitives"; import { sendUp } from "../../lib/ws"; import { useAuditStore } from "../audit"; import { Icon } from "../../components/Icon"; import { FlowGraphView } from "./FlowGraph"; import { FlowSnapshotHint } from "./flow/FlowSnapshotHint"; import { useFlowSnapshot, type FlowGraphSource } from "./flow/useFlowSnapshot"; import { MAIN_SESSION_ID } from "../../store/sessionCache"; const statusTone: Record = { idle: { label: "空闲", tone: "neutral" }, running: { label: "运行中", tone: "accent" }, blocked: { label: "阻塞", tone: "warn" }, done: { label: "完成", tone: "ok" }, error: { label: "错误", tone: "err" }, }; function AgentCard({ agent }: { agent: AgentInfo }): React.ReactNode { const meta = statusTone[agent.status] ?? statusTone.idle; const log = useAuditStore((s) => s.log); const [hint, setHint] = useState(null); const query = () => { const ok = sendUp({ type: "task_output_query", data: { taskId: agent.id } }); log(`agent.query:${agent.id}`, ok ? "ok" : "error"); setHint(ok ? "已请求上下文,输出见下方「任务日志」。" : "WS 未连接,无法查询。"); }; const kill = () => { log(`agent.kill:${agent.id}`, "error"); setHint("终止子代理需后端 M10+ 能力接入,当前为只读展示,未执行任何操作。"); }; return (
{agent.name} {agent.role ? {agent.role} : null} {meta.label}
{typeof agent.progress === "number" ? (
) : null} {agent.thinking ? (
{agent.thinking}
) : null} {agent.tools?.map((t, i) => (
{t.name}{t.ok === false ? : ""}
))} {agent.message ?
{agent.message}
: null}
{hint ?
{hint}
: null} ); } export function AgentsModule(): React.ReactNode { const agents = useAppStore((s) => s.agents); const activeSessionId = useAppStore((s) => s.activeSessionId); // 与 Flow 页同一条补拉(契约 v0.71):本页读的也是「激活会话」的图, // 刷新过页面后实时帧已经错过,不补拉就永远停在空态。 const graphSource = useFlowSnapshot(activeSessionId || MAIN_SESSION_ID); const flowGraph = useAppStore((s) => s.flowGraph); const taskLogs = useAppStore((s) => s.taskLogs); const clearTaskLog = useAppStore((s) => s.clearTaskLog); const log = useAuditStore((s) => s.log); const [tab, setTab] = useState<"main" | "sub">("main"); const [respawnHint, setRespawnHint] = useState(false); const subAgents = agents.filter((a) => a.id !== "main"); const list = tab === "main" ? agents.filter((a) => a.id === "main") : subAgents; return (
{ log("agent.respawn", "error"); setRespawnHint(true); }} > 重派 } /> {respawnHint ? ( 子代理重派需后端 M10+ 能力接入,当前未启用;可先通过「上下文查询」与任务日志查看已运行子代理。 ) : null}
setTab("main")}>主会话 setTab("sub")}>子代理 ({subAgents.length})
{list.length === 0 ? ( } title={tab === "main" ? "无主会话 Agent" : "暂无子代理"} /> ) : (
{list.map((a) => ( ))}
)}

Agent 消息流 / 后台任务日志

{Object.keys(taskLogs).length === 0 ? (
暂无任务日志。
) : ( Object.entries(taskLogs).map(([id, text]) => (
{id}
{text}
)) )} 任务日志实时来自 task_output 事件;「上下文查询」通过 task_output_query 上行帧查询真实输出(已接后端)。终止 / 重派需后端 M10+ 提供,当前为只读展示。
); } /** 流程图这一块(有图就画、没图就诚实空态)。props 驱动(不读 store), * 因此可被真实渲染测试直接喂两种输入(见 flowSnapshot.render.test.ts 头注释)。 * * 空态文案里必须说明**两条**来源都查过(实时帧 + 最近一轮快照,契约 v0.71): * 否则用户分不清「本会话没跑过带 span 的轮次」与「图源没接线」。 */ export function AgentsFlowBlock({ graph, sessionId, source, }: { graph: FlowGraph | null; sessionId: string; source: FlowGraphSource; }): React.ReactNode { if (!graph) { return ( 暂无流程图数据 —— 本会话还没有跑过带 span 的轮次(实时帧与「最近一轮快照」都为空)。发起一次会话后此处会显示链路图。 ); } return ( {/* 与后台思考的图块同一档:横向渲染 + 最新在最左(FV0), 容器默认露出最左边 ⇒ 打开就先看到最新那拍。 */} ); } function TabPill({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }): React.ReactNode { return ( ); }