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:
1 parent
793b87e3b6
commit
919dc02200
5 files changed
+263
-39
No files matched your search
@@ -54,10 +54,13 @@ import {
|
||||
activitySummary,
|
||||
authDenial,
|
||||
buildConversation,
|
||||
callCounts,
|
||||
callCountText,
|
||||
capabilityTitle,
|
||||
deniedReason,
|
||||
durationLabel,
|
||||
isDenied,
|
||||
lastReportText,
|
||||
runningText,
|
||||
toolSummary,
|
||||
} from "./conversation-model.ts";
|
||||
@@ -271,6 +274,7 @@ export function StepSummary({
|
||||
const denied = calls.filter(isDenied).length;
|
||||
const failed = calls.filter((c) => c.status === "failed" && !isDenied(c)).length;
|
||||
const span = activitySpan(activity);
|
||||
const counted = running ? "" : callCountText(callCounts(calls), "调用");
|
||||
const thinking = activity.some((a) => a.kind === "thinking");
|
||||
const head = current
|
||||
? `${done ? `${done},` : ""}${runningText(current)}…`
|
||||
@@ -292,6 +296,7 @@ export function StepSummary({
|
||||
<span className="min-w-0 truncate">{head}</span>
|
||||
{denied ? <span className="shrink-0">· {denied} 项没做</span> : null}
|
||||
{failed ? <span className="shrink-0 text-destructive">· {failed} 项失败</span> : null}
|
||||
{counted ? <span className="shrink-0 tabular-nums">· {counted}</span> : null}
|
||||
{span !== null && !running ? <span className="shrink-0 tabular-nums">· {durationLabel(span)}</span> : null}
|
||||
<ChevronRight aria-hidden className={cn("size-3.5 shrink-0", CHEVRON_CLASS, open && "rotate-90")} />
|
||||
</button>
|
||||
@@ -473,15 +478,32 @@ export function AgentConversation({
|
||||
// 等选方案 / 等审批时卡片本身就是提示,不再显示"等待中…"
|
||||
const lastPart = lastBlock?.kind === "turn" ? lastBlock.parts.at(-1) : undefined;
|
||||
const cardWaiting = run.status === "waiting" && (lastPart?.kind === "options" || lastPart?.kind === "approval");
|
||||
const showTyping = active && tail?.kind !== "text" && tail?.kind !== "thinking" && !toolRunning && !cardWaiting;
|
||||
// 本轮的实时状态(AG-4):「思考中 · 已调用 3 次工具、1 次 MCP」;本轮结束后计数并入各段进度行
|
||||
const turnCalls =
|
||||
lastBlock?.kind === "turn"
|
||||
? lastBlock.parts.flatMap((p) =>
|
||||
p.kind === "segment" ? p.activity.flatMap((a) => (a.kind === "tool" ? [a.call] : [])) : [],
|
||||
)
|
||||
: [];
|
||||
const turnCount = callCountText(callCounts(turnCalls));
|
||||
const showTyping = active && !cardWaiting;
|
||||
const typingState =
|
||||
run.status === "waiting"
|
||||
? `${run.summary === "等待选择方案" || run.summary === "等待审批" ? run.summary : "等待中"}`
|
||||
: run.status === "queued"
|
||||
? "排队中"
|
||||
: toolRunning
|
||||
? "调用工具中"
|
||||
: tail?.kind === "text"
|
||||
? "回答中"
|
||||
: "思考中";
|
||||
const typing = showTyping ? (
|
||||
<span className="inline-flex items-center gap-2 text-meta text-muted-foreground">
|
||||
<MessageTyping label={run.status === "waiting" ? "等待中" : "正在处理"} />
|
||||
{run.status === "waiting"
|
||||
? `${run.summary === "等待选择方案" || run.summary === "等待审批" ? run.summary : "等待中"}…`
|
||||
: run.status === "queued"
|
||||
? "排队中…"
|
||||
: "正在处理…"}
|
||||
<span className="inline-flex items-center gap-2 text-meta text-muted-foreground" data-slot="turn-status">
|
||||
<MessageTyping label={typingState} />
|
||||
<span className="tabular-nums">
|
||||
{typingState}
|
||||
{turnCount ? ` · ${turnCount}` : "…"}
|
||||
</span>
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
@@ -521,7 +543,6 @@ export function AgentConversation({
|
||||
{/* 流式中按原文显示(半截 Markdown 会闪),结束后按安全子集渲染 */}
|
||||
{human ? <Markdown text={human.text} /> : t.text}
|
||||
</StreamingResponse>
|
||||
{human?.replaced.length ? <ReplacedIds replaced={human.replaced} /> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -565,32 +586,6 @@ export function AgentConversation({
|
||||
);
|
||||
}
|
||||
|
||||
/** 汇报里被换掉的原文(ID、能力、原因码),收在技术信息里 */
|
||||
function ReplacedIds({ replaced }: { replaced: { from: string; to: string }[] }) {
|
||||
const uniq = [...new Map(replaced.map((r) => [r.from, r] as const)).values()];
|
||||
return (
|
||||
<TechInfo
|
||||
title="正文原文"
|
||||
className="mt-1"
|
||||
items={[
|
||||
{
|
||||
label: "换掉的标识",
|
||||
value: (
|
||||
<ul className="space-y-0.5">
|
||||
{uniq.map((r) => (
|
||||
<li key={r.from}>
|
||||
{r.from} → {r.to || "(省略)"}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
wide: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 实际结果 ──
|
||||
|
||||
/** 实际结果的一句话(服务端从下载计划 / 下载任务 / 整理记录汇总,不采信模型文本) */
|
||||
@@ -655,6 +650,8 @@ export function ResultLine({
|
||||
]
|
||||
: [];
|
||||
const bad = report?.state === "failed" || report?.state === "partial";
|
||||
const last = lastReportText(live.items);
|
||||
const rawReport = last && humanizeReport(last).replaced.length ? last : null;
|
||||
const err = run.error ? humanizeReason(String((run.error as { message?: unknown }).message ?? "")) : null;
|
||||
return (
|
||||
<section aria-label="实际结果" className="flex w-full min-w-0 flex-col gap-1 border-t border-border/60 pt-2">
|
||||
@@ -701,6 +698,8 @@ export function ResultLine({
|
||||
{ label: "费用上限", value: run.budget.maxCostUsd ? formatUsd(run.budget.maxCostUsd) : null },
|
||||
{ label: "时长上限", value: run.budget.maxDurationSec ? `${run.budget.maxDurationSec} 秒` : null },
|
||||
{ label: "汇总", value: report?.summary ?? null, wide: true, mono: false },
|
||||
// 正文里的 ID / 能力名 / 原因码已换成人话;排查用的汇报原文只放这里
|
||||
{ label: "原文", value: rawReport, wide: true, mono: false },
|
||||
{ label: "会话 ID", value: run.id, wide: true },
|
||||
{ label: "原始错误", value: run.error ? JSON.stringify(run.error) : null, wide: true },
|
||||
]}
|
||||
|
||||
Reference in new issue
Block a user