diff --git a/CHANGELOG.md b/CHANGELOG.md index cceab73..11650e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ++ [优化] Agent 思考摘要和命令执行改为无边框紧凑折叠行,并保留 Codex 实时返回的具体思考摘要。 + [调整] Agent 工具确认模式移至对话输入框,并默认使用自动确认。 + [优化] Agent 用户图片消息改为紧凑缩略图展示,并支持单击放大预览。 + [修复] 修复 Agent 图片预览打开后在对话区域产生额外空行的问题。 diff --git a/README.md b/README.md index 2b41882..93ec72b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

- basketikun%2Finfinite-canvas | Trendshift +basketikun%2Finfinite-canvas | Trendshift

diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index b081a26..dbb0da4 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -12,7 +12,7 @@ description: 当前版本已实现但仍需人工验证的变更项 - Agent 对话统计:用户消息应右对齐并使用透明无气泡的简洁排版,用户和 Codex 两侧均不显示人物头像,消息下方均不显示时间或 Token 信息;输入框上方应居中展示最新一次模型调用的输入、缓存、输出 Token 用量,不显示会话累计值,数值更新时应从旧值平滑滚动到新值而非突然跳变,新建、切换或删除当前会话后应清空旧统计。 - Agent 回复实时显示:在右侧 Agent 发送消息后,用户消息下方应立即出现“正在思考...”,任务运行期间不应闪退;工具完成后应显示 Codex 正在继续处理及已等待时长,等待超过 30 秒时提示可继续等待或停止本轮;Codex 的回复应在当前对话中持续显示,实时事件缺失时也应在任务完成后自动同步完整内容,无需切换到历史或日志再返回对话。模型繁忙等任务失败时应立即结束等待状态,在对话中显示中文错误原因和重试建议,诊断日志不应再把失败轮次记为“处理完成”,刷新历史后错误仍应保留。新建尚未发送首条消息的空会话不应反复出现历史读取失败。 - Agent 流式交互性能:发送长回复时文字应连续平滑出现,输入框、滚动和画布操作不应随回复变长而明显卡顿;长历史会话中只有当前流式消息持续更新,屏幕外消息不应造成明显布局压力;任务完成后应通过 SSE 自动同步完整历史,不再持续请求 `/health` 轮询状态。 -- Agent 过程时间线:Codex 运行时应在对话中依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;工具活动应使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行,不应由单个图标占据一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;命令详情只展示工作目录、耗时、退出状态和运行输出,文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON,刷新历史会话后仍保持相同展示。 +- Agent 过程时间线:Codex 运行时应在对话中依次显示中文的思考摘要、执行计划、命令执行、网页搜索、文件修改和画布工具活动;思考摘要的图标、标题和箭头应稳定保持在同一行,默认收起且无边框,点击箭头后才展开 Codex 实际返回的具体摘要,完成事件不应再用「已完成分析」覆盖已经收到的摘要;命令执行也应使用无边框紧凑行,显示「已执行 1 条命令」和单行命令预览,点击后再展开工作目录、耗时、退出状态和运行输出;其他工具调用继续使用紧凑卡片排版,状态图标、标题和状态文字保持在同一行;结构化任务进度不应混在对话时间线中,而应独立放在对话区下方、Token 统计上方,支持展开和折叠,并逐项实时更新「待处理」「进行中」「已完成」状态;新任务生成时默认展开,任务结束后保留最新结果,同一计划更新不应生成重复内容;文件详情展示文件路径与新增/修改/删除动作,工具详情不应出现请求 ID、英文工具名或原始 JSON,刷新历史会话后仍保持相同展示。 - Agent 历史记录:点击记录卡片应直接进入对应对话,不再显示「进入」按钮;可勾选单条或全选多条记录并批量删除,删除当前对话后聊天内容应清空。 - Agent 默认新对话:每次进入任一画布并连接 Agent 后,对话区应保持空白且不自动恢复上一次会话;第一次发送消息时才创建新线程,不应产生未发送消息的空历史记录;需要继续旧对话时可在「历史」中主动选择恢复。 - Agent 当前画布优先:在已打开某个画布时要求 Agent 创建、修改、整理或生成内容,Agent 应直接读取并操作当前画布,不应先调用 `canvas_list_projects` 或使用 `site_navigate` 重复进入画布;只有明确要求查看或切换其他画布时才允许查询画布列表并导航。 diff --git a/web/src/components/agent/agent-chat-message.tsx b/web/src/components/agent/agent-chat-message.tsx index 8f8b7bb..3a5ebaf 100644 --- a/web/src/components/agent/agent-chat-message.tsx +++ b/web/src/components/agent/agent-chat-message.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, type ReactNode } from "react"; import { Button, Image } from "antd"; -import { Brain, CheckCircle2, ChevronDown, Circle, CircleAlert, FilePenLine, FileText, ListChecks, LoaderCircle, Search, TerminalSquare, Wrench, XCircle } from "lucide-react"; +import { Brain, CheckCircle2, ChevronDown, ChevronRight, Circle, CircleAlert, FilePenLine, FileText, ListChecks, LoaderCircle, Search, TerminalSquare, Wrench, XCircle } from "lucide-react"; import { Streamdown } from "streamdown"; import { canvasThemes } from "@/lib/canvas-theme"; @@ -86,9 +86,11 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro export function AgentToolCard({ title, text, detail, theme }: { title: string; text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { const plan = planDetail(detail); if (plan) return ; + const kind = String(objectField(detail, "kind") || ""); + if (kind === "reasoning") return ; + if (kind === "command") return ; const state = toolCardState(title, text, detail); const view = userDetail(detail); - const kind = String(objectField(detail, "kind") || ""); return (

{ if (!view) event.preventDefault(); }}> @@ -107,6 +109,44 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t ); } +function AgentReasoningSummary({ text, detail, theme }: { text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const status = String(objectField(detail, "status") || ""); + const running = ["inProgress", "in_progress", "running", "started", "pending"].includes(status); + return ( +
+ +
+ {running ? : } + {running ? "正在思考" : "思考摘要"} + +
+
+
{text}
+
+ ); +} + +function AgentCommandSummary({ text, detail, theme }: { text: string; detail?: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { + const view = userDetail(detail); + const status = String(objectField(detail, "status") || ""); + const running = ["inProgress", "in_progress", "running", "started", "pending"].includes(status); + const failed = ["failed", "error"].includes(status); + const color = failed ? "#dc2626" : running ? "#d97706" : theme.node.muted; + return ( +
+ { if (!view) event.preventDefault(); }}> +
+ {running ? : } + {failed ? "命令执行失败" : running ? "正在执行命令" : "已执行 1 条命令"} + {view ? : null} +
+
{text}
+
+ {view ?
: null} +
+ ); +} + 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; @@ -241,8 +281,6 @@ function toolCardState(title: string, text: string, detail?: unknown) { } function toolIcon(kind: string | undefined, fallback: ReactNode) { - if (kind === "reasoning") return ; - if (kind === "command") return ; if (kind === "search") return ; if (kind === "file") return ; if (kind === "plan") return ; diff --git a/web/src/components/agent/local-agent-panel.tsx b/web/src/components/agent/local-agent-panel.tsx index 93352cf..fc15c0c 100644 --- a/web/src/components/agent/local-agent-panel.tsx +++ b/web/src/components/agent/local-agent-panel.tsx @@ -632,7 +632,13 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? pushMessage(item); return; } - setAgentState({ messages: currentMessages.map((message, i) => (i === index ? { ...message, ...item } : message)) }); + setAgentState({ + messages: currentMessages.map((message, i) => { + if (i !== index) return message; + const preserveReasoning = item.title === "思考摘要" && item.text === "已完成分析" && Boolean(message.text.trim()) && message.text !== activityPlaceholder("reasoning"); + return { ...message, ...item, ...(preserveReasoning ? { text: message.text } : {}) }; + }), + }); }; const appendActivityDelta = (item: AgentEventItem) => {