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

@@ -42,7 +42,7 @@ import {
import { RunPlan } from "./agent-interaction.tsx";
import { shortGoalTitle } from "./agent-store.ts";
import { SubagentTree } from "./agent-subagent-tree.tsx";
import { stepProgress } from "./conversation-model.ts";
import { callCounts, sessionStatsParts, stepProgress } from "./conversation-model.ts";
import {
NEW_RUN_COMMANDS,
PROPOSE_FIRST,
@@ -140,6 +140,47 @@ export function GoalComposer({
* 顶部概况条:一行「步骤 4/5 · 当前:核对下载任务」;展开看步骤清单、子会话和技术信息(模型、预算、用量、ID)。
* 不 sticky、不压在会话上;没有步骤和子会话时只在运行中显示状态。
*/
/**
* 会话统计(AG-4):概况条上方恒定一行小字——工具调用、MCP、已用 token、距上限、费用;
* 运行中随事件流实时更新,结束后固定为快照里的最终值。
*/
export function RunStatsLine({
snapshot,
live,
className,
}: {
snapshot: RunSnapshot;
live: AgentStreamState;
className?: string;
}) {
const r = snapshot.run;
const u = { ...r.usage, ...(live.usage ?? {}) };
const n = callCounts(live.items.flatMap((i) => (i.kind === "tool" ? [i.call] : [])));
const parts = sessionStatsParts(
{
toolCalls: Math.max(r.toolCallCount - n.mcp, n.tools),
mcpCalls: n.mcp,
totalTokens: u.totalTokens ?? null,
maxTokens: r.budget.maxTokens ?? null,
costUsd: u.costUsd ?? null,
},
{ tokens: formatTokens, usd: formatUsd },
);
return (
<section aria-label="会话统计" className={cn("shrink-0", className)}>
{/* 窄屏折行,不截断(费用在最后) */}
<p data-slot="run-stats" className="flex flex-wrap gap-x-1.5 px-1 text-meta text-muted-foreground tabular-nums">
{parts.map((t, i) => (
<span key={t} className="whitespace-nowrap">
{i ? "· " : ""}
{t}
</span>
))}
</p>
</section>
);
}
export function RunOverviewBar({
snapshot,
status,
@@ -338,6 +379,7 @@ export function AgentRunPanel({
</StatusBadge>
</div>
) : null}
<RunStatsLine snapshot={run.data} live={live} className={column} />
<RunOverviewBar
snapshot={run.data}
status={shownStatus}