import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { App, Button, Input, Segmented, Tooltip } from "antd"; import copyToClipboard from "copy-to-clipboard"; import { Copy, FolderOpen, History, KeyRound, Link2, LoaderCircle, PlugZap, Plus, RefreshCw, Square, Terminal, Trash2 } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { randomId } from "@/lib/utils"; import { useThemeStore } from "@/stores/use-theme-store"; import { useUserStore } from "@/stores/use-user-store"; import { useShallow } from "zustand/react/shallow"; import { useAgentStore, type AgentAttachment, type AgentCanvasContext, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary } from "@/stores/use-agent-store"; import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops"; import { isSiteTool, runSiteTool, SITE_TOOL_LABELS } from "@/lib/agent/agent-site-tools"; import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCard, AgentWorkingMessage, type CanvasAgentChatAttachment } from "./canvas-agent-chat-ui"; const MAX_ATTACHMENTS = 6; const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024; const DEFAULT_AGENT_URL = "http://127.0.0.1:17371"; const AGENT_CONNECT_STEPS = [ { title: "方式一:在 Codex 中使用插件", text: "在 Codex app 安装 Infinite Canvas 插件后,通过插件启动画布,插件会自动启动本地 Agent 并带上连接信息。" }, { title: "方式二:直接运行 Agent", text: "不使用 Codex 插件时,在终端运行下面命令,再回到网页里连接或手动填入 Local URL 和 Connect token。", command: "npx -y @basketikun/canvas-agent" }, ]; const AGENT_PLUGIN_REMOVE_COMMAND = "codex plugin remove infinite-canvas"; const AGENT_MCP_REMOVE_COMMAND = "codex mcp remove infinite-canvas"; type AgentEventPayload = { agent?: string; type?: string; thread_id?: string; item?: AgentEventItem; error?: { message?: string }; message?: string; usage?: Record; }; type AgentEventItem = { id?: string; type?: string; text?: unknown; message?: unknown; server?: string; tool?: string; status?: string; arguments?: unknown; result?: unknown; error?: { message?: string } }; type AgentLogContext = { endpoint: string; connected: boolean; enabled: boolean; activity: string; waiting: boolean; sending: boolean; messages: number; pendingTool?: string }; type AgentWorkspace = { workspacePath: string; activeThreadId?: string }; type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: AgentThreadSummary[] }; type AgentThreadResponse = { ok?: boolean; workspace?: AgentWorkspace; thread?: AgentThreadSummary; messages?: AgentChatItem[] }; type AgentConfigResponse = { ok?: boolean; url?: string; token?: string; hasToken?: boolean }; export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { embedded?: boolean; headless?: boolean; autoConnect?: boolean }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const user = useUserStore((state) => state.user); const { message, modal } = App.useApp(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); // 逐字段 selector + useShallow:只有这些字段变化时才重渲染。 // 注意:canvasContext 不在此订阅内 —— 它在拖拽/resize 时会被 project 每帧写入, // 但面板只在 ref 同步与防抖 postState 中用到它、渲染层从不读它。若把它放进订阅, // 面板会随画布每帧重渲染(性能问题,也是 #185 崩溃的放大器)。改为下方 subscribe 命令式监听。 const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, messages, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, activity, connectError, pendingTool } = useAgentStore( useShallow((state) => ({ width: state.width, url: state.url, token: state.token, connected: state.connected, enabled: state.enabled, prompt: state.prompt, attachments: state.attachments, sending: state.sending, waiting: state.waiting, messages: state.messages, eventLogs: state.eventLogs, threads: state.threads, activeThreadId: state.activeThreadId, workspacePath: state.workspacePath, loadingThreads: state.loadingThreads, activeTab: state.activeTab, confirmTools: state.confirmTools, activity: state.activity, connectError: state.connectError, pendingTool: state.pendingTool, })), ); const setAgentState = useAgentStore((state) => state.setAgentState); const pushMessage = useAgentStore((state) => state.addMessage); const pushEventLog = useAgentStore((state) => state.addEventLog); const clearEventLogs = useAgentStore((state) => state.clearEventLogs); const listRef = useRef(null); const canvasContextRef = useRef(useAgentStore.getState().canvasContext); const confirmToolsRef = useRef(confirmTools); const pendingToolRef = useRef(null); const autoConnectRef = useRef(false); const connectedRef = useRef(false); const errorLoggedRef = useRef(false); const attachmentUrlsRef = useRef(new Set()); const clientIdRef = useRef(randomId()); const endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]); const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken"); const loadThreads = useCallback(async () => { if (!connectedRef.current && !useAgentStore.getState().connected) return; setAgentState({ loadingThreads: true }); try { const data = await fetchAgentJson(endpoint, token, `/agent/codex/threads`); const nextThreadId = data.workspace?.activeThreadId || ""; setAgentState({ threads: data.data || [], workspacePath: data.workspace?.workspacePath || "", activeThreadId: nextThreadId, messages: [], }); if (nextThreadId) { const thread = await fetchAgentJson(endpoint, token, `/agent/codex/threads/${encodeURIComponent(nextThreadId)}`); setAgentState({ messages: normalizeHistoryMessages(thread.messages || []) }); } } catch (error) { addEventLog("读取历史失败", error); } finally { setAgentState({ loadingThreads: false }); } }, [endpoint, setAgentState, token]); // canvasContext 命令式订阅:保持 ref 最新,并在快照变化时防抖上报,全程不触发面板重渲染。 useEffect(() => { let timer: ReturnType | null = null; const unsubscribe = useAgentStore.subscribe((state) => { if (state.canvasContext === canvasContextRef.current) return; canvasContextRef.current = state.canvasContext; if (!useAgentStore.getState().connected) return; if (timer) clearTimeout(timer); timer = setTimeout(() => void postState(endpoint, token, clientIdRef.current, canvasContextRef.current?.snapshot || null), 300); }); return () => { unsubscribe(); if (timer) clearTimeout(timer); }; }, [endpoint, token]); useEffect(() => { confirmToolsRef.current = confirmTools; }, [confirmTools]); useEffect(() => { pendingToolRef.current = pendingTool; }, [pendingTool]); useEffect(() => { listRef.current?.scrollTo({ top: listRef.current.scrollHeight }); }, [messages, pendingTool, waiting]); useEffect(() => () => attachmentUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)), []); useEffect(() => { if (!enabled || !token.trim()) return; localStorage.setItem("canvas-agent-url", endpoint); localStorage.setItem("canvas-agent-token", token); const clientId = clientIdRef.current; const source = new EventSource(`${endpoint}/events?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}`); source.addEventListener("hello", () => { errorLoggedRef.current = false; connectedRef.current = true; setAgentState({ connected: true, activity: "已连接", connectError: "", silentConnect: false, messages: useAgentStore.getState().messages.filter((item) => !isConnectionErrorMessage(item)) }); if (!headless) message.success("本地 Agent 已连接"); void postState(endpoint, token, clientId, canvasContextRef.current?.snapshot || null); if (document.visibilityState === "visible" && document.hasFocus()) void activateAgentClient(endpoint, token, clientId); }); source.addEventListener("tool_call", (event) => { const data = parseEventData(event); if (data) void handleToolCall(endpoint, token, data); }); source.addEventListener("agent_event", (event) => { const data = parseEventData(event); if (data) handleAgentEvent(data); }); source.addEventListener("agent_log", (event) => { const text = parseEventData<{ text?: unknown }>(event)?.text; addEventLog("日志", text, text); }); source.addEventListener("agent_error", (event) => { const message = parseEventData<{ message?: unknown }>(event)?.message; setAgentState({ activity: "出错", waiting: false }); addMessage({ role: "error", title: "错误", text: normalizeText(message) }); addEventLog("错误", message, message); }); source.addEventListener("agent_done", () => { setAgentState({ activity: "完成", waiting: false, sending: false }); void loadThreads(); }); source.onerror = () => { const wasConnected = connectedRef.current; const silent = useAgentStore.getState().silentConnect && !wasConnected; const text = wasConnected ? "本地 Agent 连接失败或已断开" : "连接失败,请检查地址和 token"; if (!errorLoggedRef.current || wasConnected) { addEventLog(wasConnected ? "连接断开" : "连接失败", { endpoint, error: text }); if (!headless && !silent) message.error(text); } errorLoggedRef.current = true; connectedRef.current = false; clearAgentSession({ activity: wasConnected ? "连接断开" : "连接失败", connected: false, connectError: silent ? "" : text, silentConnect: false }); if (!wasConnected) { source.close(); setAgentState({ enabled: false }); } }; return () => { source.close(); connectedRef.current = false; }; }, [enabled, endpoint, loadThreads, message, setAgentState, token]); useEffect(() => { if (connected) void loadThreads(); }, [connected, loadThreads]); useEffect(() => { if (!connected) return; const activate = () => void activateAgentClient(endpoint, token, clientIdRef.current); const activateVisible = () => { if (document.visibilityState === "visible") activate(); }; window.addEventListener("focus", activate); document.addEventListener("visibilitychange", activateVisible); return () => { window.removeEventListener("focus", activate); document.removeEventListener("visibilitychange", activateVisible); }; }, [connected, endpoint, token]); const sendPrompt = async () => { const text = prompt.trim(); const files = attachments; const requestPrompt = promptWithAttachments(text, files); if (!connected || !requestPrompt || sending || waiting) return; if (attachmentPayloadBytes(files) > MAX_ATTACHMENT_PAYLOAD_BYTES) { addMessage({ role: "error", title: "图片过大", text: "图片附件超过 30MB,请删减后再发送。" }); return; } setAgentState({ activity: "发送中", sending: true, waiting: true }); addMessage({ role: "user", text: text || "发送了图片", attachments: files }); addEventLog("用户发送", { text, attachments: files.map(({ name, type, size }) => ({ name, type, size })) }); try { const res = await fetch(`${endpoint}/agent/codex/turn?token=${encodeURIComponent(token)}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ prompt: requestPrompt, threadId: useAgentStore.getState().activeThreadId || undefined, attachments: files.map(({ name, type, dataUrl }) => ({ name, type, dataUrl })) }), }); if (!res.ok) throw new Error("本地 Agent 拒绝了请求"); const data = (await res.json()) as { threadId?: string }; if (data.threadId) setAgentState({ activeThreadId: data.threadId }); addEventLog("本地 Agent 已接收", { status: res.status }); files.forEach((item) => { URL.revokeObjectURL(item.url); attachmentUrlsRef.current.delete(item.url); }); setAgentState({ prompt: "", attachments: [] }); } catch (error) { setAgentState({ activity: "发送失败", waiting: false }); addMessage({ role: "error", title: "发送失败", text: error instanceof Error ? error.message : "发送失败" }); addEventLog("发送失败", error); } finally { setAgentState({ sending: false }); } }; const stopTurn = async () => { if (!connected || (!sending && !waiting)) return; setAgentState({ activity: "停止中" }); try { await fetch(`${endpoint}/agent/codex/interrupt?token=${encodeURIComponent(token)}`, { method: "POST", headers: { "content-type": "application/json" } }); setAgentState({ activity: "已停止", sending: false, waiting: false }); addEventLog("用户停止", {}); } catch { setAgentState({ activity: "就绪", sending: false, waiting: false }); } }; const addAttachments = async (files: FileList | File[] | null) => { if (!files) return; const images = Array.from(files).filter((file) => file.type.startsWith("image/")); const prev = useAgentStore.getState().attachments; try { const next = await Promise.all( images.slice(0, Math.max(0, MAX_ATTACHMENTS - prev.length)).map(async (file) => { const dataUrl = await readDataUrl(file); const url = URL.createObjectURL(file); attachmentUrlsRef.current.add(url); return { id: createId(), name: file.name, type: file.type, size: file.size, url, dataUrl }; }), ); const merged = [...prev, ...next]; if (attachmentPayloadBytes(merged) > MAX_ATTACHMENT_PAYLOAD_BYTES) { next.forEach((item) => { URL.revokeObjectURL(item.url); attachmentUrlsRef.current.delete(item.url); }); addMessage({ role: "error", title: "图片过大", text: "图片附件最多约 30MB。" }); return; } if (next.length) setAgentState({ attachments: merged }); } catch (error) { addMessage({ role: "error", title: "图片读取失败", text: error instanceof Error ? error.message : "图片读取失败" }); } }; const removeAttachment = (id: string) => { const removed = attachments.find((item) => item.id === id); if (removed) { URL.revokeObjectURL(removed.url); attachmentUrlsRef.current.delete(removed.url); } setAgentState({ attachments: attachments.filter((item) => item.id !== id) }); }; const handleToolCall = async (endpoint: string, token: string, payload: AgentPendingToolCall) => { if (confirmToolsRef.current && payload.name === "canvas_apply_ops") { if (pendingToolRef.current) { await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: "仍有待确认的画布工具调用" }); return; } pendingToolRef.current = payload; setAgentState({ pendingTool: payload, activity: "等待确认", waiting: false }); addEventLog("等待确认", payload, payload); return; } await runToolCall(endpoint, token, payload); }; const runToolCall = async (endpoint: string, token: string, payload: AgentPendingToolCall) => { if (isSiteTool(payload.name)) { try { setAgentState({ activity: SITE_TOOL_LABELS[payload.name], waiting: true }); addEventLog(toolName(payload.name), payload, payload); const result = await runSiteTool(payload.name, payload.input || {}, navigate, { canvasSnapshot: canvasContextRef.current?.snapshot || null }); await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, result }); setAgentState({ activity: "工具完成", waiting: true }); addEventLog(`${toolName(payload.name)}完成`, result, result); addMessage({ role: "tool", title: `${toolName(payload.name)}完成`, text: siteToolSummary(payload.name, result), detail: { requestId: payload.requestId, name: payload.name, input: payload.input, result } }); } catch (error) { const message = error instanceof Error ? error.message : "工具执行失败"; setAgentState({ activity: "工具失败", waiting: false }); addMessage({ role: "tool", title: "工具失败", text: message, detail: payload }); await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: message }); } return; } try { const input: { ops?: CanvasAgentOp[]; path?: string } = payload.input || {}; setAgentState({ activity: payload.name === "canvas_apply_ops" ? "执行画布操作" : payload.name === "site_navigate" ? "跳转页面" : "读取画布", waiting: true }); addEventLog(toolName(payload.name), payload, payload); let result: unknown; if (payload.name === "site_navigate") { const path = input.path || "/"; navigate(path); result = { ok: true, path }; } else if (payload.name === "canvas_apply_ops") { const context = canvasContextRef.current; if (!context) throw new Error("当前不在画布页,请先用 site_navigate 打开画布"); result = context.applyOps(input.ops || []); void postState(endpoint, token, clientIdRef.current, result as CanvasAgentSnapshot); } else { const snapshot = canvasContextRef.current?.snapshot; if (!snapshot) throw new Error("当前不在画布页,请先用 site_navigate 打开画布"); result = snapshot; } await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, result }); setAgentState({ activity: "工具完成", waiting: true }); addEventLog(`${toolName(payload.name)}完成`, result, result); addMessage({ role: "tool", title: `${toolName(payload.name)}完成`, text: payload.name === "canvas_apply_ops" ? summarizeCanvasAgentOps(input.ops || []) || "画布操作" : payload.name === "site_navigate" ? `已跳转到 ${input.path || "/"}` : "已完成", detail: { requestId: payload.requestId, name: payload.name, input, result }, }); } catch (error) { const message = error instanceof Error ? error.message : "画布操作失败"; setAgentState({ activity: "工具失败", waiting: false }); addMessage({ role: "tool", title: "工具失败", text: message, detail: payload }); await postToolResult(endpoint, token, clientIdRef.current, { requestId: payload.requestId, error: message }); } }; const rejectPendingTool = async () => { if (!pendingTool) return; await postToolResult(endpoint, token, clientIdRef.current, { requestId: pendingTool.requestId, error: "用户取消了画布工具调用" }); setAgentState({ activity: "已取消", waiting: false }); addMessage({ role: "tool", title: "拒绝执行", text: toolName(pendingTool.name), detail: { requestId: pendingTool.requestId, name: pendingTool.name, input: pendingTool.input } }); pendingToolRef.current = null; setAgentState({ pendingTool: null }); }; const approvePendingTool = async () => { if (!pendingTool) return; const tool = pendingTool; pendingToolRef.current = null; setAgentState({ pendingTool: null }); await runToolCall(endpoint, token, tool); }; const toggleAgentConnection = async ({ silent = false }: { silent?: boolean } = {}) => { if (enabled) { clearAgentSession({ enabled: false, connected: false, activity: "离线", connectError: "" }); return; } const urlToken = searchParams.get("agentToken") || ""; const urlEndpoint = searchParams.get("agentUrl") || ""; const discovered = urlToken ? null : await discoverAgentConfig(endpoint || DEFAULT_AGENT_URL); const nextEndpoint = (urlEndpoint || discovered?.url || endpoint || DEFAULT_AGENT_URL).trim().replace(/\/$/, ""); const nextToken = (urlToken || token.trim() || discovered?.token || "").trim(); if (!nextEndpoint) { const text = "请填写本地 Agent 地址"; if (!silent) { setAgentState({ connectError: text }); if (!headless) message.warning(text); } return; } if (!nextToken) { const text = "没有发现本地 Agent,请先在 Codex 使用插件或手动启动 Canvas Agent"; if (!silent) { setAgentState({ connectError: text }); if (!headless) message.warning(text); } return; } try { const parsed = new URL(nextEndpoint); if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol"); } catch { const text = "本地 Agent 地址格式不正确"; if (!silent) { setAgentState({ connectError: text }); if (!headless) message.warning(text); } return; } errorLoggedRef.current = false; setAgentState({ url: nextEndpoint, token: nextToken, enabled: true, connected: false, silentConnect: silent, activity: "连接中", connectError: "", activeTab: "setup" }); }; useEffect(() => { if (urlAgentAutoConnect && confirmTools) setAgentState({ confirmTools: false }); }, [confirmTools, setAgentState, urlAgentAutoConnect]); useEffect(() => { if (!autoConnect || autoConnectRef.current || enabled || connected) return; autoConnectRef.current = true; void toggleAgentConnection({ silent: true }); }, [autoConnect, connected, enabled]); function clearAgentSession(patch: Parameters[0] = {}) { setAgentState({ messages: [], threads: [], activeThreadId: "", workspacePath: "", loadingThreads: false, waiting: false, sending: false, pendingTool: null, ...patch, }); pendingToolRef.current = null; } const startNewThread = async () => { if (!connected) return; setAgentState({ loadingThreads: true }); try { const data = await fetchAgentJson(endpoint, token, "/agent/codex/threads/new", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); setAgentState({ activeThreadId: data.thread?.id || data.workspace?.activeThreadId || "", messages: [], activeTab: "chat", activity: "新对话" }); await loadThreads(); } catch (error) { addEventLog("新建对话失败", error); message.error(error instanceof Error ? error.message : "新建对话失败"); } finally { setAgentState({ loadingThreads: false }); } }; const resumeThread = async (threadId: string) => { if (!connected || !threadId) return; setAgentState({ loadingThreads: true }); try { const data = await fetchAgentJson(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/resume`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); setAgentState({ activeThreadId: data.thread?.id || threadId, messages: normalizeHistoryMessages(data.messages || []), activeTab: "chat", activity: "已恢复会话" }); await loadThreads(); } catch (error) { addEventLog("恢复对话失败", error); message.error(error instanceof Error ? error.message : "恢复对话失败"); } finally { setAgentState({ loadingThreads: false }); } }; const deleteThread = async (threadId: string) => { if (!connected || !threadId) return; setAgentState({ loadingThreads: true }); try { await fetchAgentJson(endpoint, token, `/agent/codex/threads/${encodeURIComponent(threadId)}/delete`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}) }); const current = useAgentStore.getState(); setAgentState({ threads: current.threads.filter((thread) => thread.id !== threadId), activeThreadId: current.activeThreadId === threadId ? "" : current.activeThreadId, messages: current.activeThreadId === threadId ? [] : current.messages, }); message.success("记录已删除"); } catch (error) { addEventLog("删除对话失败", error); message.error(error instanceof Error ? error.message : "删除对话失败"); } finally { setAgentState({ loadingThreads: false }); } }; const confirmDeleteThread = (thread: AgentThreadSummary) => { const label = thread.name || thread.preview || "未命名对话"; modal.confirm({ title: "删除对话记录", content: `确定删除「${label.length > 48 ? `${label.slice(0, 48)}...` : label}」吗?`, okText: "删除", okType: "danger", cancelText: "取消", onOk: () => deleteThread(thread.id), }); }; const addMessage = (item: Omit) => { const text = normalizeText(item.text); if (!text && !item.attachments?.length) return; const next = { ...item, id: `${Date.now()}-${Math.random()}`, text }; const currentMessages = useAgentStore.getState().messages; if (next.streamId) { const index = currentMessages.findIndex((message) => message.streamId === next.streamId); if (index >= 0) { setAgentState({ messages: currentMessages.map((message, i) => (i === index ? { ...message, ...next, id: message.id, text: next.text || message.text } : message)) }); return; } } const last = currentMessages.at(-1); if (last?.role === "assistant" && next.role === "assistant" && last.title === next.title) { const merged = mergeAgentText(last.text, next.text); if (merged === last.text) return; setAgentState({ messages: [...useAgentStore.getState().messages.slice(0, -1), { ...last, text: merged, meta: next.meta || last.meta }] }); return; } pushMessage(next); }; const addEventLog = (title: string, text: unknown, raw?: unknown) => { pushEventLog({ id: `${Date.now()}-${Math.random()}`, time: new Date().toLocaleTimeString(), title, text: normalizeText(text) || title, raw }); }; const handleAgentEvent = (event: AgentEventPayload) => { if (shouldLogAgentEvent(event)) addEventLog(eventTitle(event), event, event); if (event.type === "thread.started" && event.thread_id) setAgentState({ activeThreadId: event.thread_id }); const nextActivity = activityText(event); if (nextActivity) setAgentState({ activity: nextActivity }); if (event.type === "turn.started") setAgentState({ waiting: true }); if (event.type === "turn.completed" || event.type === "turn.failed" || event.type === "error") setAgentState({ waiting: false, sending: false }); const item = formatAgentEvent(event); if (item) { if (item.role === "error") setAgentState({ waiting: false, sending: false }); addMessage(item); } }; const content = ( <> }, { value: "chat", label: "对话" }, { value: "history", label: "历史", icon: , count: threads.length }, { value: "log", label: "日志", icon: , count: eventLogs.length }, ]} onChange={(activeTab) => { setAgentState({ activeTab }); if (activeTab === "history") void loadThreads(); }} right={ <> } /> {activeTab === "setup" ? ( setAgentState({ url, connectError: "" })} onTokenChange={(token) => setAgentState({ token, connectError: "" })} onToggleEnabled={toggleAgentConnection} /> ) : activeTab === "history" ? ( void loadThreads()} onNewThread={() => void startNewThread()} onResumeThread={(threadId) => void resumeThread(threadId)} onDeleteThread={confirmDeleteThread} /> ) : activeTab === "log" ? ( message.success(text)} onCopyBlocked={(text) => message.warning(text)} /> ) : ( <>
{messages.map((item) => ( ))} {pendingTool ? ( ) : null} {waiting && !pendingTool ? : null}
setAgentState({ prompt })} onSubmit={sendPrompt} onStop={stopTurn} onAddFiles={addAttachments} onRemoveAttachment={removeAttachment} left={ attachments.length ? ( {formatBytes(attachmentPayloadBytes(attachments))} / 30MB ) : null } /> )} ); if (headless) return null; return embedded ? content : null; } function AgentLogView({ logs, theme, context, onClear, onCopied, onCopyBlocked, }: { logs: AgentEventLog[]; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; context: AgentLogContext; onClear: () => void; onCopied: (text: string) => void; onCopyBlocked: (text: string) => void; }) { const [mode, setMode] = useState<"text" | "json">("text"); const textareaRef = useRef(null); const content = mode === "text" ? formatLogText(logs, context) : formatLogJson(logs, context); const lastError = [...logs].reverse().find((item) => /错误|失败|error/i.test(`${item.title}\n${item.text}`)); const copy = async (value = content, tip = "日志已复制") => { if (await copyToClipboard(value)) { onCopied(tip); return; } textareaRef.current?.focus(); textareaRef.current?.select(); onCopyBlocked("已选中日志,请手动复制"); }; return (
运行日志
setMode(value as "text" | "json")} options={[ { label: "排查日志", value: "text" }, { label: "原始 JSON", value: "json" }, ]} />
{logs.length} 条