feat(web): 智能体对话重做(AGUI-3)

- 会话流:用户消息 → 一行进度 → 结论正文 → 没做的事 → 一行实际结果。进度行把一段里的工具调用、思考和过程中的话
  聚成一行(1 次也折叠),按能力 verb/noun 人话计数(「查了站点、2 个下载任务和媒体库 · 1 项没做 · 3 秒」),
  展开每步一行;工具名、调用 ID、幂等键、输入输出进技术信息。去掉回合头、「模型说的」重复卡和系统核实大卡。
- agent.plan / agent.propose_options 不作为工具调用出现(旧事件回放同样跳过);被拒调用不出红卡,
  回合末尾一行「没做:检测站点,需要你批准 [批准并重试]」(非计划类能力建一次性持续授权后重试)。
- 方案卡:每个方案一个按钮,点一下就选定;推荐项第一位、主按钮;按钮写服务端给的动作要点;风险折叠;
  「都不要…」点开才出原因框;选完收成一行。审批卡只留摘要和批准 / 拒绝,计划 ID、hash、版本、到期进技术信息。
- 会话页去掉右侧栏和页头目标,步骤 / 子会话 / 模型 / 预算 / ID 进顶部概况条;输入区只有输入框和发送 / 停止,
  / 指令 /先给方案、/重跑(「先看方案再执行」开关原本只是新建时的本地状态,不需要迁移)。
- 抽屉去掉「当前 / 历史」Tab:头部「历史」弹层(最近 20 条)+「新对话」;上下文显示成名称标签,可去掉。
- 会话列表:搜索 + 全部 / 等你处理 / 失败;等你处理置顶,子会话收在父会话下,正常结束不加徽章。
- 入口页与空态给 3 个快捷目标。mock 加审查报告 3 个示例的演示数据;smoke 改测历史弹层。
- 组件测试:聚合、隐藏、拒绝一行、方案一键选定、历史置顶与子会话、指令。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 22:41:34 +08:00
1 parent 2ab6d452dd
commit 6fcf82afb0
17 files changed
+2381 -1846

No files matched your search

@@ -1,17 +1,9 @@
import type { AgentRunEvent } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import {
authDenial,
type ConvEntry,
groupConversation,
isDenied,
isLongReport,
pairAudit,
runPlanIds,
toolSummary,
} from "./agent-conversation.tsx";
import { matchesHistory, rangeStart } from "./agent-session-list.tsx";
import { authDenial, isDenied, pairAudit, runPlanIds, toolSummary } from "./agent-conversation.tsx";
import { matchesHistory } from "./agent-session-list.tsx";
import { contextFromPath, contextPrefix, shortGoalTitle, splitContextGoal } from "./agent-store.ts";
import { isLongReport } from "./conversation-model.ts";
import { foldEvents, type StreamItem, type ToolCallView } from "./use-agent-stream.ts";
const call = (p: Partial<ToolCallView>): ToolCallView => ({
@@ -100,7 +92,7 @@ describe("事件折叠", () => {
const start = {
type: "tool_call_start",
toolCallId: "tc1",
capabilityId: "agent.propose_options",
capabilityId: "download.submit",
inputSummary: "{}",
};
const items = foldEvents([
@@ -109,7 +101,7 @@ describe("事件折叠", () => {
ev(3, {
type: "tool_call_end",
toolCallId: "tc1",
capabilityId: "agent.propose_options",
capabilityId: "download.submit",
outputSummary: "ok",
isError: false,
recovery: true,
@@ -122,67 +114,29 @@ describe("事件折叠", () => {
});
});
describe("会话排版(AGUI-2)", () => {
const e = (key: string, role: ConvEntry["role"], foldable = false): ConvEntry => ({
key,
at: key,
role,
foldable,
label: foldable ? "查本地库存" : undefined,
node: null,
});
it("同一助手回合归为一组,用户 / 系统条目打断回合", () => {
const blocks = groupConversation([e("a", "assistant"), e("b", "assistant"), e("c", "user"), e("d", "assistant")]);
expect(blocks.map((b) => b.kind)).toEqual(["turn", "single", "turn"]);
const first = blocks[0];
expect(first?.kind === "turn" && first.parts.length).toBe(2);
});
it("连续 ≥3 条可折叠的活动并成一行,不足 3 条原样展示", () => {
const [t] = groupConversation([
e("1", "assistant", true),
e("2", "assistant", true),
e("3", "assistant", true),
e("4", "assistant"),
e("5", "assistant", true),
e("6", "assistant", true),
]);
expect(t?.kind).toBe("turn");
if (t?.kind !== "turn") return;
expect(t.parts.map((p) => (p.kind === "fold" ? `fold${p.entries.length}` : p.entry.key))).toEqual([
"fold3",
"4",
"5",
"6",
]);
});
it("工具卡一行摘要", () => {
describe("工具结果摘要", () => {
it("展开后每步一行的「→ 结果」", () => {
expect(toolSummary({ status: "running", outputSummary: null })).toBe("等待结果…");
expect(toolSummary({ status: "succeeded", outputSummary: '{"items":[1,2,3]}' })).toBe("3 条结果");
expect(toolSummary({ status: "succeeded", outputSummary: '{"items":[1,2,3]}' })).toBe("3 条");
expect(toolSummary({ status: "failed", outputSummary: '{"error":{"code":"x","message":"超时"}}' })).toBe("超时");
expect(toolSummary({ status: "succeeded", outputSummary: '{"found":true,"count":2}' })).toBe(
"found: true · count: 2",
);
// 没有人话可写的对象不上屏(字段名进技术信息)
expect(toolSummary({ status: "succeeded", outputSummary: '{"found":true,"count":2}' })).toBeNull();
expect(toolSummary({ status: "succeeded", outputSummary: '{"plan":{"id":"x"…' })).toBeNull();
expect(toolSummary({ status: "succeeded", outputSummary: "下载器连接超时" })).toBe("下载器连接超时");
});
it("长汇报判定", () => {
it("长正文判定", () => {
expect(isLongReport("短")).toBe(false);
expect(isLongReport("x".repeat(601))).toBe(true);
expect(isLongReport(Array(14).fill("行").join("\n"))).toBe(true);
});
});
describe("会话历史筛选", () => {
const now = new Date("2026-09-29T12:00:00Z").getTime();
const run = { id: "abcd1234-0000", goal: "[上下文 作品 x] 把 沙丘 收进来", createdAt: "2026-09-25T12:00:00Z" };
describe("会话历史搜索", () => {
const run = { id: "abcd1234-0000", goal: "[上下文 作品 x] 把 沙丘 收进来" };
it("搜索匹配目标或 ID 前缀", () => {
expect(matchesHistory(run, { q: "沙丘", range: "" }, now)).toBe(true);
expect(matchesHistory(run, { q: "abcd", range: "" }, now)).toBe(true);
expect(matchesHistory(run, { q: "奥本海默", range: "" }, now)).toBe(false);
});
it("时间范围按创建时间", () => {
expect(matchesHistory(run, { q: "", range: "7d" }, now)).toBe(true);
expect(matchesHistory(run, { q: "", range: "today" }, now)).toBe(false);
expect(rangeStart("", now)).toBeNull();
expect(matchesHistory(run, { q: "沙丘" })).toBe(true);
expect(matchesHistory(run, { q: "abcd" })).toBe(true);
expect(matchesHistory(run, { q: "奥本海默" })).toBe(false);
});
});