feat(agent): enhance Codex reasoning summary with Markdown rendering and improve message history merging

This commit is contained in:
HouYunFei
2026-07-29 16:34:44 +08:00
parent a9243b6c2e
commit bd8cf318f7
7 changed files with 24 additions and 5 deletions
+2
View File
@@ -2,6 +2,8 @@
## Unreleased
+ [优化] Agent 思考摘要支持 Markdown 渲染,改善强调、列表和代码内容的阅读效果。
+ [修复] Agent 对话启用 Codex 可读思考摘要,并修复一轮结束或刷新恢复后思考摘要、命令和工具过程记录缺失的问题。
+ [优化] Agent 思考摘要和命令执行改为无边框紧凑折叠行,并保留 Codex 实时返回的具体思考摘要。
+ [调整] Agent 工具确认模式移至对话输入框,并默认使用自动确认。
+ [优化] Agent 用户图片消息改为紧凑缩略图展示,并支持单击放大预览。
+1 -1
View File
@@ -314,7 +314,7 @@ function canvasAgentMcpCommand() {
/** 生成 Codex app-server 使用的 MCP 配置。 */
function codexConfig() {
return { mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } } };
return { model_reasoning_summary: "auto", mcp_servers: { "infinite-canvas": { command: canvasAgentMcp.command, args: canvasAgentMcp.args, default_tools_approval_mode: "approve", startup_timeout_sec: 20, tool_timeout_sec: 90 } } };
}
/** 将文本和本地图片转换为 Codex turn 输入。 */
+2 -1
View File
@@ -47,7 +47,8 @@ export async function resumeCodexThread(emit: AgentEmit, threadId: string, cwd?:
const thread = await app.resumeThread(threadId, cwd);
assertThreadWorkspace(thread, cwd);
codexThreadId = String(field(thread, "id") || threadId);
return { thread, messages: threadMessages(thread, app.planUpdates(threadId)) };
const historyThread = await loadCodexThread(emit, codexThreadId, cwd, true);
return { thread, messages: threadMessages(historyThread, app.planUpdates(threadId)) };
}
/** 查询当前工作空间中的 Codex 线程。 */
+1 -1
View File
@@ -12,7 +12,7 @@ description: 当前版本已实现但仍需人工验证的变更项
- Agent 对话统计:用户消息应右对齐并使用透明无气泡的简洁排版,用户和 Codex 两侧均不显示人物头像,消息下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。
- Agent 流式交互性能:发送长回复时文字应连续平滑出现,输入框、滚动和画布操作不应随回复变长而明显卡顿;长历史会话中只有当前流式消息持续更新,屏幕外消息不应造成明显布局压力;任务完成后应通过 SSE 自动同步完整历史,不再持续请求 `/health` 轮询状态。
- Agent 过程时间线:Codex 运行时应在对话中依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;思考摘要的图标、标题和箭头应稳定保持在同一行,默认收起且无边框,点击箭头后才展开 Codex 实际返回的具体摘要,完成事件不应再用「已完成分析」覆盖已经收到的摘要;命令执行也应使用无边框紧凑行,显示「已执行 1 条命令」和单行命令预览,点击后再展开工作目录、耗时、退出状态和运行输出;其他工具调用继续使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON,刷新历史会话后仍保持相同展示。
- Agent 过程时间线:新建 Agent 对话后 Codex 应生成可读思考摘要,并在运行时依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;思考摘要的图标、标题和箭头应稳定保持在同一行,默认收起且无边框,点击箭头后才展开 Codex 实际返回的具体摘要,其中 Markdown 强调、列表和代码应正确渲染而非显示原始符号,完成事件不应再用「已完成分析」覆盖已经收到的摘要;命令执行也应使用无边框紧凑行,显示「已执行 1 条命令」和单行命令预览,点击后再展开工作目录、耗时、退出状态和运行输出;其他工具调用继续使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON;一轮对话结束自动同步历史以及刷新页面后从历史中 resume 对话时,思考摘要、命令和工具过程记录仍应完整保留并保持相同展示。
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
- Agent 默认新对话:每次进入任一画布并连接 Agent 后,对话区应保持空白且不自动恢复上一次会话;第一次发送消息时才创建新线程,不应产生未发送消息的空历史记录;需要继续旧对话时可在「历史」中主动选择恢复。
- Agent 当前画布优先:在已打开某个画布时要求 Agent 创建、修改、整理或生成内容,Agent 应直接读取并操作当前画布,不应先调用 `canvas_list_projects` 或使用 `site_navigate` 重复进入画布;只有明确要求查看或切换其他画布时才允许查询画布列表并导航。
@@ -121,7 +121,9 @@ function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?:
<ChevronRight className="size-3.5 shrink-0 transition-transform group-open:rotate-90" />
</div>
</summary>
<div className="whitespace-pre-wrap break-words pb-1 pl-6 pr-2 text-xs leading-5" style={{ color: theme.node.muted }}>{text}</div>
<div className="break-words pb-1 pl-6 pr-2 text-xs leading-5 [&_code]:rounded [&_code]:px-1 [&_p]:my-1 [&_pre]:my-2" style={{ color: theme.node.muted }}>
<Streamdown animated isAnimating={running}>{text}</Streamdown>
</div>
</details>
);
}
@@ -487,3 +487,15 @@ export function mergeHistoryAttachments(messages: AgentChatItem[], currentMessag
})
.reverse();
}
export function mergeHistoryMessages(historyMessages: AgentChatItem[], currentMessages: AgentChatItem[]) {
if (!currentMessages.length) return historyMessages;
const remaining = [...historyMessages];
const messages = currentMessages.map((current) => {
const index = remaining.findIndex((history) => history.id === current.id || ((current.role === "user" || current.role === "assistant") && history.role === current.role && history.text === current.text));
if (index < 0) return current;
const history = remaining.splice(index, 1)[0];
return { ...current, ...history, id: current.id, attachments: current.attachments || history.attachments };
});
return [...messages, ...remaining];
}
@@ -38,6 +38,7 @@ import {
isConnectionErrorMessage,
isCurrentThreadEvent,
mergeHistoryAttachments,
mergeHistoryMessages,
mergeAgentText,
normalizeHistoryMessages,
normalizeText,
@@ -131,7 +132,8 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
let thread = await currentThreadRequest;
thread ||= await fetchAgentJson<AgentThreadResponse>(endpoint, token, `/agent/codex/threads/${encodeURIComponent(currentThreadId)}`);
const storedMessages = (await storedMessagesRequest) || [];
nextMessages = mergeHistoryAttachments(normalizeHistoryMessages(thread.messages || []), [...storedMessages, ...useAgentStore.getState().messages]);
const currentMessages = useAgentStore.getState().messages;
nextMessages = mergeHistoryMessages(mergeHistoryAttachments(normalizeHistoryMessages(thread.messages || []), [...storedMessages, ...currentMessages]), currentMessages);
}
if (sequence !== loadThreadsSequenceRef.current) return;
setAgentState({ threads: data.data || [], workspacePath: data.workspace?.workspacePath || "", ...(skipHistory ? {} : { messages: nextMessages }) });