From 5b689c8feba86579323e9ac9362cd34ef9f2cfb7 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Thu, 30 Jul 2026 13:18:09 +0800 Subject: [PATCH] feat(agent): implement immediate reset of conversation state and synchronize empty session status without waiting for Codex thread creation --- CHANGELOG.md | 1 + canvas-agent/src/server/http.ts | 4 +++ docs/content/docs/progress/pending-test.mdx | 1 + .../components/agent/local-agent-panel.tsx | 25 +++++++++++-------- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a896c..db7afcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ++ [优化] Agent「新对话」改为立即清空当前界面并后台同步空白会话状态,不再等待创建 Codex 线程。 + [优化] Agent「读取画布」工具卡片按文本、图片、配置、视频、音频、分组及连线分类展示画布内容概览。 + [优化] Agent 首次发送消息时立即清空输入框并展示用户消息,避免等待线程创建期间出现输入内容滞留和后续草稿被清空。 + [优化] Agent 动态工具卡片显示具体工具名称、准确执行状态和失败原因,避免统一显示为含糊的「工具操作」。 diff --git a/canvas-agent/src/server/http.ts b/canvas-agent/src/server/http.ts index 617c48b..49db85b 100644 --- a/canvas-agent/src/server/http.ts +++ b/canvas-agent/src/server/http.ts @@ -108,6 +108,10 @@ export function startHttpServer() { const nextWorkspace = setActiveThread(activeThreadId, { emptyThread: true }); res.json({ ok: true, workspace: nextWorkspace, thread: summarizeCodexThread(thread), messages: [] }); })); + app.post("/agent/codex/threads/reset", (req, res) => { + if (session.codexBusy) return res.status(409).json({ ok: false, error: "Codex 正在运行,请等待当前任务完成" }); + res.json({ ok: true, workspace: setActiveThread("", { emptyThread: true, draftThread: true }) }); + }); app.get("/agent/codex/threads/:threadId", route(async (req, res) => { const workspace = ensureSiteWorkspace(config); const threadId = routeParam(req.params.threadId); diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 86e82a4..553d18a 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应立即清空并进入空白对话,不出现等待或新建按钮卡顿;此时不应生成空历史记录,第一次发送消息时才创建线程;多个标签页应同步进入空白对话,点击后立即发送也不得误发到上一条会话。 - Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。 - Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。 - Agent 动态工具信息:执行内置生图、查看图片、命令、文件修改或其他动态工具时,卡片标题应显示具体工具名称;执行失败时正文应显示真实错误原因,刷新并恢复历史对话后仍应保持一致,不再统一显示「工具操作已完成」。 diff --git a/web/src/components/agent/local-agent-panel.tsx b/web/src/components/agent/local-agent-panel.tsx index fe46d51..f813d30 100644 --- a/web/src/components/agent/local-agent-panel.tsx +++ b/web/src/components/agent/local-agent-panel.tsx @@ -66,7 +66,7 @@ type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: A type AgentThreadResponse = { ok?: boolean; workspace?: AgentWorkspace; thread?: AgentThreadSummary; messages?: AgentChatItem[] }; type AgentCodexState = { busy?: boolean; threadId?: string; turnId?: string }; type AgentHelloEvent = { ok?: boolean; clientId?: string; codex?: AgentCodexState }; -type AgentWorkspaceEvent = { activeThreadId?: string; threadId?: string; emptyThread?: boolean }; +type AgentWorkspaceEvent = { activeThreadId?: string; threadId?: string; emptyThread?: boolean; draftThread?: boolean }; type AgentChatEvent = { threadId?: string; sourceClientId?: string; message?: AgentChatItem }; export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?: boolean; headless?: boolean; autoConnect?: boolean }) { @@ -119,6 +119,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const attachmentUrlsRef = useRef(new Set()); const clientIdRef = useRef(randomId()); const loadThreadsSequenceRef = useRef(0); + const resetThreadRef = useRef | null>(null); const endpoint = useMemo(() => url.trim().replace(/\/$/, ""), [url]); const urlAgentAutoConnect = searchParams.has("agentUrl") && searchParams.has("agentToken"); const loadThreads = useCallback(async (skipHistory = false) => { @@ -226,7 +227,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? const keepPendingMessage = Boolean(data.emptyThread && current.sending && current.activeThreadId === nextThreadId); pendingToolRef.current = null; setAgentState({ activeThreadId: nextThreadId, ...(keepPendingMessage ? {} : { messages: [] }), tokenUsage: null, pendingTool: null, pendingApprovals: [] }); - await loadThreads(Boolean(data.emptyThread)); + if (!data.draftThread) await loadThreads(Boolean(data.emptyThread)); }); }); source.addEventListener("chat_message", (event) => { @@ -304,6 +305,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? addMessage({ id: messageId, role: "user", text: userText, historyText: requestPrompt, attachments: files }); let threadId = useAgentStore.getState().activeThreadId; try { + await resetThreadRef.current; if (!threadId) { const created = await fetchAgentJson(endpoint, token, "/agent/codex/threads/new", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ permissionMode }) }); threadId = created.thread?.id || created.workspace?.activeThreadId || ""; @@ -576,18 +578,19 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? pendingToolRef.current = null; } - const startNewThread = async () => { + const startNewThread = () => { if (!connected || sending || waiting) 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({ permissionMode }) }); - setAgentState({ activeThreadId: data.thread?.id || data.workspace?.activeThreadId || "", messages: [], tokenUsage: null, activeTab: "chat", activity: "新对话" }); - } catch (error) { + setAgentState({ activeThreadId: "", messages: [], tokenUsage: null, activeTab: "chat", activity: "新对话", pendingTool: null, pendingApprovals: [] }); + pendingToolRef.current = null; + const request = fetchAgentJson(endpoint, token, "/agent/codex/threads/reset", { method: "POST" }).catch((error) => { addEventLog("新建对话失败", error); message.error(error instanceof Error ? error.message : "新建对话失败"); - } finally { - setAgentState({ loadingThreads: false }); - } + throw error; + }); + resetThreadRef.current = request; + void request.finally(() => { + if (resetThreadRef.current === request) resetThreadRef.current = null; + }).catch(() => undefined); }; const resumeThread = async (threadId: string) => {