feat(agent): implement immediate reset of conversation state and synchronize empty session status without waiting for Codex thread creation

This commit is contained in:
HouYunFei
2026-07-30 13:18:09 +08:00
parent 1291dd08c3
commit 5b689c8feb
4 changed files with 20 additions and 11 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unreleased
+ [优化] Agent「新对话」改为立即清空当前界面并后台同步空白会话状态,不再等待创建 Codex 线程。
+ [优化] Agent「读取画布」工具卡片按文本、图片、配置、视频、音频、分组及连线分类展示画布内容概览。
+ [优化] Agent 首次发送消息时立即清空输入框并展示用户消息,避免等待线程创建期间出现输入内容滞留和后续草稿被清空。
+ [优化] Agent 动态工具卡片显示具体工具名称、准确执行状态和失败原因,避免统一显示为含糊的「工具操作」。
+4
View File
@@ -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);
@@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试
- Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应立即清空并进入空白对话,不出现等待或新建按钮卡顿;此时不应生成空历史记录,第一次发送消息时才创建线程;多个标签页应同步进入空白对话,点击后立即发送也不得误发到上一条会话。
- Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。
- Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。
- Agent 动态工具信息:执行内置生图、查看图片、命令、文件修改或其他动态工具时,卡片标题应显示具体工具名称;执行失败时正文应显示真实错误原因,刷新并恢复历史对话后仍应保持一致,不再统一显示「工具操作已完成」。
+14 -11
View File
@@ -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<string>());
const clientIdRef = useRef(randomId());
const loadThreadsSequenceRef = useRef(0);
const resetThreadRef = useRef<Promise<unknown> | 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<AgentThreadResponse>(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<AgentThreadResponse>(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) => {