feat(web): session stats line, live turn status, drop replaced-ID list (AG-4)

- Remove the "换掉的标识" mapping list under humanized reports; the raw
  report text is kept only as "原文" in the result line's 技术信息 (no mapping).
- Session stats line above the overview bar, always shown: tool calls, MCP
  calls (only when any), tokens used and left to the budget (K/M + percent),
  cost; live while running, fixed after the run ends.
- Live turn status: "思考中 / 调用工具中 / 回答中 · 已调用 N 次工具、M 次 MCP";
  after the turn the count merges into each progress line ("调用 N 次工具").
- agent.spawn calls answered with an existing same-goal child (reused=true)
  are not counted in "派出了 N 个子会话".

Screenshots (desktop + mobile): apps/web/test/screenshots/ag4/ (gitignored).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 17:58:15 +08:00
1 parent 793b87e3b6
commit 919dc02200
5 files changed
+263 -39

No files matched your search

+52 -2
View File
@@ -6,9 +6,9 @@ import { MemoryRouter } from "react-router";
import { describe, expect, it } from "vitest";
import type { AgentRunView } from "~/api/agent.ts";
import { qk } from "~/lib/query-keys.ts";
import { AgentConversation, contextLabel, DeniedLines, StepSummary } from "./agent-conversation.tsx";
import { AgentConversation, contextLabel, DeniedLines, ResultLine, StepSummary } from "./agent-conversation.tsx";
import { OptionsCard, optionPoint, orderOptions } from "./agent-interaction.tsx";
import { goalFromInput } from "./agent-run-panel.tsx";
import { goalFromInput, RunStatsLine } from "./agent-run-panel.tsx";
import { arrangeHistory } from "./agent-session-list.tsx";
import { type ActivityItem, activitySummary, buildConversation, deniedReason } from "./conversation-model.ts";
import { parseCommand, RUN_COMMANDS } from "./slash-commands.ts";
@@ -392,3 +392,53 @@ describe("输入区:/ 指令与上下文标签", () => {
expect(contextLabel(null)).toBeNull();
});
});
describe("会话统计与本轮状态(AG-4)", () => {
const humanized = (): AgentRunEvent[] => {
seq = 0;
return [
ev(0, { type: "run_started", goal: "检查站点", resumed: false }),
...tool(1, "t1", "site.list", '{"items":[{"name":"憨憨"}]}'),
ev(3, {
type: "text_delta",
delta: "site.list 返回站点 4e71672f-0000-4000-8000-000000000000 正常,没有 invalid_input。",
}),
ev(9, { type: "run_completed", finalText: "", usage: { totalTokens: 1000, costUsd: 0.01 } }),
];
};
it("正文里不再列「换掉的标识」;原始汇报只在结果行的技术信息「原文」里", () => {
const live = liveOf(humanized());
const t = text(render(createElement(AgentConversation, { run: RUN, live })));
expect(t).not.toContain("换掉的标识");
expect(t).not.toContain("正文原文");
expect(t).not.toContain("→ 列出站点");
const result = render(createElement(ResultLine, { run: RUN, report: null, live }));
expect(text(result)).toContain("原文");
expect(result).toContain("4e71672f-0000-4000-8000-000000000000");
expect(result).not.toContain("换掉的标识");
});
it("结束后本轮调用次数并入进度行", () => {
const activity: ActivityItem[] = [
{ kind: "tool", key: "c1", call: call({ capabilityId: "site.list" }) },
{ kind: "tool", key: "c2", call: call({ toolCallId: "c2", capabilityId: "inventory.query" }) },
];
const html = render(createElement(StepSummary, { activity, running: false }));
expect(text(html.slice(0, html.indexOf("</button>")))).toContain("调用 2 次工具");
});
it("运行中:思考状态与本轮已调用次数放在一行", () => {
const events = humanized().slice(0, 3);
const live = { ...liveOf(events), finished: false };
const t = text(render(createElement(AgentConversation, { run: { ...RUN, status: "running" }, live })));
expect(t).toContain("思考中 · 已调用 1 次工具");
});
it("概况条上方的会话统计:工具调用、token(K)、剩余、费用", () => {
const live = liveOf(humanized());
const snapshot = {
run: { ...RUN, toolCallCount: 1, budget: { maxTokens: 200_000 }, usage: { totalTokens: 45_200, costUsd: 0.03 } },
report: null,
messages: [],
} as never;
const t = text(render(createElement(RunStatsLine, { snapshot, live })));
expect(t).toContain("工具调用 1 次 · token 45.2K · 剩 154.8K(77%) · 费用 $0.03");
});
});