fix(web): v0.2 collapse Agent workspace debug noise into a 调试信息 dialog (HWLAB #803) (#807)

The Agent workspace (top #code-agent-summary and each agent message card)
exposed too many "未观测" / readiness / providerTrace / runtime-path rows
inline; per-message panels (MessageRuntimePath, MessageSessionContinuity,
MessagePendingContext) ate most of the card width and made the agent trace
events list (the only thing the user really wants to see) read at ~83% of
the card.

This change:
- introduces a reusable <DebugDialog> component (web/hwlab-cloud-web/src/components/shared/DebugDialog.tsx)
  that surfaces the full availability / readiness / runnerTrace / raw JSON
  payload only when the user clicks a 调试信息 button.
- shrinks codeAgentSummaryRows() in state/code-agent-status.ts to only
  return rows with meaningful values; "未观测" / "字段缺失:证据不足" /
  "DEGRADED:运行路径字段不完整" / "无"-only readiness blockers are
  filtered out of the top summary and remain reachable via the dialog.
- rewrites MessageCard to drop the inline MessageRuntimePath /
  MessageSessionContinuity / MessagePendingContext panels and to show a
  single compact footer (timestamp + 调试信息 button). The pending running
  hint is reduced to a one-line tag instead of a 7-row grid.
- makes .message-trace and .message-trace-events fill the message card so
  the trace events list is the dominant element per the issue.
- adds tools/capture-issue-803-noise.mjs so CLI can log into the live or
  local Cloud Web, capture the rendered #code-agent-summary / per-message
  meta / trace widths into a JSON document, and diff before/after the
  refactor.

web:check, web:layout, dev-cloud-workbench-smoke --static, and
dev-cloud-workbench-layout-smoke all pass on the local build.
Issue: pikasTech/HWLAB#803

Co-authored-by: HWLAB Agent <hwlab-agent@pikastech.local>
This commit is contained in:
Lyon
2026-06-04 11:17:18 +08:00
committed by GitHub
parent 10c03e6c32
commit fb267d28b2
5 changed files with 426 additions and 48 deletions
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env node
// HWLAB #803 noise-capture CLI.
//
// Captures the rendered Agent workspace (#code-agent-summary, per-message
// meta/pending/runtime/session/trace cards) from a live or local Cloud Web
// build and prints a structured JSON dump that can be diffed before/after a
// Web UI refactor.
//
// Usage:
// CHROMIUM_PATH=/usr/bin/chromium-browser \
// node tools/capture-issue-803-noise.mjs \
// --url http://74.48.78.17:17666/ --user admin --pass hwlab2026 \
// --png /tmp/hwlab-noise.png
// CHROMIUM_PATH=/usr/bin/chromium-browser \
// NOISE_USER=admin NOISE_PASS=hwlab2026 \
// node tools/capture-issue-803-noise.mjs
import { chromium } from "playwright";
import { setTimeout as sleep } from "node:timers/promises";
function parseArgs(argv) {
const args = {
url: process.env.NOISE_URL ?? "http://74.48.78.17:17666/",
user: process.env.NOISE_USER ?? "admin",
pass: process.env.NOISE_PASS ?? "hwlab2026",
message: process.env.NOISE_MESSAGE ?? null,
viewport: process.env.NOISE_VIEWPORT ?? "1440x900",
png: process.env.NOISE_PNG ?? "/tmp/hwlab-noise.png"
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--url") args.url = argv[++index];
else if (arg === "--user") args.user = argv[++index];
else if (arg === "--pass") args.pass = argv[++index];
else if (arg === "--message") args.message = argv[++index];
else if (arg === "--png") args.png = argv[++index];
else if (arg === "--viewport") args.viewport = argv[++index];
}
const [w, h] = args.viewport.split("x").map(Number);
args.viewportWidth = Number.isFinite(w) ? w : 1440;
args.viewportHeight = Number.isFinite(h) ? h : 900;
return args;
}
const args = parseArgs(process.argv.slice(2));
const launchOptions = { headless: true };
if (process.env.CHROMIUM_PATH) launchOptions.executablePath = process.env.CHROMIUM_PATH;
const browser = await chromium.launch(launchOptions);
const ctx = await browser.newContext({ viewport: { width: args.viewportWidth, height: args.viewportHeight } });
const page = await ctx.newPage();
await page.goto(args.url, { waitUntil: "networkidle", timeout: 30000 });
try {
await page.locator("#login-username").fill(args.user, { timeout: 5000 });
await page.locator("#login-password").fill(args.pass);
await page.locator("#login-submit").click();
await page.locator("#command-input").waitFor({ state: "visible", timeout: 12000 });
} catch (error) {
console.error("[noise] login failed:", error instanceof Error ? error.message : String(error));
}
await page.waitForTimeout(1500);
if (args.message) {
try {
await page.locator("#command-input").fill(args.message, { timeout: 5000 });
await page.locator("#command-send").click();
await sleep(8000);
} catch (error) {
console.error("[noise] submit failed:", error instanceof Error ? error.message : String(error));
}
}
const data = await page.evaluate(() => {
const summary = document.querySelector("#code-agent-summary");
const summaryRect = summary?.getBoundingClientRect();
const summaryText = summary?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 800) ?? null;
const summaryDetail = document.querySelector("#code-agent-summary-detail");
const summaryDetailRect = summaryDetail?.getBoundingClientRect();
const summaryDetailText = summaryDetail?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 1500) ?? null;
const summaryIsOpen = summary ? summary.open === true : null;
const summaryDebugBtn = document.querySelector("#code-agent-debug-trigger");
const convList = document.querySelector("#conversation-list");
const convListRect = convList?.getBoundingClientRect();
const messages = [...document.querySelectorAll(".message-card")].map((card) => {
const rect = card.getBoundingClientRect();
const meta = card.querySelector(".message-meta")?.textContent?.replace(/\s+/gu, " ").trim() ?? null;
const pending = card.querySelector(".message-pending-context")?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 600) ?? null;
const session = card.querySelector(".message-session-context")?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 600) ?? null;
const runtime = card.querySelector(".message-runtime-path")?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 600) ?? null;
const trace = card.querySelector(".message-trace")?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 400) ?? null;
const traceEventsRect = card.querySelector(".message-trace-events")?.getBoundingClientRect();
const debugTrigger = card.querySelector(".debug-dialog-trigger");
const cardWidth = rect.width;
const tracePct = cardWidth > 0 && traceEventsRect ? Math.round((traceEventsRect.width / cardWidth) * 100) : null;
return {
meta,
pending,
session,
runtime,
trace,
debugTriggerText: debugTrigger?.textContent ?? null,
cardWidth: Math.round(cardWidth),
traceEventsWidth: traceEventsRect ? Math.round(traceEventsRect.width) : null,
traceEventsPct: tracePct
};
});
return {
summaryText,
summaryDetailText,
summaryIsOpen,
summaryDebugBtnExists: !!summaryDebugBtn,
summaryRect: summaryRect ? { width: Math.round(summaryRect.width), height: Math.round(summaryRect.height) } : null,
summaryDetailRect: summaryDetailRect ? { width: Math.round(summaryDetailRect.width), height: Math.round(summaryDetailRect.height) } : null,
convListRect: convListRect ? { width: Math.round(convListRect.width), height: Math.round(convListRect.height) } : null,
messageCount: messages.length,
messages
};
});
console.log(JSON.stringify(data, null, 2));
await page.screenshot({ path: args.png, fullPage: false });
await browser.close();
@@ -1,17 +1,15 @@
import type { ReactElement } from "react";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { renderMessageMarkdown } from "../../services/markdown/render";
import type { ChatMessage, CodeAgentAvailability, LiveSurface } from "../../types/domain";
import { formatBeijingTime, jsonPreview, shortToken, toneClass } from "../../utils";
import { codeAgentSummaryRows, classifyCodeAgentStatusSummary } from "../../state/code-agent-status";
import { codeAgentSummaryRows, classifyCodeAgentStatusSummary, codeAgentRuntimePathFromMessage } from "../../state/code-agent-status";
import { agentStatusLabel } from "../../state/agent-status-labels";
import { StateTag } from "../shared/StateTag";
import { DebugDialog, type DebugRow, type DebugSection } from "../shared/DebugDialog";
import { MessageTracePanel } from "./MessageTracePanel";
import { MessageActions } from "./MessageActions";
import { MessagePendingContext } from "./MessagePendingContext";
import { MessageRuntimePath } from "./MessageRuntimePath";
import { MessageSessionContinuity } from "./MessageSessionContinuity";
interface ConversationPanelProps {
messages: ChatMessage[];
@@ -60,7 +58,16 @@ export function ConversationPanel({ messages, availability, live, chatPending, c
</div>
<CodeAgentSummary availability={availability} live={live} latestMessage={latestAgentMessage(messages)} />
<div className="conversation-list" id="conversation-list" aria-live="polite">
{[...intro, ...messages].map((message) => <MessageCard key={message.id} message={message} codeAgentTimeoutMs={codeAgentTimeoutMs} onCancel={() => onCancelMessage(message.id)} onRetry={() => onRetryMessage(message.id)} onReplayTrace={() => onReplayTrace(message.id)} />)}
{[...intro, ...messages].map((message) => (
<MessageCard
key={message.id}
message={message}
codeAgentTimeoutMs={codeAgentTimeoutMs}
onCancel={() => onCancelMessage(message.id)}
onRetry={() => onRetryMessage(message.id)}
onReplayTrace={() => onReplayTrace(message.id)}
/>
))}
</div>
</div>
</div>
@@ -78,27 +85,49 @@ function latestAgentMessage(messages: ChatMessage[]): Record<string, unknown> |
function CodeAgentSummary({ availability, live, latestMessage }: { availability: CodeAgentAvailability | null; live: LiveSurface | null; latestMessage: Record<string, unknown> | null }): ReactElement {
const summary = useMemo(() => classifyCodeAgentStatusSummary({ availability, live, latestMessage }), [availability, live, latestMessage]);
const rows = useMemo(() => codeAgentSummaryRows(summary), [summary]);
const rows = useMemo<DebugRow[]>(() => codeAgentSummaryRows(summary).map((row) => ({ label: row.key, value: row.value, tone: row.tone as DebugRow["tone"] })), [summary]);
const [open, setOpen] = useState(false);
const sections: DebugSection[] = [
{ title: "Code Agent 详情", rows },
{ title: "readiness blockers", rows: summary.readinessBlockers.length > 0
? summary.readinessBlockers.map((blocker) => ({ label: blocker, value: blocker, tone: "blocked" as const }))
: [] , empty: summary.readinessBlockers.length === 0 ? "无" : undefined }
];
return (
<details className={`code-agent-summary tone-border-${toneClass(summary.tone)}`} id="code-agent-summary" data-code-agent-status-kind={summary.kind}>
<summary>
<span className={`code-agent-summary-icon tone-${toneClass(summary.tone)}`} id="code-agent-summary-icon" aria-hidden="true">{summary.icon}</span>
<strong className={`code-agent-summary-label tone-${toneClass(summary.tone)}`} id="code-agent-summary-label">{summary.label}</strong>
<span className="code-agent-summary-capability" id="code-agent-summary-capability">{summary.capabilityLevel}</span>
<span className="code-agent-summary-trace" id="code-agent-summary-trace">trace {summary.lastTraceId}</span>
</summary>
<div className="code-agent-summary-detail" id="code-agent-summary-detail">
<div className="code-agent-summary-rows" role="table" aria-label="Code Agent 状态详情">
{rows.map((row) => (
<div className="code-agent-summary-row" role="row" key={row.key}>
<span className="code-agent-summary-key" role="rowheader">{row.key}</span>
<span className={`code-agent-summary-value tone-${toneClass(row.tone)}`} role="cell">{row.value}</span>
</div>
))}
<div className="code-agent-summary-wrapper">
<details
className={`code-agent-summary tone-border-${toneClass(summary.tone)}`}
id="code-agent-summary"
data-code-agent-status-kind={summary.kind}
open={open}
onToggle={(event) => setOpen((event.currentTarget as HTMLDetailsElement).open)}
>
<summary>
<span className={`code-agent-summary-icon tone-${toneClass(summary.tone)}`} id="code-agent-summary-icon" aria-hidden="true">{summary.icon}</span>
<strong className={`code-agent-summary-label tone-${toneClass(summary.tone)}`} id="code-agent-summary-label">{summary.label}</strong>
<span className="code-agent-summary-capability" id="code-agent-summary-capability">{summary.capabilityLevel}</span>
</summary>
<div className="code-agent-summary-detail" id="code-agent-summary-detail">
<div className="code-agent-summary-rows" role="table" aria-label="Code Agent 状态详情">
{rows.map((row, index) => (
<div className="code-agent-summary-row" role="row" key={`${row.label}-${index}`}>
<span className="code-agent-summary-key" role="rowheader">{row.label}</span>
<span className={`code-agent-summary-value tone-${toneClass(row.tone)}`} role="cell">{row.value}</span>
</div>
))}
</div>
</div>
{summary.readinessBlockers.length > 0 ? <details className="code-agent-summary-raw"><summary> readiness JSON</summary><pre>{jsonPreview(availability ?? { status: "unverified" }, 1200)}</pre></details> : null}
</div>
</details>
</details>
<DebugDialog
dialogId="code-agent-debug"
title="Code Agent 调试信息"
sections={sections}
rawJson={availability ?? summary.fields}
triggerLabel="调试信息"
triggerTitle="查看 Code Agent 状态、readiness blockers 和原始 JSON"
className="code-agent-summary-debug"
/>
</div>
);
}
@@ -115,20 +144,96 @@ function MessageCard({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplayT
<StateTag tone={statusLabel(message).tone}>{statusLabel(message).label}</StateTag>
</header>
<div className="message-body" dangerouslySetInnerHTML={{ __html: html }} />
{message.role === "agent" && message.status === "running" ? <MessagePendingContext message={message} codeAgentTimeoutMs={codeAgentTimeoutMs} /> : null}
{message.role === "agent" && message.status !== "running" ? <MessageSessionContinuity message={message} /> : null}
{message.role === "agent" ? <MessageRuntimePath message={message} /> : null}
<footer className="message-meta">
<span>trace {shortToken(message.traceId, 10)}</span>
<span>session {shortToken(message.sessionId, 10)}</span>
<span>{formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })}</span>
</footer>
<MessageDebug
message={message}
codeAgentTimeoutMs={codeAgentTimeoutMs}
onCancel={onCancel}
onRetry={onRetry}
onReplayTrace={onReplayTrace}
/>
{message.runnerTrace ? <MessageTracePanel trace={message.runnerTrace} storageKey={traceKey} /> : null}
<MessageActions message={message} onCancel={onCancel} onRetry={onRetry} onReplayTrace={onReplayTrace} />
</article>
);
}
function MessageDebug({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplayTrace }: { message: ChatMessage; codeAgentTimeoutMs: number; onCancel(): void; onRetry(): void; onReplayTrace(): void }): ReactElement | null {
if (message.role === "user" || message.role === "system") {
if (!message.traceId) return null;
return (
<footer className="message-meta message-meta-compact">
<span>{formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })}</span>
<DebugDialog
dialogId={`message-debug-${message.id}`}
title="会话调试信息"
triggerLabel="调试信息"
triggerTitle="查看会话 traceId、threadId 等调试信息"
sections={systemDebugSections(message)}
/>
</footer>
);
}
return (
<>
<footer className="message-meta message-meta-compact">
<span>{formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })}</span>
<MessageActions message={message} onCancel={onCancel} onRetry={onRetry} onReplayTrace={onReplayTrace} />
<DebugDialog
dialogId={`message-debug-${message.id}`}
title="Agent 消息调试信息"
triggerLabel="调试信息"
triggerTitle="查看会话 traceId、sessionId、threadId、runtime path 和 readiness 详情"
sections={agentDebugSections(message, codeAgentTimeoutMs)}
/>
</footer>
{message.status === "running" ? (
<p className="message-pending-inline" data-message-pending-inline>
<span className={`state-tag tone-pending`}></span>
<span className="message-pending-inline-text">{message.runnerTrace?.waitingFor ? `等待 ${message.runnerTrace.waitingFor}` : "等待后端事件"}</span>
</p>
) : null}
</>
);
}
function systemDebugSections(message: ChatMessage): DebugSection[] {
const rows: DebugRow[] = [];
if (message.traceId) rows.push({ label: "traceId", value: message.traceId, copyable: true });
return [{ title: "会话标识", rows }];
}
function agentDebugSections(message: ChatMessage, codeAgentTimeoutMs: number): DebugSection[] {
const sessionRows: DebugRow[] = [];
if (message.traceId) sessionRows.push({ label: "traceId", value: message.traceId, copyable: true });
if (message.conversationId) sessionRows.push({ label: "conversation", value: message.conversationId, copyable: true });
if (message.sessionId) sessionRows.push({ label: "session", value: message.sessionId, copyable: true });
else if (message.runnerTrace?.sessionId) sessionRows.push({ label: "session", value: String(message.runnerTrace.sessionId), copyable: true });
if (message.threadId) sessionRows.push({ label: "thread", value: message.threadId, copyable: true });
else if (message.runnerTrace?.threadId) sessionRows.push({ label: "thread", value: String(message.runnerTrace.threadId), copyable: true });
const sessionStatus = typeof message.runnerTrace?.sessionStatus === "string" ? message.runnerTrace.sessionStatus : message.status;
if (sessionStatus) sessionRows.push({ label: "sessionStatus", value: String(sessionStatus) });
const lifecycle = typeof message.runnerTrace?.sessionLifecycleStatus === "string" ? message.runnerTrace.sessionLifecycleStatus : null;
if (lifecycle) sessionRows.push({ label: "lifecycle", value: lifecycle });
if (message.runnerTrace?.lastEventLabel) sessionRows.push({ label: "lastEvent", value: String(message.runnerTrace.lastEventLabel) });
sessionRows.push({ label: "activityTimeout", value: `${Math.round(codeAgentTimeoutMs / 60000)}m 无新事件` });
const runtime = codeAgentRuntimePathFromMessage({ role: "agent", ...(message as unknown as Record<string, unknown>) });
const runtimeRows: DebugRow[] = [
{ label: "provider", value: runtime.rows[0]?.value ?? "未观测" },
{ label: "runnerKind", value: runtime.rows[1]?.value ?? "未观测" },
{ label: "protocol", value: runtime.rows[2]?.value ?? "未观测" },
{ label: "implementationType", value: runtime.rows[3]?.value ?? "未观测" },
{ label: "providerTrace.command", value: runtime.rows[4]?.value ?? "未观测" },
{ label: "providerTrace.terminalStatus", value: runtime.rows[5]?.value ?? "未观测" },
{ label: "providerTrace.failureKind", value: runtime.rows[6]?.value ?? "未观测" }
].filter((row) => row.value !== "未观测" && row.value !== "字段缺失:证据不足");
const runtimeSection: DebugSection = { title: "Runtime Path", rows: runtimeRows, empty: runtime.missingFields.length > 0 ? "未观测到 providerTrace 字段,跳过该 section。" : "运行路径不可用。" };
return [
{ title: "会话标识", rows: sessionRows },
runtimeSection
];
}
function statusLabel(message: ChatMessage): { label: string; tone: ReturnType<typeof toneClass> } {
const label = agentStatusLabel(message);
return { label: label.label, tone: label.tone };
@@ -0,0 +1,123 @@
import type { ReactElement } from "react";
import { useEffect, useId, useState } from "react";
export interface DebugRow {
label: string;
value: string;
tone?: "ok" | "warn" | "blocked" | "source" | "pending" | "dev-live" | "error";
copyable?: boolean;
}
export interface DebugSection {
title: string;
rows: DebugRow[];
empty?: string;
}
interface DebugDialogProps {
triggerLabel?: string;
triggerTitle?: string;
dialogId: string;
title: string;
sections: DebugSection[];
rawJson?: unknown;
empty?: string;
className?: string;
}
function toneClass(tone: DebugRow["tone"]): string {
if (tone === "ok" || tone === "warn" || tone === "blocked" || tone === "source" || tone === "pending" || tone === "dev-live" || tone === "error") return tone;
return "source";
}
export function DebugDialog({ triggerLabel, triggerTitle, dialogId, title, sections, rawJson, empty, className }: DebugDialogProps): ReactElement {
const fallbackId = useId();
const id = dialogId || fallbackId;
const triggerId = `${id}-trigger`;
const [open, setOpen] = useState(false);
useEffect(() => {
if (!open) return;
const handleKey = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [open]);
const totalRows = sections.reduce((acc, section) => acc + section.rows.length, 0);
const allEmpty = totalRows === 0 && !rawJson;
return (
<>
<button
type="button"
className={`debug-dialog-trigger${className ? ` ${className}` : ""}`}
id={triggerId}
data-debug-dialog-trigger={id}
aria-haspopup="dialog"
aria-controls={id}
title={triggerTitle ?? "打开调试信息弹窗"}
onClick={() => setOpen(true)}
>
{triggerLabel ?? "调试信息"}
</button>
{open ? (
<div
className="debug-dialog-scrim"
data-debug-dialog-scrim={id}
role="presentation"
onClick={(event) => {
if (event.target === event.currentTarget) setOpen(false);
}}
>
<dialog
className="debug-dialog"
id={id}
data-debug-dialog={id}
aria-labelledby={`${id}-title`}
open
onClick={(event) => event.stopPropagation()}
>
<header className="debug-dialog-head">
<h3 id={`${id}-title`}>{title}</h3>
<button type="button" className="debug-dialog-close" aria-label="关闭调试信息弹窗" onClick={() => setOpen(false)}></button>
</header>
<div className="debug-dialog-body">
{allEmpty ? (
<p className="debug-dialog-empty">{empty ?? "暂无可显示的调试信息。"}</p>
) : (
<>
{sections.map((section) => {
if (section.rows.length === 0) {
return section.empty ? <p key={section.title} className="debug-dialog-section-empty">{section.empty}</p> : null;
}
return (
<section key={section.title} className="debug-dialog-section">
<h4>{section.title}</h4>
<div className="debug-dialog-rows" role="table" aria-label={section.title}>
{section.rows.map((row) => (
<div className="debug-dialog-row" role="row" key={row.label}>
<span className="debug-dialog-key" role="rowheader">{row.label}</span>
<span className={`debug-dialog-value mono tone-${toneClass(row.tone)}`} role="cell">{row.value}</span>
</div>
))}
</div>
</section>
);
})}
{rawJson !== undefined && rawJson !== null ? (
<section className="debug-dialog-section">
<h4> JSON</h4>
<pre className="debug-dialog-raw" data-debug-dialog-raw>{JSON.stringify(rawJson, null, 2)}</pre>
</section>
) : null}
</>
)}
</div>
</dialog>
</div>
) : null}
</>
);
}
@@ -353,7 +353,9 @@ function compactRunnerTraceSummary(runnerTrace: unknown): string {
}
export function codeAgentSummaryRows(summary: CodeAgentStatusSummary): { key: string; value: string; tone: CodeAgentStatusSummary["tone"] | "source" }[] {
return [
// 顶层 summary 仅保留有意义条目;providerTrace.* / facts / runnerTrace echo
// 等未观测噪音移出,调试用户走"调试信息" dialog 读取 summary.fields。
const allRows: Array<{ key: string; value: string; tone: CodeAgentStatusSummary["tone"] | "source" }> = [
{ key: "codeAgent.status", value: summary.codeAgentStatus, tone: summary.tone },
{ key: "provider/mode/backend", value: summary.providerModeBackend, tone: "source" },
{ key: "capabilityLevel", value: summary.capabilityLevel, tone: summary.tone },
@@ -363,20 +365,20 @@ export function codeAgentSummaryRows(summary: CodeAgentStatusSummary): { key: st
{ key: "workspace", value: summary.workspace, tone: "source" },
{ key: "sandbox", value: summary.sandbox, tone: "source" },
{ key: "runnerKind", value: summary.runnerKind, tone: summary.kind === "long-lived-session" ? "ok" : "source" },
{ key: "protocol", value: summary.protocol, tone: summary.runtimePathTone },
{ key: "implementationType", value: summary.implementationType, tone: summary.runtimePathTone },
{ key: "providerTrace.command", value: summary.providerTraceCommand, tone: summary.runtimePathTone },
{ key: "providerTrace.terminalStatus", value: summary.providerTraceTerminalStatus, tone: summary.runtimePathTone },
{ key: "providerTrace.failureKind", value: summary.providerTraceFailureKind, tone: summary.runtimePathTone },
{ key: "运行路径语义", value: summary.runtimePathLabel, tone: summary.runtimePathTone },
{ key: "toolCalls", value: summary.toolCalls, tone: summary.toolCalls === "none" ? "source" : summary.tone },
{ key: "skills", value: summary.skills, tone: summary.skills === "none" ? "source" : summary.tone },
{ key: "conversation facts", value: summary.fields.runnerTrace ?? "未观测", tone: "source" },
{ key: "runnerTrace", value: summary.runnerTrace, tone: "source" },
{ key: "readiness blockers", value: summary.blockersLabel, tone: summary.readinessBlockers.length > 0 ? "blocked" : "source" },
{ key: "last traceId", value: summary.lastTraceId, tone: "source" },
{ key: "当前部署 revision", value: summary.deploymentRevision, tone: "source" }
];
return allRows.filter((row) => {
if (!row.value) return false;
if (row.value === "未观测") return false;
if (row.value === "字段缺失:证据不足") return false;
if (row.value === "DEGRADED:运行路径字段不完整") return false;
if (row.value === "无" && row.key === "readiness blockers") return false;
return true;
});
}
function sessionSummaryTone(summary: CodeAgentStatusSummary): CodeAgentStatusSummary["tone"] {
@@ -395,4 +397,4 @@ export function summarizeLastEvent(trace: RunnerTrace | null | undefined): { lab
const status = String(last?.status ?? "");
const tone: CodeAgentStatusSummary["tone"] = status === "completed" ? "ok" : status === "running" || status === "pending" ? "warn" : status === "failed" ? "blocked" : "source";
return { label: lastLabel, ts, tone };
}
}
+32 -4
View File
@@ -101,7 +101,7 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
.message-trace-label { font-weight: 700; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; overflow-wrap: anywhere; }
.message-trace-time { color: var(--muted); font-size: 12px; }
.message-trace-body { margin: 6px 0 0; max-height: 220px; overflow: auto; background: #f3f5f1; padding: 6px 8px; border-radius: 6px; font-size: 12px; }
.message-trace-events { min-height: 0; max-height: 360px; overflow: auto; }
.message-trace-events { min-height: 0; max-height: 360px; overflow: auto; width: 100%; }
.message-trace-empty { color: var(--muted); font-size: 12px; padding: 10px; }
.message-trace-last { color: var(--muted); font-size: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.message-actions { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 6px 8px; border-top: 1px dashed var(--line); flex-wrap: wrap; }
@@ -163,8 +163,35 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
.message-body { line-height: 1.58; }
.message-body pre { overflow: auto; background: #f3f5f1; padding: 10px; border-radius: 6px; }
.message-meta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.message-trace { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; }
.message-trace-events { max-height: 260px; overflow: auto; display: grid; gap: 8px; }
.message-trace { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; width: 100%; }
.message-trace[data-trace-mode="all"], .message-trace[open] { width: 100%; }
.message-trace-events { max-height: min(540px, 60dvh); overflow: auto; display: grid; gap: 8px; width: 100%; }
.code-agent-summary-wrapper { display: flex; align-items: stretch; gap: 8px; min-width: 0; }
.code-agent-summary-wrapper > .code-agent-summary { flex: 1 1 auto; min-width: 0; }
.code-agent-summary-debug { flex: 0 0 auto; align-self: center; }
.debug-dialog-trigger { border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 4px 10px; font-size: 12px; font-weight: 700; cursor: pointer; }
.debug-dialog-trigger:hover, .debug-dialog-trigger:focus-visible { border-color: var(--accent); outline: 0; }
.debug-dialog-scrim { position: fixed; inset: 0; background: rgba(15, 22, 20, 0.45); display: grid; place-items: center; z-index: 60; padding: 16px; }
.debug-dialog { max-width: min(720px, 92vw); width: 100%; max-height: 86vh; border: 1px solid var(--line); border-radius: 10px; padding: 0; background: var(--surface); color: var(--ink); box-shadow: 0 24px 60px rgba(15, 22, 20, 0.4); overflow: hidden; }
.debug-dialog-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.debug-dialog-head h3 { margin: 0; font-size: 16px; }
.debug-dialog-close { border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); padding: 4px 10px; font-size: 12px; font-weight: 700; cursor: pointer; }
.debug-dialog-body { display: grid; gap: 12px; padding: 12px 16px; overflow: auto; max-height: 70vh; }
.debug-dialog-section { display: grid; gap: 6px; }
.debug-dialog-section h4 { margin: 0; font-size: 13px; color: var(--muted); }
.debug-dialog-empty, .debug-dialog-section-empty { margin: 0; color: var(--muted); font-size: 12px; }
.debug-dialog-rows { display: grid; gap: 4px 12px; grid-template-columns: minmax(140px, max-content) 1fr; align-items: baseline; }
.debug-dialog-row { display: contents; }
.debug-dialog-key { color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
.debug-dialog-value { font-size: 12px; overflow-wrap: anywhere; }
.debug-dialog-raw { margin: 0; max-height: 240px; overflow: auto; background: #f3f5f1; padding: 8px; border-radius: 6px; font-size: 12px; }
.message-meta-compact { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 6px; }
.message-meta-compact .debug-dialog-trigger { margin-left: auto; }
.message-pending-inline { display: flex; align-items: center; gap: 8px; margin: 8px 0 0; padding: 4px 8px; background: #fbfcf8; border-radius: 6px; }
.message-pending-inline-text { color: var(--muted); font-size: 12px; }
.message-trace { width: 100%; }
.message-trace-events { width: 100%; }
.command-bar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "input send clear" "profile agent gateway"; gap: 8px; align-items: stretch; padding: 10px 14px; border-top: 1px solid var(--line); background: #f7f8f4; }
.command-bar .input-shell { grid-area: input; }
@@ -242,7 +269,8 @@ button:disabled { cursor: not-allowed; opacity: 0.55; }
.right-sidebar { grid-column: 2; grid-row: 2; padding: 10px; }
.view { padding: 8px; }
.conversation-panel { padding: 10px; }
.command-bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "input send clear" "profile profile profile" "agent agent agent" "gateway gateway gateway"; align-items: stretch; }
.command-bar { grid-template-columns: minmax(0, 1fr) auto auto; grid-template-areas: "input send clear" "profile profile profile" "agent agent agent" "gateway gateway gateway"; align-items: stretch; }
.agent-timeout-control { display: none; }
.input-shell { grid-column: 1 / span 3; }
.command-button { min-height: 40px; }