qwen-code/packages/web-shell/client/components/dialogs/DaemonStatusDialog.module.css
Shaojin Wen d1cda89875
feat(serve): add model API error & retry metrics to daemon status (#6837)
* feat(serve): chart model API errors and auto-retries in daemon status

The Daemon Status dialog's Metrics tab tracked client→daemon HTTP request
errors but had no visibility into model/LLM API failures or the automatic
retries that absorb them. Add a "Model API health" chart plotting the
per-window model API error and auto-retry counts.

Those events originate in the ACP child (where LLM calls run), so they reach
the daemon over the same child→daemon channel token usage already rides: a
self-contained process-global counter (api-activity-tracker) is bumped at the
logApiError / logApiRetry choke points, drained per live model round by the
ACP MessageEmitter onto agent_message_chunk._meta, sniffed at the bridge
fan-in, and folded into the daemon metrics ring as the llmApiErrors /
llmApiRetries window aggregates (mirrored into the SDK wire type).

A dedicated counter — rather than a new SessionMetrics field — keeps the
change from rippling through ~20 stats/display/snapshot sites. Draining is
guarded on a live frame's durationMs so replayed history can't consume real
pending counts.

* feat(web-shell): add per-chart help tooltips to daemon status metrics

The Metrics tab packs ~12 dense time-series charts; with no inline
explanation they generate recurring "what does this measure / why are
there two 'errors' charts?" questions. Add an ⓘ affordance beside every
chart title whose hover/focus reveals a one-line, plain-language note
(what it measures, its unit/window, and what's normal). The button's
aria-label carries the text to assistive tech; the visual bubble reuses
the existing data-tooltip's surface so it reads in light and dark.

The copy targets the highest-friction ambiguities: the HTTP "Requests"
chart is called out as NOT model calls (vs. the model "API health"
chart), and "API health" spells out the errors-vs-retries relationship
(each failed attempt = 1 error, each backoff = 1 retry; errors above
retries = hard failures). en + zh.

* fix(web-shell): keep chart ⓘ hint out of the heading accessible name

Review follow-up on #6837:

- Move InfoHint out of the <h3> into a flex card header. The
  accessible-name algorithm folds a descendant button's aria-label (the
  whole help sentence) into the heading name, so a screen reader would
  announce "Model API health Each failed attempt = 1 error…" in the
  heading rotor. The flex header keeps the visual layout identical; a
  test now asserts no <h3> contains a button.

- Add the missing logApiError OTel-SDK-off test, mirroring the
  logApiRetry one, so a future refactor that moves recordError() after
  the isTelemetrySdkInitialized guard is caught by a red test.

* fix(web-shell): visible focus on chart ⓘ button, plus review nits

Second review pass on #6837:

- InfoHint focus: the ⓘ button now takes a filled-background focus/hover
  state (--muted, in scope under the DialogShell portal), matching the
  dialog's own header buttons (DialogShell .close / .iconButton). Keyboard
  focus is now unmistakable rather than a color shift alone (WCAG 2.4.7).
- logApiRetry: doc comment corrected from "three-sink" to a four-sink
  fan-out — the apiActivityTracker increment (sink 0, fires before the SDK
  guard) was undocumented and read as stale.
- Tighten the Metrics-tab SvgLineChart count assertion from >=10 to >=12
  so a silently dropped chart card fails the test.
2026-07-14 02:06:09 +00:00

405 lines
8.7 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Scrolling is delegated to the DialogShell body (one scroll container, not
two), so this grows to its content and the shell — including fullscreen —
controls the height. */
.dialog {
display: flex;
flex-direction: column;
gap: 12px;
font-size: 13px;
color: var(--foreground);
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.toolbarActions {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.updatedAt {
color: var(--muted-foreground);
font-size: 12px;
}
.refreshError {
color: var(--error-color);
font-size: 12px;
}
.refreshButton {
padding: 4px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: var(--foreground);
font-size: 12px;
cursor: pointer;
}
.refreshButton:disabled {
opacity: 0.5;
cursor: default;
}
.badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
line-height: 1.5;
}
.levelOk {
color: var(--success-color);
background: color-mix(in srgb, var(--success-color) 14%, transparent);
}
.levelWarning {
color: var(--warning-color);
background: color-mix(in srgb, var(--warning-color) 14%, transparent);
}
.levelError {
color: var(--error-color);
background: color-mix(in srgb, var(--error-color) 14%, transparent);
}
.levelUnavailable {
color: var(--muted-foreground);
background: color-mix(in srgb, var(--muted-foreground) 14%, transparent);
}
.tabs {
display: flex;
gap: 2px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.tab {
min-height: 32px;
padding: 6px 14px;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
background: transparent;
color: var(--muted-foreground);
cursor: pointer;
font: inherit;
font-weight: 500;
}
.tab:hover {
color: var(--foreground);
}
/* Active tab: underline in the accent color, matching the dashboard's chart
accents; carries the selected state that `aria-selected` announces. */
.tabActive {
color: var(--foreground);
border-bottom-color: var(--primary);
}
/* Overview config cards: wider track so a wide panel shows ~3 roomy columns
instead of 4 cramped ones (still collapses to 2 on a narrow viewport). The
metrics tab overrides this with .chartsGrid below. */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
gap: 12px;
}
/* The metrics tab renders in a full-height, extra-wide panel (not the old 70vh
dialog), so give the 11 time-series charts a wider track than the dense
overview cards — legible ~23-column trends instead of a squeezed 4-up. */
.chartsGrid {
grid-template-columns: repeat(auto-fill, minmax(min(100%, 460px), 1fr));
gap: 16px;
}
.card {
border: 1px solid color-mix(in srgb, var(--border) 74%, transparent);
border-radius: 10px;
padding: 10px 14px;
min-width: 0;
}
.cardTitle {
margin: 0 0 8px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted-foreground);
}
/* Chart cards render the title and its ⓘ help as siblings in a flex row (the
hint must stay out of the <h3> for a clean heading accessible name). The row
owns the bottom gap so the heading's own margin doesn't double it. */
.cardHeader {
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 8px;
}
.cardHeader .cardTitle {
margin: 0;
}
/* Per-chart help affordance: an ⓘ button whose hover/focus reveals a plain
explanation. The card has no `overflow: hidden` and the ⓘ sits at the card's
top-left after the title, so the bubble opens downward into the card's own
area — never clipped, never pushing horizontal scroll. */
.infoHint {
position: relative;
display: inline-flex;
vertical-align: middle;
}
.infoHintButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--muted-foreground);
/* Reset the uppercase heading context so the glyph renders cleanly. */
text-transform: none;
letter-spacing: normal;
font-size: 13px;
line-height: 1;
cursor: help;
}
/* Focus/hover: a filled background (not a color shift alone) so keyboard focus
is unmistakable — WCAG 2.4.7. Mirrors the dialog's own header buttons
(DialogShell `.close` / `.iconButton`), which use --muted rather than an
outline; --muted is in scope under the DialogShell portal. */
.infoHintButton:hover,
.infoHintButton:focus-visible {
background: var(--muted);
color: var(--foreground);
}
.infoHintTip {
position: absolute;
top: calc(100% + 6px);
left: 0;
z-index: 30;
width: max-content;
max-width: 260px;
padding: 8px 10px;
border-radius: 6px;
/* Match the SvgLineChart data tooltip's surface; --popover/--card are absent
under the DialogShell body portal, so fall back to --background. */
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
background: var(--popover, var(--card, var(--background)));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
/* Reset the heading's uppercase/tracking so the sentence reads normally. */
text-transform: none;
letter-spacing: normal;
font-size: 12px;
font-weight: 400;
line-height: 1.45;
color: var(--foreground);
white-space: normal;
opacity: 0;
visibility: hidden;
transition: opacity 120ms ease;
pointer-events: none;
}
.infoHint:hover .infoHintTip,
.infoHint:focus-within .infoHintTip {
opacity: 1;
visibility: visible;
}
.row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 2px 0;
min-width: 0;
}
.rowLabel {
color: var(--muted-foreground);
}
.rowValue {
/* Keep short values (counts, "0 / 0 / 0", "first-responder") on one line;
the label wraps instead. Long path values opt back into wrapping via
.pathValue, which restores a small min-content width for this flex item. */
text-align: right;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.pathRow {
display: flex;
flex-direction: column;
gap: 2px;
padding: 2px 0;
min-width: 0;
}
.pathValue {
max-width: 100%;
font-family: var(--font-mono, monospace);
font-size: 12px;
/* Single line, ellipsis at the START so the tail (…/parent/workspace)
stays visible; the row widens/narrows with the card. */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
text-align: left;
}
.empty {
color: var(--muted-foreground);
padding: 4px 0;
}
.issueRow {
display: flex;
align-items: baseline;
gap: 10px;
padding: 3px 0;
}
.issueMessage {
overflow-wrap: anywhere;
}
.featureChips {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
/* A long capability set shouldn't make this card tower over its neighbours;
cap the height and let it scroll. */
max-height: 132px;
overflow-y: auto;
}
.featureChip {
padding: 1px 7px;
border-radius: 5px;
background: color-mix(in srgb, var(--border) 40%, transparent);
color: var(--muted-foreground);
font-size: 11px;
line-height: 1.5;
font-family: var(--font-mono, monospace);
white-space: nowrap;
}
.summaryChip {
padding: 2px 8px;
border-radius: 6px;
background: color-mix(in srgb, var(--border) 40%, transparent);
font-size: 12px;
font-family: var(--font-mono, monospace);
overflow-wrap: anywhere;
}
.sessionRow {
padding: 6px 0;
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.sessionRow:first-of-type {
border-top: none;
}
.sessionName {
font-family: var(--font-mono, monospace);
font-size: 12px;
overflow-wrap: anywhere;
}
.sessionMeta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 2px;
color: var(--muted-foreground);
font-size: 12px;
}
.activePrompt {
color: var(--warning-color);
}
.workspaceRow {
padding: 6px 0;
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.workspaceRow:first-of-type {
border-top: none;
}
.workspaceRowHead {
display: flex;
align-items: center;
gap: 10px;
}
.workspaceName {
font-weight: 600;
}
.workspaceDuration {
margin-left: auto;
color: var(--muted-foreground);
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.workspaceError {
margin-top: 2px;
color: var(--error-color);
font-size: 12px;
overflow-wrap: anywhere;
}
.workspaceSummary {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.workspaceCell {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
font-size: 12px;
}
.workspaceCellLabel {
font-family: var(--font-mono, monospace);
font-weight: 600;
}
.workspaceCellMessage {
color: var(--muted-foreground);
overflow-wrap: anywhere;
}