diff --git a/tools/capture-issue-803-noise.mjs b/tools/capture-issue-803-noise.mjs new file mode 100644 index 00000000..ac7b5c0e --- /dev/null +++ b/tools/capture-issue-803-noise.mjs @@ -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(); diff --git a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx index 768cda0f..2e8165df 100644 --- a/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx +++ b/web/hwlab-cloud-web/src/components/conversation/ConversationPanel.tsx @@ -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
- {[...intro, ...messages].map((message) => onCancelMessage(message.id)} onRetry={() => onRetryMessage(message.id)} onReplayTrace={() => onReplayTrace(message.id)} />)} + {[...intro, ...messages].map((message) => ( + onCancelMessage(message.id)} + onRetry={() => onRetryMessage(message.id)} + onReplayTrace={() => onReplayTrace(message.id)} + /> + ))}
@@ -78,27 +85,49 @@ function latestAgentMessage(messages: ChatMessage[]): Record | function CodeAgentSummary({ availability, live, latestMessage }: { availability: CodeAgentAvailability | null; live: LiveSurface | null; latestMessage: Record | null }): ReactElement { const summary = useMemo(() => classifyCodeAgentStatusSummary({ availability, live, latestMessage }), [availability, live, latestMessage]); - const rows = useMemo(() => codeAgentSummaryRows(summary), [summary]); + const rows = useMemo(() => 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 ( -
- - - {summary.label} - {summary.capabilityLevel} - trace {summary.lastTraceId} - -
-
- {rows.map((row) => ( -
- {row.key} - {row.value} -
- ))} +
+
setOpen((event.currentTarget as HTMLDetailsElement).open)} + > + + + {summary.label} + {summary.capabilityLevel} + +
+
+ {rows.map((row, index) => ( +
+ {row.label} + {row.value} +
+ ))} +
- {summary.readinessBlockers.length > 0 ?
展开 readiness 原始 JSON
{jsonPreview(availability ?? { status: "unverified" }, 1200)}
: null} -
-
+ + + ); } @@ -115,20 +144,96 @@ function MessageCard({ message, codeAgentTimeoutMs, onCancel, onRetry, onReplayT {statusLabel(message).label}
- {message.role === "agent" && message.status === "running" ? : null} - {message.role === "agent" && message.status !== "running" ? : null} - {message.role === "agent" ? : null} -
- trace {shortToken(message.traceId, 10)} - session {shortToken(message.sessionId, 10)} - {formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })} -
+ {message.runnerTrace ? : null} - ); } +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 ( +
+ {formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })} + +
+ ); + } + return ( + <> +
+ {formatBeijingTime(message.updatedAt ?? message.createdAt, { withSeconds: true })} + + +
+ {message.status === "running" ? ( +

+ 处理中 + {message.runnerTrace?.waitingFor ? `等待 ${message.runnerTrace.waitingFor}` : "等待后端事件"} +

+ ) : 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) }); + 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 } { const label = agentStatusLabel(message); return { label: label.label, tone: label.tone }; diff --git a/web/hwlab-cloud-web/src/components/shared/DebugDialog.tsx b/web/hwlab-cloud-web/src/components/shared/DebugDialog.tsx new file mode 100644 index 00000000..4bd43ca4 --- /dev/null +++ b/web/hwlab-cloud-web/src/components/shared/DebugDialog.tsx @@ -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 ( + <> + + {open ? ( +
{ + if (event.target === event.currentTarget) setOpen(false); + }} + > + event.stopPropagation()} + > +
+

{title}

+ +
+
+ {allEmpty ? ( +

{empty ?? "暂无可显示的调试信息。"}

+ ) : ( + <> + {sections.map((section) => { + if (section.rows.length === 0) { + return section.empty ?

{section.empty}

: null; + } + return ( +
+

{section.title}

+
+ {section.rows.map((row) => ( +
+ {row.label} + {row.value} +
+ ))} +
+
+ ); + })} + {rawJson !== undefined && rawJson !== null ? ( +
+

原始 JSON

+
{JSON.stringify(rawJson, null, 2)}
+
+ ) : null} + + )} +
+
+
+ ) : null} + + ); +} diff --git a/web/hwlab-cloud-web/src/state/code-agent-status.ts b/web/hwlab-cloud-web/src/state/code-agent-status.ts index 7c4e9221..f7c6175d 100644 --- a/web/hwlab-cloud-web/src/state/code-agent-status.ts +++ b/web/hwlab-cloud-web/src/state/code-agent-status.ts @@ -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 }; -} +} \ No newline at end of file diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css index 59252d9d..e16f763d 100644 --- a/web/hwlab-cloud-web/src/styles/workbench.css +++ b/web/hwlab-cloud-web/src/styles/workbench.css @@ -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; }