diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d24998..6a87255 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@
+ [优化] Agent 对话采用透明无气泡的简洁用户消息样式,输入框上方居中实时展示带平滑数字动画的最新一次模型调用 Token 用量。
+ [优化] Canvas Agent 流式回复改为增量合并传输并隔离消息列表渲染,减少长回复和长会话中的重复更新与界面卡顿。
+ [优化] Agent 对话新增思考摘要、计划、命令、搜索和文件变更时间线,工具卡片改为中文摘要与用户可读详情,不再展示原始 JSON。
-+ [新增] Agent 对话支持 Codex 结构化任务进度,运行期间固定展示并逐项实时更新待处理、进行中和已完成状态。
++ [新增] Agent 对话支持 Codex 结构化任务进度,在输入区上方独立折叠展示并逐项实时更新待处理、进行中和已完成状态。
+ [优化] Agent 长时间等待时显示当前处理阶段、已等待时长和停止提示,并避免新建空会话反复记录历史读取失败。
+ [修复] 修复 Agent 发送后的运行提示闪退及回复需要切换标签页才显示的问题。
+ [修复] Agent 在模型繁忙等任务失败时会立即结束等待状态,并在对话和历史中展示中文错误原因与重试建议。
diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx
index 1274aac..53053a5 100644
--- a/docs/content/docs/progress/pending-test.mdx
+++ b/docs/content/docs/progress/pending-test.mdx
@@ -11,7 +11,7 @@ description: 当前版本已实现但仍需人工验证的变更项
- Agent 对话统计:用户消息应右对齐并使用透明无边框、无气泡背景的简洁样式,用户消息和 Codex 回复下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。
- Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现 GPT 图标和“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。
- Agent 流式交互性能:发送长回复时文字应连续平滑出现,输入框、滚动和画布操作不应随回复变长而明显卡顿;长历史会话中只有当前流式消息持续更新,屏幕外消息不应造成明显布局压力;任务完成后应通过 SSE 自动同步完整历史,不再持续请求 `/health` 轮询状态。
-- Agent 过程时间线:Codex 运行时应在对话中依次显示中文的任务进度、思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;当前轮结构化任务进度应固定在对话可视区域,不随后续消息滚出屏幕,并逐项实时更新「待处理」「进行中」「已完成」状态,任务结束后恢复为普通历史消息;同一操作应原位更新而不是生成重复卡片;命令详情只展示工作目录、耗时、退出状态和运行输出,文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON,刷新历史会话后仍保持相同展示。
+- Agent 过程时间线:Codex 运行时应在对话中依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;命令详情只展示工作目录、耗时、退出状态和运行输出,文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON,刷新历史会话后仍保持相同展示。
- Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。
- Agent 工作目录指令:`canvas-agent/agent-instructions.md` 应作为独立维护源;重启 Canvas Agent 后,当前工作目录应自动生成 `AGENTS.md`;新建对话发送消息时,Codex 日志中的用户消息只包含本轮请求和必要的附件上下文,不再重复整段 Infinite Canvas 前置提示词,画布及工作台工具仍可正常调用。
- 画布文本设置:文本节点和生成配置节点切换到文本模式后应显示推理强度设置,可选择自动、低、中、高、极高;选择自动时默认 OpenAI Responses 请求不应携带 `reasoning`,选择其他档位时应携带所选强度,刷新画布后节点设置应保留;文本模型自定义调用脚本应能读取 `reasoningEffort`,OpenAI 模板应按自动或指定档位正确组装请求。
diff --git a/web/src/components/canvas/canvas-agent-chat-ui.tsx b/web/src/components/canvas/canvas-agent-chat-ui.tsx
index 67c391b..4501ffe 100644
--- a/web/src/components/canvas/canvas-agent-chat-ui.tsx
+++ b/web/src/components/canvas/canvas-agent-chat-ui.tsx
@@ -126,16 +126,18 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
}
function AgentPlanCard({ title, plan, theme }: { title: string; plan: PlanDetail; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) {
+ const [open, setOpen] = useState(true);
const completed = plan.tasks.filter((item) => item.status === "completed").length;
const state = planCardState(plan, completed);
return (
-
-
+ setOpen(event.currentTarget.open)} className="group min-w-0 flex-1 rounded-xl border px-3 py-2.5 text-left" style={{ borderColor: theme.node.stroke, background: "transparent", color: theme.node.text }}>
+
{title}
{state.label}
- {completed}/{plan.tasks.length}
-
+
{completed}/{plan.tasks.length}
+
+
{plan.explanation ?
{plan.explanation}
: null}
{plan.tasks.map((item, index) => {
@@ -149,7 +151,7 @@ function AgentPlanCard({ title, plan, theme }: { title: string; plan: PlanDetail
);
})}
-
+
);
}
diff --git a/web/src/components/canvas/canvas-local-agent-panel.tsx b/web/src/components/canvas/canvas-local-agent-panel.tsx
index 8a458ba..600e2db 100644
--- a/web/src/components/canvas/canvas-local-agent-panel.tsx
+++ b/web/src/components/canvas/canvas-local-agent-panel.tsx
@@ -17,7 +17,7 @@ import { useShallow } from "zustand/react/shallow";
import { useAgentStore, type AgentAttachment, type AgentCanvasContext, type AgentChatItem, type AgentEventLog, type AgentPanelTab, type AgentPendingToolCall, type AgentThreadSummary, type AgentTokenUsage } 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";
+import { AgentChatComposer, AgentChatMessage, AgentPanelTabs, AgentPendingToolCard, AgentToolCard, AgentWorkingMessage, type CanvasAgentChatAttachment } from "./canvas-agent-chat-ui";
const MAX_ATTACHMENTS = 6;
const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024;
@@ -786,6 +786,7 @@ export function CanvasLocalAgentPanel({ embedded, headless, autoConnect }: { emb
) : (
<>
+
{tokenUsage ? : null}
message.streamId);
- const activePlan = sending || waiting ? currentPlanMessage(messages) : undefined;
const working = workingActivity(messages.at(-1));
const updateScrollState = useCallback(() => {
const list = listRef.current;
@@ -858,15 +858,10 @@ function AgentChatTimeline({
return () => cancelAnimationFrame(frame);
}, [messages, pendingTool, scrollToBottom, updateScrollState, waiting]);
return (
-
- {activePlan ? (
-
- ) : null}
-
+
+
{messages.map((item) => (
- item.id === activePlan?.id ? null :
+ isPlanMessage(item) ? null :
))}
{pendingTool ? (
busy ? currentPlanMessage(state.messages) : latestPlanMessage(state.messages));
+ if (!plan) return null;
+ return (
+
+ );
+}
+
const AgentChatMessageRow = memo(function AgentChatMessageRow({ item, theme, user }: { item: AgentChatItem; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; user: LocalUser | null }) {
return (
@@ -1621,11 +1626,21 @@ function workingActivity(item?: AgentChatItem) {
function currentPlanMessage(messages: AgentChatItem[]) {
for (let index = messages.length - 1; index >= 0; index--) {
const message = messages[index];
- if (message.role === "tool" && objectField(message.detail, "kind") === "todo") return message;
+ if (isPlanMessage(message)) return message;
if (message.role === "user") return;
}
}
+function latestPlanMessage(messages: AgentChatItem[]) {
+ for (let index = messages.length - 1; index >= 0; index--) {
+ if (isPlanMessage(messages[index])) return messages[index];
+ }
+}
+
+function isPlanMessage(message: AgentChatItem) {
+ return message.role === "tool" && objectField(message.detail, "kind") === "todo";
+}
+
function parseToolResult(result: unknown) {
const content = objectField(result, "content");
const text = Array.isArray(content)