qwen-code/packages/web-shell/client/components/dialogs/DaemonStatusDialog.tsx
Shaojin Wen 8296ce9e54
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
fix(web-shell): i18n for ~43 hardcoded English strings across 15 files (#6516)
* fix(web-shell): i18n for ~43 hardcoded English strings across 15 files

Multi-round audit replaced hardcoded UI strings with t() calls and
added en/zh-CN i18n keys for:

- Session timeline labels, aria-labels, and kind labels (MessageList)
- SubAgent tab labels, tools count, pending/running status
- Auth protocol option labels, placeholders, API key masking
- Voice dictation UI states (VoiceButton)
- Copy tooltip, Runtime badge, N/A, Server/Agent fallbacks
- Mermaid code block labels, image alt text, model switch summary

* fix(web-shell): i18n for missed models placeholder in AuthMessage

* fix(web-shell): invalidate timeline cache on locale switch

Store the t function reference in sessionTimelineCache alongside the
message signature. When the locale changes, t gets a new reference,
triggering cache invalidation and re-generating entries with the new
language labels.
2026-07-08 08:46:37 +00:00

1011 lines
35 KiB
TypeScript

import {
useCallback,
useEffect,
useRef,
useState,
type KeyboardEvent,
type ReactNode,
} from 'react';
import {
useStatusReport,
type DaemonMetricsSeriesBucket,
type DaemonStatusReport,
type DaemonStatusReportLevel,
type DaemonStatusReportSection,
} from '@qwen-code/webui/daemon-react-sdk';
import { useI18n } from '../../i18n';
import { ErrorBoundary } from '../ErrorBoundary';
import { SvgLineChart, type ChartSeries } from './SvgLineChart';
import { UsageDashboardTab } from './UsageDashboardTab';
import styles from './DaemonStatusDialog.module.css';
// The cheap in-memory summary is polled continuously; the expensive detail
// (per-session, workspace diagnostics, auth — the daemon may spawn the ACP
// child and aggregate several diagnostic surfaces to build it) is fetched only
// on open and on an explicit refresh, so parking the dialog open never rehits
// that path. Both surface as one dashboard: the summary/full split is a daemon
// cost boundary, not something the operator should have to think about.
const REFRESH_INTERVAL_MS = 5000;
// The dashboard splits into tabs once it carries live charts: monitoring
// (charts you watch), configuration (static cards you glance at), and
// diagnostics (sessions/workspace you open when something is wrong) are
// different intents — and 6 cards + 7 charts + diagnostics overflow one 70vh
// scroll. Status badge / refresh / issues stay global above the tabs.
type DaemonTab = 'overview' | 'usage' | 'metrics' | 'diagnostics';
const DAEMON_TABS: ReadonlyArray<{ id: DaemonTab; labelKey: string }> = [
{ id: 'overview', labelKey: 'daemon.tab.overview' },
{ id: 'usage', labelKey: 'daemon.tab.usage' },
{ id: 'metrics', labelKey: 'daemon.tab.metrics' },
{ id: 'diagnostics', labelKey: 'daemon.tab.diagnostics' },
];
function formatUptime(ms: number): string {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
if (minutes > 0) return `${minutes}m ${seconds}s`;
return `${seconds}s`;
}
function formatDurationMs(ms: number): string {
ms = Math.max(0, ms); // clamp clock-skew negatives to a "0ms" contract
if (ms >= 60_000) return formatUptime(ms);
if (ms >= 1000) return `${(ms / 1000).toFixed(ms % 1000 === 0 ? 0 : 1)}s`;
return `${ms}ms`;
}
function formatBytes(bytes: number): string {
// Adaptive unit: sub-MB windows (idle-pipe keep-alive traffic) would read a
// misleading "0.0 MB", so drop to KB/B and show a nonzero value. RSS/heap are
// always ≥ 1 MB, so those charts stay in MB/GB unchanged.
const kb = bytes / 1024;
if (kb < 1) return `${Math.round(bytes)} B`;
const mb = kb / 1024;
if (mb < 1) return `${kb.toFixed(kb < 10 ? 1 : 0)} KB`;
return mb >= 1024 ? `${(mb / 1024).toFixed(2)} GB` : `${mb.toFixed(1)} MB`;
}
// Compact counts for chart peaks/current values: thousands collapse to "12.3k"
// so token burn and request counts stay legible in the narrow legend.
function formatCount(value: number): string {
const n = Math.round(value);
if (n >= 10_000) {
return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}k`;
}
return n.toLocaleString();
}
function formatPercent(value: number): string {
return `${value.toFixed(value >= 100 || value === 0 ? 0 : 1)}%`;
}
function channelWorkerState(
worker: DaemonStatusReport['runtime']['channelWorker'],
): string {
if (worker.exitCode != null) {
return `${worker.state} (exit ${worker.exitCode})`;
}
if (worker.signal) return `${worker.state} (${worker.signal})`;
return worker.state;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
interface WorkspaceProblemCell {
label: string;
status: 'warning' | 'error';
message?: string;
}
// A section's status is the worst of its individual checks, but the summary
// chips only carry counts — so a "warning preflight" reads as opaque. Pull the
// individual warning/error entries out of the raw section data so the dashboard
// can say *what* is wrong (e.g. "auth: No auth method configured"). Section
// payloads differ but consistently carry status cells under these keys.
const SECTION_CELL_KEYS = [
'cells',
'servers',
'errors',
'skills',
'tools',
'providers',
'hooks',
'extensions',
'budgets',
] as const;
function extractProblemCells(data: unknown): WorkspaceProblemCell[] {
if (!isRecord(data)) return [];
const problems: WorkspaceProblemCell[] = [];
for (const key of SECTION_CELL_KEYS) {
const arr = data[key];
if (!Array.isArray(arr)) continue;
for (const item of arr) {
if (!isRecord(item)) continue;
const status = item['status'];
if (status !== 'warning' && status !== 'error') continue;
const label = String(
item['kind'] ?? item['name'] ?? item['serverName'] ?? key,
);
const message =
typeof item['error'] === 'string'
? item['error']
: typeof item['hint'] === 'string'
? item['hint']
: undefined;
problems.push({ label, status, message });
}
}
return problems;
}
function levelClass(
level: DaemonStatusReportLevel | 'unavailable',
): string | undefined {
switch (level) {
case 'ok':
return styles.levelOk;
case 'warning':
return styles.levelWarning;
case 'error':
return styles.levelError;
default:
return styles.levelUnavailable;
}
}
function Row({ label, value }: { label: string; value: ReactNode }) {
return (
<div className={styles.row}>
<span className={styles.rowLabel}>{label}</span>
<span className={styles.rowValue}>{value}</span>
</div>
);
}
function Card({ title, children }: { title: string; children: ReactNode }) {
return (
<section className={styles.card}>
<h3 className={styles.cardTitle}>{title}</h3>
{children}
</section>
);
}
function WorkspaceSectionRow({
name,
section,
}: {
name: string;
section: DaemonStatusReportSection;
}) {
const { t } = useI18n();
const summaryEntries = Object.entries(section.summary ?? {});
const problemCells = extractProblemCells(section.data);
return (
<div className={styles.workspaceRow}>
<div className={styles.workspaceRowHead}>
<span className={`${styles.badge} ${levelClass(section.status)}`}>
{t(`daemon.level.${section.status}`)}
</span>
<span className={styles.workspaceName}>{name}</span>
<span className={styles.workspaceDuration}>
{formatDurationMs(section.durationMs)}
</span>
</div>
{section.error && (
<div className={styles.workspaceError}>{section.error.message}</div>
)}
{/* Name the individual checks that pushed this section to warning/error,
so the badge is self-explanatory. */}
{problemCells.map((cell, index) => (
<div key={`${cell.label}-${index}`} className={styles.workspaceCell}>
<span className={`${styles.badge} ${levelClass(cell.status)}`}>
{t(`daemon.level.${cell.status}`)}
</span>
<span className={styles.workspaceCellLabel}>{cell.label}</span>
{cell.message && (
<span className={styles.workspaceCellMessage}>{cell.message}</span>
)}
</div>
))}
{summaryEntries.length > 0 && (
<div className={styles.workspaceSummary}>
{summaryEntries.map(([key, value]) => (
<span key={key} className={styles.summaryChip}>
{key}: {value === null ? t('common.na') : String(value)}
</span>
))}
</div>
)}
</div>
);
}
function FullDetail({ report }: { report: DaemonStatusReport }) {
const { t } = useI18n();
const full = report.full;
if (!full) return null;
const workspaceEntries = Object.entries(full.workspace).sort(([a], [b]) =>
a.localeCompare(b),
);
return (
<>
<Card title={t('daemon.full.sessions.title')}>
{full.sessions.length === 0 ? (
<div className={styles.empty}>{t('daemon.full.sessions.empty')}</div>
) : (
full.sessions.map((session) => (
<div key={session.sessionId} className={styles.sessionRow}>
<div className={styles.sessionName}>
{session.displayName || session.sessionId}
</div>
<div className={styles.sessionMeta}>
<span>
{t('common.clients', { count: session.clientCount })}
</span>
<span>
{t('daemon.full.session.pendingPrompts', {
count: session.pendingPromptCount,
})}
</span>
<span>
{t('daemon.full.session.pendingPermissions', {
count: session.pendingPermissionCount,
})}
</span>
{session.hasActivePrompt && (
<span className={styles.activePrompt}>
{t('daemon.full.session.prompting')}
</span>
)}
</div>
</div>
))
)}
</Card>
<Card title={t('daemon.full.workspace.title')}>
{workspaceEntries.length === 0 ? (
<div className={styles.empty}>{t('daemon.full.workspace.empty')}</div>
) : (
workspaceEntries.map(([name, section]) => (
<WorkspaceSectionRow key={name} name={name} section={section} />
))
)}
</Card>
<Card title={t('daemon.full.auth.title')}>
<Row
label={t('daemon.full.auth.providers')}
value={
full.auth.supportedDeviceFlowProviders.join(', ') ||
t('daemon.none')
}
/>
<Row
label={t('daemon.full.auth.pending')}
value={full.auth.pendingDeviceFlowCount}
/>
<Row
label={t('daemon.full.acp.title')}
value={full.acpConnections.length}
/>
</Card>
</>
);
}
// Bottleneck-analysis dashboard: the daemon samples load, throughput, latency,
// resource pressure, and token burn into one time-bucketed series, so these
// charts share an x-axis. Lining up "N tasks running at once" (concurrency)
// against event-loop lag, queue wait, memory, and API latency shows *where* a
// busy daemon is actually stalling.
function MetricsCharts({ series }: { series: DaemonMetricsSeriesBucket[] }) {
const { t } = useI18n();
if (series.length === 0) {
return (
<Card title={t('daemon.charts.title')}>
<div className={styles.empty}>{t('daemon.charts.empty')}</div>
</Card>
);
}
const col = (pick: (b: DaemonMetricsSeriesBucket) => number): number[] =>
series.map(pick);
// Bucket timestamps drive the hover tooltip's time header.
const times = series.map((b) => b.t);
const chart = (
titleKey: string,
format: (v: number) => string,
lines: ChartSeries[],
): ReactNode => (
<Card title={t(titleKey)}>
<SvgLineChart
series={lines}
timestamps={times}
format={format}
ariaLabel={t(titleKey)}
peakLabel={t('daemon.charts.peak')}
/>
</Card>
);
return (
<div className={`${styles.grid} ${styles.chartsGrid}`}>
{chart('daemon.charts.concurrency', formatCount, [
{
label: t('daemon.charts.activePrompts'),
values: col((b) => b.activePrompts),
color: 'var(--primary)',
},
{
label: t('daemon.charts.queuedPrompts'),
values: col((b) => b.queuedPrompts),
color: 'var(--warning-color)',
},
{
label: t('daemon.charts.activeSessions'),
values: col((b) => b.activeSessions),
color: 'var(--muted-foreground)',
},
])}
{chart('daemon.charts.requests', formatCount, [
{
label: t('daemon.charts.reqTotal'),
values: col((b) => b.requests),
color: 'var(--success-color)',
},
{
label: t('daemon.charts.reqErrors'),
values: col((b) => b.errors),
color: 'var(--error-color)',
},
{
label: t('daemon.charts.reqRejected'),
values: col((b) => b.rateLimitRejected),
color: 'var(--warning-color)',
},
])}
{chart('daemon.charts.apiLatency', formatDurationMs, [
{
label: 'p50',
values: col((b) => b.latencyP50Ms),
color: 'var(--agent-blue-400)',
},
{
label: 'p95',
values: col((b) => b.latencyP95Ms),
color: 'var(--warning-color)',
},
])}
{chart('daemon.charts.llmLatency', formatDurationMs, [
{
label: 'p50',
values: col((b) => b.llmApiP50Ms),
color: 'var(--agent-blue-400)',
},
{
label: 'p95',
values: col((b) => b.llmApiP95Ms),
color: 'var(--primary)',
},
])}
{chart('daemon.charts.promptLatency', formatDurationMs, [
{
label: t('daemon.charts.queueWait'),
values: col((b) => b.promptQueueWaitP95Ms),
color: 'var(--warning-color)',
},
{
label: t('daemon.charts.promptDuration'),
values: col((b) => b.promptDurationP95Ms),
color: 'var(--primary)',
},
])}
{chart('daemon.charts.eventLoop', formatDurationMs, [
{
label: t('daemon.charts.eventLoopLag'),
values: col((b) => b.eventLoopLagP99Ms),
color: 'var(--error-color)',
},
])}
{chart('daemon.charts.cpu', formatPercent, [
{
label: t('daemon.charts.cpuDaemon'),
values: col((b) => b.cpuPercent),
color: 'var(--muted-foreground)',
},
{
label: t('daemon.charts.cpuChild'),
values: col((b) => b.childCpuPercent),
color: 'var(--primary)',
},
])}
{chart('daemon.charts.memory', formatBytes, [
{
label: t('daemon.charts.rssDaemon'),
values: col((b) => b.rssBytes),
color: 'var(--muted-foreground)',
},
{
label: t('daemon.charts.heap'),
values: col((b) => b.heapUsedBytes),
color: 'var(--agent-blue-400)',
},
{
label: t('daemon.charts.rssChild'),
values: col((b) => b.childRssBytes),
color: 'var(--primary)',
},
])}
{chart('daemon.charts.pipe', formatBytes, [
{
label: t('daemon.charts.pipeIn'),
values: col((b) => b.pipeInBytes),
color: 'var(--agent-blue-400)',
},
{
label: t('daemon.charts.pipeOut'),
values: col((b) => b.pipeOutBytes),
color: 'var(--success-color)',
},
])}
{chart('daemon.charts.connections', formatCount, [
{
label: 'SSE',
values: col((b) => b.sseConnections),
color: 'var(--primary)',
},
{
label: 'WS',
values: col((b) => b.wsConnections),
color: 'var(--agent-blue-400)',
},
{
label: 'ACP',
values: col((b) => b.acpConnections),
color: 'var(--muted-foreground)',
},
])}
{chart('daemon.charts.tokens', formatCount, [
{
label: t('daemon.charts.tokensIn'),
values: col((b) => b.tokensIn),
color: 'var(--agent-blue-400)',
},
{
label: t('daemon.charts.tokensOut'),
values: col((b) => b.tokensOut),
color: 'var(--success-color)',
},
])}
</div>
);
}
function DaemonStatusDialogInner() {
const { t } = useI18n();
const [activeTab, setActiveTab] = useState<DaemonTab>('overview');
// WAI-ARIA tabs keyboard support: roving tabindex (only the active tab is in
// the tab order) + Arrow/Home/End moving focus and selection across the
// tablist, so keyboard-only users can switch tabs without tabbing through all
// the panel content in between.
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
const handleTabKeyDown = (
e: KeyboardEvent<HTMLButtonElement>,
index: number,
): void => {
let next: number;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
next = (index + 1) % DAEMON_TABS.length;
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
next = (index - 1 + DAEMON_TABS.length) % DAEMON_TABS.length;
} else if (e.key === 'Home') {
next = 0;
} else if (e.key === 'End') {
next = DAEMON_TABS.length - 1;
} else {
return;
}
e.preventDefault();
setActiveTab(DAEMON_TABS[next].id);
tabRefs.current[next]?.focus();
};
// Two independent fetches: the summary drives the always-live top cards and
// rides the auto-refresh interval; the full report backs the detail sections
// and is only pulled on open (autoLoad) and on manual refresh.
const summary = useStatusReport({ autoLoad: true, detail: 'summary' });
const full = useStatusReport({ autoLoad: true, detail: 'full' });
// `reload` is a stable callback; depend on it (not the hook object, which is
// a fresh spread each render) so the poll interval is installed once rather
// than torn down and reinstalled on every data update.
const summaryReload = summary.reload;
const fullReload = full.reload;
// Skip a tick when the tab is backgrounded (matching the sidebar poll) or
// when the previous poll is still outstanding: useDaemonResource discards
// stale completions but does not abort, and the client timeout is 30s, so a
// degraded daemon could otherwise accumulate overlapping calls.
const summaryPollInFlightRef = useRef(false);
useEffect(() => {
const timer = window.setInterval(() => {
if (document.hidden || summaryPollInFlightRef.current) return;
summaryPollInFlightRef.current = true;
void summaryReload().finally(() => {
summaryPollInFlightRef.current = false;
});
}, REFRESH_INTERVAL_MS);
return () => window.clearInterval(timer);
}, [summaryReload]);
const refreshAll = useCallback(() => {
void summaryReload();
void fullReload();
}, [summaryReload, fullReload]);
// Prefer the continuously-refreshed summary for the top cards; fall back to
// the full report so the dashboard still renders if only that has landed.
const report = summary.report ?? full.report;
const fullReport = full.report;
const loading = summary.loading || full.loading;
const error = summary.error ?? full.error;
if (!report) {
return (
<div className={styles.dialog}>
<div className={styles.empty}>
{error
? `${t('daemon.loadFailed')}: ${error.message}`
: t('daemon.loading')}
</div>
</div>
);
}
// The daemon only appends workspace/preflight/MCP issues (and rolls them into
// `status`) for detail=full, so the summary can read "ok" with an empty issue
// list while a loaded full report is failing. Drive the badge and issue list
// off the full report whenever it is available; keep the live counters on the
// summary. The rollup then refreshes on open/manual rather than every 5s,
// which only ever over-reports (safe) between full fetches.
const rollupReport = fullReport ?? report;
const { daemon, runtime, security, limits, capabilities } = report;
const acp = runtime.transport.acp;
const rateRejected = Object.values(
runtime.rateLimit.rejectedSinceStart,
).reduce((sum, count) => sum + count, 0);
const limitValue = (value: number | null) =>
value === null ? t('daemon.limits.unlimited') : value;
return (
<div className={styles.dialog}>
<div className={styles.toolbar}>
<span
role="status"
aria-label={`${t('daemon.title')}: ${t(
`daemon.level.${rollupReport.status}`,
)}`}
className={`${styles.badge} ${levelClass(rollupReport.status)}`}
>
{t(`daemon.level.${rollupReport.status}`)}
</span>
<span className={styles.updatedAt}>
{t('daemon.updatedAt', {
time: new Date(report.generatedAt).toLocaleTimeString(),
})}
</span>
{/* Flag the toolbar only when the summary that owns the visible
counters/timestamp is the failing, stale source: it errored AND
still has (now-stale) data on screen. When the summary never loaded
and the cards are rendering from the full fallback, or when only the
full fetch failed (surfaced in the diagnostics section), the banner
would misrepresent an otherwise-usable dashboard. */}
{summary.error && summary.report && (
<span className={styles.refreshError}>{t('daemon.loadFailed')}</span>
)}
<div className={styles.toolbarActions}>
<button
type="button"
className={styles.refreshButton}
onClick={refreshAll}
disabled={loading}
>
{t('daemon.refresh')}
</button>
</div>
</div>
{rollupReport.issues.length > 0 && (
<Card title={t('daemon.issues.title')}>
{rollupReport.issues.map((issue, index) => (
<div key={`${issue.code}-${index}`} className={styles.issueRow}>
<span
className={`${styles.badge} ${
issue.severity === 'error'
? styles.levelError
: styles.levelWarning
}`}
>
{issue.severity === 'error'
? t('daemon.level.error')
: t('daemon.level.warning')}
</span>
<span className={styles.issueMessage}>{issue.message}</span>
</div>
))}
</Card>
)}
<div
className={styles.tabs}
role="tablist"
aria-label={t('daemon.title')}
>
{DAEMON_TABS.map((tab, index) => (
<button
key={tab.id}
ref={(el) => {
tabRefs.current[index] = el;
}}
type="button"
role="tab"
id={`daemon-tab-${tab.id}`}
aria-selected={tab.id === activeTab}
aria-controls={`daemon-tabpanel-${tab.id}`}
tabIndex={tab.id === activeTab ? 0 : -1}
className={`${styles.tab} ${
tab.id === activeTab ? styles.tabActive : ''
}`}
onClick={() => setActiveTab(tab.id)}
onKeyDown={(e) => handleTabKeyDown(e, index)}
>
{t(tab.labelKey)}
</button>
))}
</div>
{activeTab === 'overview' && (
<div
role="tabpanel"
id="daemon-tabpanel-overview"
aria-labelledby="daemon-tab-overview"
tabIndex={0}
className={styles.grid}
>
<Card title={t('daemon.overview.title')}>
{daemon.qwenCodeVersion && (
<Row
label={t('daemon.overview.version')}
value={daemon.qwenCodeVersion}
/>
)}
<Row label={t('daemon.overview.pid')} value={daemon.pid} />
<Row label={t('daemon.overview.mode')} value={daemon.mode} />
<Row
label={t('daemon.overview.uptime')}
value={formatUptime(daemon.uptimeMs)}
/>
{/* The workspace path is long; give it its own full-width row and
keep it to a single line — front-truncated so the meaningful tail
(…/parent/workspace) stays visible, full path on hover. */}
<div className={styles.pathRow}>
<span className={styles.rowLabel}>
{t('daemon.overview.workspace')}
</span>
<span
className={styles.pathValue}
title={daemon.workspaceCwd}
// Front-truncate (ellipsis at the start) via CSS `direction:rtl`
// so the meaningful tail stays visible; `bdi` keeps the path's
// own characters in logical order despite the rtl context.
>
<bdi>{daemon.workspaceCwd}</bdi>
</span>
</div>
</Card>
<Card title={t('daemon.runtime.title')}>
{/* The counters below read as plausible zeros while the daemon
runtime is still coming up or has failed; call that out so they
are not mistaken for a healthy idle daemon. */}
{runtime.error ? (
<div className={styles.workspaceError}>
{t('daemon.runtime.startFailed')}: {runtime.error}
</div>
) : runtime.loading ? (
<div className={styles.empty}>
{t('daemon.runtime.startingUp')}
</div>
) : null}
<Row
label={t('daemon.runtime.activeSessions')}
value={runtime.sessions.active}
/>
{/* Activity counters (daemons predating this omit the sub-object). */}
{runtime.activity && (
<>
<Row
label={t('daemon.runtime.activePrompts')}
value={runtime.activity.activePrompts}
/>
<Row
label={t('daemon.runtime.idle')}
value={
runtime.activity.idleSinceMs === null
? t('daemon.runtime.noActivity')
: formatDurationMs(runtime.activity.idleSinceMs)
}
/>
</>
)}
<Row
label={t('daemon.runtime.pendingPermissions')}
value={runtime.permissions.pending}
/>
<Row
label={t('daemon.runtime.permissionPolicy')}
value={runtime.permissions.policy}
/>
<Row
label={t('daemon.runtime.channel')}
value={
runtime.channel.live
? t('daemon.runtime.channelLive')
: t('daemon.runtime.channelDown')
}
/>
{/* Surface why a channel worker is unhealthy instead of leaving the
operator with a bare "down" — these fields are already fetched. */}
{runtime.channelWorker.enabled && (
<>
<Row
label={t('daemon.runtime.channelWorker')}
value={channelWorkerState(runtime.channelWorker)}
/>
{runtime.channelWorker.error && (
<div className={styles.workspaceError}>
{runtime.channelWorker.error}
</div>
)}
{(runtime.channelWorker.restartCount ?? 0) > 0 && (
<Row
label={t('daemon.runtime.channelWorkerRestarts')}
value={runtime.channelWorker.restartCount}
/>
)}
</>
)}
<Row
label={t('daemon.runtime.memory')}
value={`${formatBytes(runtime.process.rss)} / ${formatBytes(
runtime.process.heapUsed,
)}`}
/>
</Card>
<Card title={t('daemon.transport.title')}>
<Row
label={t('daemon.transport.restSse')}
value={runtime.transport.restSseActive}
/>
{acp.enabled ? (
<>
<Row
label={t('daemon.transport.acpConnections')}
value={acp.connections}
/>
<Row
label={t('daemon.transport.acpStreams')}
value={`${acp.sessionStreams} / ${acp.sseStreams} / ${acp.wsStreams}`}
/>
<Row
label={t('daemon.transport.pendingRequests')}
value={acp.pendingClientRequests}
/>
</>
) : (
<div className={styles.empty}>
{t('daemon.transport.acpDisabled')}
</div>
)}
<Row
label={t('daemon.transport.rateLimitRejected')}
value={
runtime.rateLimit.enabled ? rateRejected : t('common.disabled')
}
/>
</Card>
<Card title={t('daemon.security.title')}>
<Row
label={t('daemon.security.token')}
value={
security.tokenConfigured
? t('daemon.security.configured')
: t('daemon.security.notConfigured')
}
/>
<Row
label={t('daemon.security.requireAuth')}
value={
security.requireAuth
? t('common.enabled')
: t('common.disabled')
}
/>
<Row
label={t('daemon.security.loopback')}
value={
security.loopbackBind
? t('common.enabled')
: t('common.disabled')
}
/>
<Row
label={t('daemon.security.allowOrigin')}
value={security.allowOriginMode}
/>
<Row
label={t('daemon.security.shell')}
value={
security.sessionShellCommandEnabled
? t('common.enabled')
: t('common.disabled')
}
/>
</Card>
<Card title={t('daemon.limits.title')}>
<Row
label={t('daemon.limits.maxSessions')}
value={limitValue(limits.maxSessions)}
/>
<Row
label={t('daemon.limits.maxPendingPrompts')}
value={limitValue(limits.maxPendingPromptsPerSession)}
/>
<Row
label={t('daemon.limits.maxConnections')}
value={limitValue(limits.listenerMaxConnections)}
/>
<Row
label={t('daemon.limits.eventRing')}
value={limits.eventRingSize}
/>
<Row
label={t('daemon.limits.promptDeadline')}
value={
limits.promptDeadlineMs === null
? t('daemon.limits.unlimited')
: formatDurationMs(limits.promptDeadlineMs)
}
/>
<Row
label={t('daemon.limits.sessionIdle')}
value={formatDurationMs(limits.sessionIdleTimeoutMs)}
/>
</Card>
<Card
title={
capabilities.features.length
? t('daemon.capabilities.titleCount', {
count: capabilities.features.length,
})
: t('daemon.capabilities.title')
}
>
{capabilities.features.length === 0 ? (
<span className={styles.empty}>{t('daemon.none')}</span>
) : (
<div className={styles.featureChips}>
{[...capabilities.features].sort().map((feature) => (
<span key={feature} className={styles.featureChip}>
{feature}
</span>
))}
</div>
)}
</Card>
</div>
)}
{/* Aggregate token-usage dashboard (today's totals + 6-month heatmap).
Mounts only when active so the heavy aggregate loads on demand; a
crash in the payload is contained here, not the whole dialog. */}
{activeTab === 'usage' && (
<div
role="tabpanel"
id="daemon-tabpanel-usage"
aria-labelledby="daemon-tab-usage"
tabIndex={0}
>
<ErrorBoundary
label="daemon-usage"
fallback={
<div className={styles.empty}>{t('daemon.usage.failed')}</div>
}
>
<UsageDashboardTab />
</ErrorBoundary>
</div>
)}
{/* Time-series charts for bottleneck analysis. Driven by the
continuously-refreshed summary report so the curves advance on every
poll; the daemon retains the history, so it survives dialog close. */}
{activeTab === 'metrics' && (
<div
role="tabpanel"
id="daemon-tabpanel-metrics"
aria-labelledby="daemon-tab-metrics"
tabIndex={0}
>
<MetricsCharts series={report.runtime.metrics?.series ?? []} />
</div>
)}
{/* Contain a crash in the detail sections (e.g. a partial detail=full
payload) to this region so the healthy summary cards above stay live,
rather than letting the outer boundary replace the whole dialog. */}
{activeTab === 'diagnostics' && (
<div
role="tabpanel"
id="daemon-tabpanel-diagnostics"
aria-labelledby="daemon-tab-diagnostics"
tabIndex={0}
>
<ErrorBoundary
label="daemon-status-detail"
fallback={
<div className={styles.empty}>{t('daemon.details.failed')}</div>
}
>
{fullReport?.full ? (
<FullDetail report={fullReport} />
) : full.loading ? (
<div className={styles.empty}>{t('daemon.details.loading')}</div>
) : full.error ? (
<div className={styles.empty}>
{t('daemon.details.failed')}: {full.error.message}
</div>
) : (
// Fetch resolved but the daemon omitted the `full` section — don't
// hang on the loading placeholder forever.
<div className={styles.empty}>{t('daemon.details.failed')}</div>
)}
</ErrorBoundary>
</div>
)}
</div>
);
}
// A malformed or partial daemon response — most likely exactly when the daemon
// is sick and this dashboard is most needed — must not white-screen the whole
// web shell. Contain any render throw to the dialog; the function-form fallback
// surfaces the actual render error (distinct from a network failure). Because
// the parent only mounts the dialog while open, closing and re-opening remounts
// the boundary, so a transient bad payload recovers on the next open.
export function DaemonStatusDialog() {
const { t } = useI18n();
return (
<ErrorBoundary
label="daemon-status"
fallback={(error) => (
<div className={styles.dialog}>
<div className={styles.empty}>
{t('daemon.loadFailed')}: {error.message}
</div>
</div>
)}
>
<DaemonStatusDialogInner />
</ErrorBoundary>
);
}