fix(canvas): Agent 面板 - 读清单与看画面分档、操作行折叠动效与时间线标识清理

(cherry picked from commit 6c0cadcf9d0dad48a065598ff57b4bde1e135fcd)
This commit is contained in:
AetherNo2332 committed 2026-09-25 20:57:09 +08:00
1 parent 73ca5a5e9f
commit 27afacd5ac
10 files changed
+477 -81

No files matched your search

+25 -4
View File
@@ -33,14 +33,35 @@ test("empty read and operation events stay flat instead of rendering empty cards
});
test("canvas reads show one node by default and expose the collapsed count", () => {
const html = renderToStaticMarkup(<AgentToolCard title="canvas_get_state" text="已读取当前画布" detail={{ eventType: "tool_completed", result: { nodes: [
const html = renderToStaticMarkup(<AgentToolCard title="canvas_get_state" text="工具执行成功" detail={{ eventType: "tool_completed", result: { nodes: [
{ id: "node-1", title: "第一个节点", type: "image" },
{ id: "node-2", title: "第二个节点", type: "text" },
{ id: "node-3", title: "第三个节点", type: "video" },
] } }} theme={canvasThemes.dark} onFocusNode={() => {}} />);
expect(html).toContain("读取了图片节点《第一个节点》");
expect(html).not.toContain("读取了文本节点《第二个节点》");
expect(html).toContain("已折叠 2 个节点,展开查看");
// 清单档的 chip 必须是"存在式":它只说明画布上有这个节点,没看过它的画面
expect(html).toContain("画布上有图片节点《第一个节点》");
expect(html).not.toContain("读取了图片节点《第一个节点》");
expect(html).not.toContain("画布上有文本节点《第二个节点》");
expect(html).toContain("另有 2 个节点只是清单(未查看画面),展开查看");
expect(html).toContain("已读取画布清单(未查看画面)");
});
test("the vision card points at the image it actually sent", () => {
const html = renderToStaticMarkup(<AgentToolCard title="canvas_inspect_image" text="工具执行成功" detail={{ eventType: "tool_completed", result: { nodeId: "node-9", title: "封面.png", batchImages: [{ nodeId: "node-8", title: "剧照.png" }, { nodeId: "node-9", title: "封面.png" }] } }} theme={canvasThemes.dark} onFocusNode={() => {}} />);
expect(html).toContain("查看画面");
expect(html).toContain("agent-tool-row--vision");
expect(html).toContain("已附上 2 张画面");
expect(html).toContain("查看了图片节点《封面.png》");
expect(html).toContain("查看了图片节点《剧照.png》");
expect(html).not.toContain("操作画布");
expect(html).not.toContain("操作已完成");
});
test("a reused observation is not reported as a fresh look at the picture", () => {
const html = renderToStaticMarkup(<AgentToolCard title="canvas_inspect_image" text="工具执行成功" detail={{ eventType: "tool_completed", result: { nodeId: "node-9", title: "封面.png", repeat: true, reuseObservation: true } }} theme={canvasThemes.dark} onFocusNode={() => {}} />);
expect(html).toContain("复用已记录的观察,未重复看图");
expect(html).toContain("复用观察:图片节点《封面.png》");
expect(html).not.toContain("查看了");
});
test("failed tools retain a visible failure label", () => {
+45 -10
View File
@@ -5,7 +5,8 @@ import { canvasThemes } from "@/lib/canvas-theme";
const step = (id: string, title: string, text: string, detail: unknown = { eventType: "tool_completed" }): CloudAgentChatMessage => ({ id, role: "tool", title, text, detail });
const steps: CloudAgentChatMessage[] = [step("t1", "canvas_get_state", "已读取当前画布"), step("t2", "model_list", "已获取可用模型")];
const steps: CloudAgentChatMessage[] = [step("t1", "canvas_get_state", "工具执行成功"), step("t2", "model_list", "工具执行成功")];
const viewed = (id: string, title: string): CloudAgentChatMessage => step(id, "canvas_inspect_image", "工具执行成功", { eventType: "tool_completed", result: { nodeId: id, title } });
test("operations fold into one line that reports only the latest action", () => {
for (const theme of [canvasThemes.light, canvasThemes.dark]) {
@@ -13,18 +14,31 @@ test("operations fold into one line that reports only the latest action", () =>
expect(html).toContain("agent-operation-feed");
expect(html).toContain('aria-expanded="false"');
expect(html).toContain("已获取可用模型");
expect(html).not.toContain("已读取当前画布");
expect(html).not.toContain("已读取画布清单");
expect(html).not.toContain("agent-operation-list");
expect(html).toContain("2 步");
expect(html).toContain("读取信息");
}
});
test("a single step keeps the line clean without a step counter", () => {
const html = renderToStaticMarkup(<AgentOperationFeed items={[steps[0]]} theme={canvasThemes.light} />);
expect(html).toContain("已读取当前画布");
// 只有一步时不加计数徽标;无障碍名称里仍会说明步数与最新一步。
expect(html).not.toContain('class="agent-operation-count"');
expect(html).toContain('aria-label="展开 1 步操作记录,最新一步:已读取当前画布"');
test("the folded line names the tier it is reporting", () => {
const read = renderToStaticMarkup(<AgentOperationFeed items={[steps[0]]} theme={canvasThemes.light} />);
expect(read).toContain("读取清单");
expect(read).toContain('data-agent-category="read"');
expect(read).not.toContain("操作画布");
expect(read).toContain('aria-label="展开 1 步读取清单记录,最新一步:已读取画布清单(未查看画面)"');
// 只有一步时不加计数徽标
expect(read).not.toContain('class="agent-operation-count"');
const vision = renderToStaticMarkup(<AgentOperationFeed items={[viewed("n1", "剧照1.png"), viewed("n2", "封面.png")]} theme={canvasThemes.light} />);
expect(vision).toContain("查看画面");
expect(vision).toContain('data-agent-category="vision"');
expect(vision).toContain("查看了 2 张画面 · 最新《封面.png》");
expect(vision).not.toContain("操作已完成");
const operate = renderToStaticMarkup(<AgentOperationFeed items={[step("t9", "canvas_apply_ops", "工具执行成功", { eventType: "canvas_updated" })]} theme={canvasThemes.light} />);
expect(operate).toContain("修改画布");
expect(operate).toContain('data-agent-category="operate"');
});
test("a failed step is marked and expanded instead of hidden behind the folded line", () => {
@@ -34,7 +48,28 @@ test("a failed step is marked and expanded instead of hidden behind the folded l
expect(html).toContain('aria-expanded="true"');
expect(html).toContain("更新画布内容失败");
expect(html).toContain("agent-operation-list");
// 展开的完整记录仍然是原样的工具卡(含任务 ID 与历史步骤)。
expect(html).toContain("已读取当前画布");
// 展开的完整记录仍然是原样的工具卡(含任务 ID 与历史步骤),失败那步的文字是红的
expect(html).toContain("已读取画布清单(未查看画面)");
expect(html).toContain("任务 ID:task-1");
expect(html).toContain("#dc2626");
});
test("the shimmer only runs while the panel says the segment is live", () => {
const idle = renderToStaticMarkup(<AgentOperationFeed items={steps} theme={canvasThemes.light} />);
const running = renderToStaticMarkup(<AgentOperationFeed items={steps} theme={canvasThemes.light} live />);
expect(idle).not.toContain("is-live");
expect(running).toContain("is-live");
// 失败的那一段即使是末尾也不流光:红色静态文字承担语义
const failedLive = renderToStaticMarkup(<AgentOperationFeed items={[step("t3", "canvas_apply_ops", "", { eventType: "tool_failed" })]} theme={canvasThemes.light} live />);
expect(failedLive).not.toContain("is-live");
});
test("style contract: no per-step status ticks, vision tint and shimmer stay token-driven", async () => {
const css = await Bun.file(new URL("../src/components/canvas/canvas-cloud-agent.css", import.meta.url)).text();
// 展开后的每一步不再渲染状态勾/叉(一列绿勾会把记录读成成绩单)
expect(css).toContain(".agent-operation-list .agent-tool-status {");
// 流光动画只挂在 .is-live 上:任务完成即停
expect(css).toContain(".agent-operation-feed.is-live .agent-operation-latest {");
// 类别取色走面板注入的主题 token,不写字面值
expect(css).toContain("--agent-tool-accent: var(--agent-accent, var(--foreground));");
});
+46 -3
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { agentOperationFailed, agentOperationLabel, buildAgentFeedSegments, isAgentCarrierRecord, isAgentOperationRecord } from "@/lib/canvas/agent-operation-feed";
import { agentOperationCategory, agentOperationFailed, agentOperationLabel, agentOperationSegmentLabel, buildAgentFeedSegments, isAgentCarrierRecord, isAgentOperationRecord } from "@/lib/canvas/agent-operation-feed";
type Row = { id: string; role: string; title?: string; text: string; detail?: unknown; planItems?: { id: string }[]; question?: unknown };
@@ -49,11 +49,11 @@ describe("Agent operation feed", () => {
});
it("reports the latest step, including an in-flight and a failed one", () => {
expect(agentOperationLabel(step("t1", "canvas_get_state", "已读取当前画布"))).toBe("已读取当前画布");
expect(agentOperationLabel(step("t1", "canvas_get_state", "工具执行成功"))).toBe("已读取画布清单(未查看画面)");
expect(agentOperationLabel(step("t2", "canvas_apply_ops", "", {}))).toBe("准备更新画布内容");
expect(agentOperationLabel(step("t3", "canvas_apply_ops", "", { eventType: "tool_failed" }))).toBe("更新画布内容失败");
expect(agentOperationFailed(step("t3", "canvas_apply_ops", "", { eventType: "tool_failed" }))).toBe(true);
expect(agentOperationFailed(step("t1", "canvas_get_state", "已读取当前画布"))).toBe(false);
expect(agentOperationFailed(step("t1", "canvas_get_state", "工具执行成功"))).toBe(false);
});
it("reports the retry group instead of a stale completion", () => {
@@ -63,3 +63,46 @@ describe("Agent operation feed", () => {
expect(agentOperationLabel(recovered)).toBe("自动纠正后已恢复 · 2/3 次尝试");
});
});
describe("Agent operation segment label", () => {
const viewed = (id: string, title: string, extra: Record<string, unknown> = {}) => step(id, "canvas_inspect_image", "工具执行成功", { eventType: "tool_completed", result: { nodeId: id, title, ...extra } });
it("aggregates a vision run by the images that really went out", () => {
const items = [viewed("n1", "剧照1.png"), viewed("n2", "剧照2.png"), viewed("n3", "封面.png")];
expect(agentOperationSegmentLabel(items)).toBe("查看了 3 张画面 · 最新《封面.png》");
expect(agentOperationCategory(items[0])).toBe("vision");
});
it("counts a batch receipt once per image instead of once per call", () => {
const items = [
viewed("n1", "剧照1.png"),
step("last", "canvas_inspect_image", "工具执行成功", {
eventType: "tool_completed",
result: {
nodeId: "n3",
title: "剧照3.png",
batchImages: [
{ nodeId: "n1", title: "剧照1.png" },
{ nodeId: "n2", title: "剧照2.png" },
{ nodeId: "n3", title: "剧照3.png" },
],
},
}),
];
expect(agentOperationSegmentLabel(items)).toBe("查看了 3 张画面 · 最新《剧照3.png》");
});
it("does not count a reused observation or a read-loop receipt as a delivered picture", () => {
const items = [viewed("n1", "剧照1.png", { repeat: true, reuseObservation: true }), viewed("n2", "剧照2.png", { repeat: true, refreshIgnored: true })];
// 一张画面都没送出去:退回最新一步自己的摘要,绝不报"查看了 N 张画面"
expect(agentOperationSegmentLabel(items)).toBe("本轮重复查看,只回执文字、未附图");
expect(agentOperationSegmentLabel(items)).not.toContain("查看了");
});
it("keeps the newest step's own copy when the segment is not about pictures", () => {
const items = [step("t1", "canvas_get_state", "工具执行成功"), step("t2", "canvas_apply_ops", "工具执行成功", { eventType: "canvas_updated" })];
expect(agentOperationSegmentLabel(items)).toBe("画布内容已保存至服务端");
expect(agentOperationCategory(items[1])).toBe("operate");
expect(agentOperationSegmentLabel([])).toBe("");
});
});
+77 -3
View File
@@ -1,15 +1,89 @@
import { describe, expect, it } from "bun:test";
import { agentToolCategory, agentToolCategoryLabel, agentToolErrorClassLabel, agentToolStatus, friendlyAgentToolSummary } from "@/lib/canvas/agent-tool-presentation";
import { AGENT_TOOL_NAMES, agentToolCategory, agentToolCategoryLabel, agentToolErrorClassLabel, agentToolStatus, agentVisionFailureSummary, friendlyAgentToolSummary } from "@/lib/canvas/agent-tool-presentation";
describe("Agent tool presentation", () => {
it("separates read, create, and canvas operation activity", () => {
expect(agentToolCategory("canvas_get_state", { eventType: "tool_completed" })).toBe("read");
expect(agentToolCategoryLabel("canvas_get_state", "read")).toBe("读取节点");
expect(agentToolCategoryLabel("canvas_get_state", "read")).toBe("读取清单");
expect(agentToolCategoryLabel("model_list", "read")).toBe("读取信息");
expect(agentToolCategory("generate_media", { eventType: "generation_task_created" })).toBe("create");
expect(agentToolCategoryLabel("generate_media", "create")).toBe("创建节点");
expect(agentToolCategory("canvas_apply_ops", { eventType: "canvas_updated", actions: [{ action: "updated" }] })).toBe("operate");
expect(agentToolCategoryLabel("canvas_apply_ops", "operate")).toBe("修改画布");
expect(agentToolCategory("canvas_apply_ops", { eventType: "canvas_updated", actions: [{ action: "created" }] })).toBe("create");
});
it("keeps the canvas listing distinct from actually seeing a picture", () => {
const summary = friendlyAgentToolSummary("canvas_get_state", "工具执行成功", { eventType: "tool_completed", result: { nodes: [{ id: "n1", title: "图.png", type: "image" }] } });
expect(summary).toBe("已读取画布清单(未查看画面)");
expect(summary).toContain("清单");
expect(summary).toContain("未查看画面");
// 不变量 ①:只有"查看画面"档可以说图片/画面相关的话
expect(summary).not.toContain("读取了");
expect(summary).not.toContain("画面已");
});
it("treats the vision tool as its own tier", () => {
expect(agentToolCategory("canvas_inspect_image", { eventType: "tool_completed" })).toBe("vision");
expect(agentToolCategoryLabel("canvas_inspect_image", "vision")).toBe("查看画面");
});
it("describes the four image inspection receipts without claiming more than happened", () => {
const normal = friendlyAgentToolSummary("canvas_inspect_image", "工具执行成功", { eventType: "tool_completed", result: { nodeId: "n1", title: "图.png" } });
expect(normal).toBe("已附上《图.png》的画面");
expect(normal).toContain("画面");
expect(normal).not.toContain("操作画布");
expect(normal).not.toContain("操作已完成");
const pending = friendlyAgentToolSummary("canvas_inspect_image", "", { result: { nodeId: "n1", title: "图.png" } }, true);
expect(pending).toBe("准备查看画面《图.png》");
const reused = friendlyAgentToolSummary("canvas_inspect_image", "工具执行成功", { eventType: "tool_completed", result: { nodeId: "n1", title: "图.png", repeat: true, reuseObservation: true } });
expect(reused).toBe("复用已记录的观察,未重复看图");
const repeated = friendlyAgentToolSummary("canvas_inspect_image", "工具执行成功", { eventType: "tool_completed", result: { nodeId: "n1", title: "图.png", repeat: true, refreshIgnored: true } });
expect(repeated).toBe("本轮重复查看,只回执文字、未附图");
const batch = friendlyAgentToolSummary("canvas_inspect_image", "工具执行成功", { eventType: "tool_completed", result: { nodeId: "n3", title: "图3.png", batchImages: [{ nodeId: "n1" }, { nodeId: "n2" }, { nodeId: "n3" }] } });
expect(batch).toBe("已附上 3 张画面");
});
it("splits the four image inspection failures and carries the numbers it gets", () => {
expect(agentVisionFailureSummary("cloud agent image inspection budget exhausted: 已用 7 次、本轮额度 5 次")).toBe("本轮识图额度已用完(已用 7 / 额度 5)");
expect(agentVisionFailureSummary("cloud agent image inspection budget exhausted")).toBe("本轮识图额度已用完");
expect(agentVisionFailureSummary("Agent 连续重复读取同一份canvas_inspect_image结果,本轮已停止以避免继续消耗模型调用;请让 Agent 使用已有结果继续,不要再次读取")).toBe("同一张图同版本重复读取,已拦下");
expect(agentVisionFailureSummary("本批看图数量已达到当前模型限制:本批已附 3 张、单次上限 4 张。请在本批结果返回后、下一步再继续查看剩余图片。")).toBe("本批图片数已达模型上限(已附 3 / 上限 4),下一步继续");
expect(agentVisionFailureSummary("当前模型未声明图片输入能力")).toBe("当前渠道模型未声明图片输入能力");
expect(agentVisionFailureSummary("指定节点不在当前画布")).toBe("查看画面未完成");
expect(friendlyAgentToolSummary("canvas_inspect_image", "当前模型未声明图片输入能力", { eventType: "tool_failed" })).toBe("当前渠道模型未声明图片输入能力");
});
it("never labels an unregistered tool as a canvas write", () => {
const name = "canvas_dance_future_tool";
const category = agentToolCategory(name, { eventType: "tool_completed" });
expect(category).toBe("other");
expect(agentToolCategoryLabel(name, category)).toBe("其他操作");
expect(agentToolCategoryLabel(name, category)).not.toContain("画布");
expect(friendlyAgentToolSummary(name, "工具执行成功", { eventType: "tool_completed" })).toBe(`已完成:${name}`);
expect(friendlyAgentToolSummary(name, "", undefined, true)).toBe(`准备执行 ${name}`);
// 历史事件里的 skills_load 仍在只读档,不能落进写操作
expect(agentToolCategory("skills_load")).toBe("read");
});
it("gives every backend tool name a category and copy", () => {
for (const name of AGENT_TOOL_NAMES) {
const category = agentToolCategory(name, { eventType: "tool_completed" });
expect(["read", "vision", "create", "operate", "other"]).toContain(category);
expect(agentToolCategoryLabel(name, category).length).toBeGreaterThan(0);
const summary = friendlyAgentToolSummary(name, "工具执行成功", { eventType: "tool_completed" });
expect(summary.length).toBeGreaterThan(0);
// 兜底句不能再是"操作已完成"这种既不说做了什么、又暗示写操作的旧文案
expect(["操作已完成", "准备执行操作"]).not.toContain(summary);
}
// 未登记的工具(上游将来新增)必须点名自己
expect(friendlyAgentToolSummary("canvas_brand_new_tool", "工具执行成功", { eventType: "tool_completed" })).toBe("已完成:canvas_brand_new_tool");
});
it("distinguishes media submission from a completed canvas result", () => {
expect(friendlyAgentToolSummary("generate_media", "", { eventType: "generation_task_created" })).toBe("媒体节点已创建,生成任务已提交");
expect(friendlyAgentToolSummary("generate_media", "", { eventType: "tool_completed" })).toBe("生成结果已回写画布节点");
@@ -40,7 +114,7 @@ describe("Agent tool presentation", () => {
expect(friendlyAgentToolSummary("canvas_apply_ops", "", undefined, true)).toBe("准备更新画布内容");
expect(friendlyAgentToolSummary("skills_load", "已从用户技能库固定加载 3 个技能")).toBe("已加载 3 个 Skills 技能");
expect(agentToolStatus("canvas_get_state", "失败")).toBe("failed");
expect(friendlyAgentToolSummary("canvas_get_state", "无法访问", { eventType: "tool_failed" })).toBe("获取画布内容失败");
expect(friendlyAgentToolSummary("canvas_get_state", "无法访问", { eventType: "tool_failed" })).toBe("获取画布清单失败");
});
});