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:
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user