You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Activity page is a stack of prose and repeated "N/A" lines rather than a status page. Observed 2026-09-17 against a release build of main 0ef0a1a4 after one streaming chat on qwen3-0.6b-4bit (mlxcel-server --webui --models-dir models/mlx --api-key-file <f> --no-models-autoload --settings --props --metrics --slots, Playwright at 1440x900): the page is 1,584 px tall and 254 words for four idle slots and one operation. Three defects sit on top of the layout problem:
Slot 0 shows a 21 / 40960 tokens bar while slots 1 to 3 print "context denominator is unknown; no occupancy percentage is inferred" (text.noContext) although runtime.slots.request_context_tokens is 40960 for every slot. The bar is gated on slot.prompt_tokens !== null (webui/src/features/activity/runtime.tsx:26), and the server sets prompt_tokens to null whenever the slot has no task (src/server/slots_state.rs:194), so an idle slot is mislabelled as having an unknown denominator. "Accepted decode: N/A tokens · Cached prompt: N/A tokens" (runtime.tsx:27) repeats once per idle slot.
Identifiers are page copy: each operation article prints the operation id and model id inside "Operation details" (webui/src/features/activity/operations.tsx:36), the kind is operation.kind.replaceAll('_', ' ') and the state badge shows the raw enum word (operations.tsx:31).
Current Behavior
webui/src/features/activity/index.tsx:23-32 stacks: a .screen-heading with "Last successful snapshot"; a full-width "Select a model to observe" Select; a button row (Refresh observations, Export sanitized diagnostics); the stale banner; Operations; RuntimeView; and a full-width "Show recent history" toggle that lazy-loads history.tsx, an SVG of discrete dots. operations.tsx:14 opens with "Operations: 0 active · 0 failed" and the retention sentence text.session. runtime.tsx:18-20 renders four dl.activity-metric tiles then the sentence "Unavailable measurements: 12. Some sources are disabled or do not publish an authoritative sample. Expand details for individual reasons." as body text, then "Request slots", "Effective parallelism: N/A / 4", "Request context window: 40960 tokens · Shared pool context: N/A tokens" and one <section> per slot. All copy lives in webui/src/features/activity/strings.ts outside webui/src/i18n/catalog.ts (the locale === 'ko' branch at strings.ts:37, and inline locale === 'ko' ? '진행 중' : 'active' at operations.tsx:14). format.ts:4-7metricValue formats a MeasuredValue with its unit.
Proposed Solution
Recompose the page on #1914 (PageHeader with actions) and the @lablup/ui-common components that #1902 exposes through webui/src/design-system/primitives.tsx (StatCard, Badge, SmoothHeight, Skeleton, EmptyState, Tooltip). Feature code keeps importing from primitives.tsx, never from the package (docs/webui/ui-common.md).
Header. Title, then the model Select and the Refresh and Export buttons in the PageHeader actions slot. Options sort by entry.lifecycle.state, Ready models first, then the catalog order, so the loaded model is the first choice instead of one row among 212. The last-snapshot time moves into the header description. The Select stays a combobox whose options are identity.display_name, because webui/scripts/activity-performance.mjs:55-61 selects the model by getByRole('combobox') and getByRole('option', { name }).
Summary tiles. Four StatCards (Active requests, Completed requests, Completion tokens, Queued requests) with hint set to the measured_at time and metric.reason (provenance). A null value renders the card with value "unknown" and the reason as hint, never 0 and never hidden. While snapshot.runtimes has no entry for the selected model and the connection is ready, render four StatCard loading skeletons rather than the "no authoritative sample is available" EmptyState.
Unavailable count. Replace the sentence at runtime.tsx:20 with a Badge reading "12 unavailable" placed inside the <summary> of the existing details.activity-measurement-details, so activating it opens the per-metric list. The badge is omitted when the count is 0. The memory-scope and timing notes (text.memory, text.timing) stay inside that disclosure.
Slot table. Replace the per-slot <section>s with a compact DataTable (the typed export in primitives.tsx, required for Activity by docs/webui/ui-common.md) with columns slot, state (StatusBadge Processing or Idle), occupancy (ProgressBar with prompt_tokens / request_context_tokens tokens), decoded, cached prompt. Rules, in this order: if request_context_tokens is null or 0, every occupancy cell shows the existing "unknown denominator" text and no bar; else if prompt_tokens is non-null, bar plus N / D tokens; else if processing is false, 0 / D tokens with an empty bar, because the server derives null from the absence of a task (slots_state.rs:194), not from a failed measurement, and an empty slot holds no context; else (processing with null count) "unknown". Decoded and cached cells show the number, "unknown" for null on a processing slot, and stay empty for an idle slot with no task. The 0 / D idle case is a documented exception to "null is never shown as zero" in docs/webui/ux-contract.md:23; add the sentence there and in docs/webui/activity.md:11. "Effective parallelism", "Request context window" and "Shared pool context" become one line above the table; a null value still reads "unknown".
Operations. Each row is one line inside SmoothHeight (active when the list length changes): kind label from the catalog (six keys, one per OperationKind: catalog_refresh, model_load, model_unload, download, model_removal, settings_patch), target label (unchanged logic from operations.tsx:22), StatusBadge with the existing state mapping (operations.tsx:31) but a catalog label per OperationState instead of the enum word, relative time via a new relativeTime(iso, now, locale) in webui/src/features/activity/format.ts built on Intl.RelativeTimeFormat (no such helper exists in webui/src/design-system/format.ts), the download ProgressBar and the cancel Button for non-terminal rows. Operation id and model id stay behind the "Operation details" disclosure; nothing else prints them. The role="status" aria-live="polite" count moves into the section heading as "1 active · 0 failed". The retention sentence (text.session) becomes the EmptyState body when there are no operations and a Tooltip on the count otherwise.
History. Delete the page-wide "Show recent history" toggle and the lazy import. history.tsx becomes a small always-rendered sparkline passed to the Active requests StatCardsparkline slot (StatCardProps.sparkline in dist/components/StatCard/StatCard.d.ts), keeping the discrete-dot SVG so gaps stay blank; rendered only when runtimeHistory holds two or more samples with a non-null active_requests. historyNote and the per-point list move into the measurements disclosure.
Rejected: computing a percentage for a processing slot with null prompt_tokens from decoded_tokens (invents occupancy, docs/webui/activity.md:11); summing memory scopes into one tile (they overlap on unified memory).
Scope
In scope:webui/src/features/activity/{index,operations,runtime,history}.tsx, format.ts, strings.ts, activity.css; catalog keys in webui/src/i18n/catalog.ts and tests/fixtures/webui/strings.json; tests activity.test.tsx, format.test.ts, webui/tests/activity.spec.ts; webui/scripts/activity-performance.mjs selectors if the DOM change requires; docs/webui/activity.md and docs/webui/ux-contract.md for the idle-slot rule.
Out of scope: server changes (#1911 owns the counters; src/server/webui/runtime.rs is untouched); the shell and PageHeader (shell child); catalog display names (names child); the diagnostics allowlist in format.ts:17-30, which keeps its shape and its test.
Implementation Notes
Reuse: metricValue and operationProgress (format.ts), isTerminal (webui/src/state/observation.ts:24), StatusBadge/ProgressBar/DataTable/EmptyState adapters in common-adapters.tsx, downloadDiagnostics unchanged.
Constraints: no /ui-api/v1 contract change; keyboard order, visible focus, axe clean and expectSafeLayout at 1440 and 390 (activity.spec.ts); the observation schedule and the 2 percent median-degradation budget of scripts/webui/verify_activity_performance.py and webui/scripts/activity-performance.mjs are unchanged, so no new polling or timers; sparkline SVG has role="img" and a catalog aria-label.
Edge cases: slots.available === false with a reason (fixture tests/fixtures/webui/examples/runtime.snapshot.json) renders the reason in place of the table; slots.reason "showing the first 256 observational slots" still appears above the table (existing test at activity.test.tsx:59-64); more than 8 slots scroll inside the table, not the page; a failed operation keeps its ErrorBanner with error.code only, never error.message (activity.test.tsx:36-43); stale connection disables cancel and shows the warning banner as today.
Error handling: cancel failure keeps the inline cancelFailed banner; a runtime fetch error leaves the previous tiles with the stale banner rather than blanking them.
With --slots and four idle slots, every idle row shows 0 / 40960 tokens; "unknown" appears only when request_context_tokens is null, covered by a unit test that fixes the mislabelled-idle case and was shown failing before the change.
No operation id or model id is visible outside an "Operation details" disclosure; the unit test asserts the ids are absent from the collapsed DOM text.
At 1440x900 with four idle slots and one operation, document.documentElement.scrollHeight <= 900 in a browser test using the mock API; the value is recorded in the PR with the before number 1,584.
The Unavailable badge opens the measurements disclosure; the sentence at runtime.tsx:20 no longer exists.
The kind and state of every operation row come from the catalog in both locales; strings.ts holds no locale === 'ko' ternary.
Integrated into the real page (index.tsx), not a parallel component; pnpm --dir webui run lint, typecheck, unit, browser, verify-generated green; the Activity performance script passes in visible-only-headed mode at minimum against the bundled binary.
Verification
pnpm --dir webui run lint && pnpm --dir webui run typecheck && pnpm --dir webui run unit && pnpm --dir webui run browser && pnpm --dir webui run verify-generated
cargo build --release --features metal,accelerate
./target/release/mlxcel-server --webui --models-dir models/mlx --api-key-file <f> --no-models-autoload --settings --props --metrics --slots
# load qwen3-0.6b-4bit from Models, send one chat from Chat, open Activity: tiles 1 / non-zero, four idle rows read 0 / 40960 tokens, page has no vertical scrollbar at 1440x900cd webui && WEBUI_PERF_MODE=visible-only-headed WEBUI_PERF_BASE=http://127.0.0.1:8080/ WEBUI_PERF_KEY_FILE=<f> WEBUI_PERF_INFERENCE_MODEL=<inference-id> WEBUI_PERF_MODEL_ID=<mdl_id> WEBUI_PERF_OUTPUT=/tmp/activity-paired.json node scripts/activity-performance.mjs
Technical Considerations
Depends on #1911 (counters), #1902 (StatCard, Badge, SmoothHeight, Skeleton, Tooltip through primitives.tsx), #1914 (PageHeader actions) and #1913 (catalog consolidation). Sibling of the Models, Chat and Settings redesign children in #1910 Phase 2.
Part of #1910.
Problem / Background
The Activity page is a stack of prose and repeated "N/A" lines rather than a status page. Observed 2026-09-17 against a release build of main
0ef0a1a4after one streaming chat onqwen3-0.6b-4bit(mlxcel-server --webui --models-dir models/mlx --api-key-file <f> --no-models-autoload --settings --props --metrics --slots, Playwright at 1440x900): the page is 1,584 px tall and 254 words for four idle slots and one operation. Three defects sit on top of the layout problem:21 / 40960 tokensbar while slots 1 to 3 print "context denominator is unknown; no occupancy percentage is inferred" (text.noContext) althoughruntime.slots.request_context_tokensis 40960 for every slot. The bar is gated onslot.prompt_tokens !== null(webui/src/features/activity/runtime.tsx:26), and the server setsprompt_tokensto null whenever the slot has no task (src/server/slots_state.rs:194), so an idle slot is mislabelled as having an unknown denominator. "Accepted decode: N/A tokens · Cached prompt: N/A tokens" (runtime.tsx:27) repeats once per idle slot.Metrics(fix(server): record streaming chat completions in Metrics so Activity stops reporting zero #1911).articleprints the operation id and model id inside "Operation details" (webui/src/features/activity/operations.tsx:36), the kind isoperation.kind.replaceAll('_', ' ')and the state badge shows the raw enum word (operations.tsx:31).Current Behavior
webui/src/features/activity/index.tsx:23-32stacks: a.screen-headingwith "Last successful snapshot"; a full-width "Select a model to observe"Select; a button row (Refresh observations, Export sanitized diagnostics); the stale banner;Operations;RuntimeView; and a full-width "Show recent history" toggle that lazy-loadshistory.tsx, an SVG of discrete dots.operations.tsx:14opens with "Operations: 0 active · 0 failed" and the retention sentencetext.session.runtime.tsx:18-20renders fourdl.activity-metrictiles then the sentence "Unavailable measurements: 12. Some sources are disabled or do not publish an authoritative sample. Expand details for individual reasons." as body text, then "Request slots", "Effective parallelism: N/A / 4", "Request context window: 40960 tokens · Shared pool context: N/A tokens" and one<section>per slot. All copy lives inwebui/src/features/activity/strings.tsoutsidewebui/src/i18n/catalog.ts(thelocale === 'ko'branch atstrings.ts:37, and inlinelocale === 'ko' ? '진행 중' : 'active'atoperations.tsx:14).format.ts:4-7metricValueformats aMeasuredValuewith its unit.Proposed Solution
Recompose the page on #1914 (PageHeader with
actions) and the@lablup/ui-commoncomponents that #1902 exposes throughwebui/src/design-system/primitives.tsx(StatCard,Badge,SmoothHeight,Skeleton,EmptyState,Tooltip). Feature code keeps importing fromprimitives.tsx, never from the package (docs/webui/ui-common.md).Selectand the Refresh and Export buttons in the PageHeaderactionsslot. Options sort byentry.lifecycle.state, Ready models first, then the catalog order, so the loaded model is the first choice instead of one row among 212. The last-snapshot time moves into the header description. TheSelectstays a combobox whose options areidentity.display_name, becausewebui/scripts/activity-performance.mjs:55-61selects the model bygetByRole('combobox')andgetByRole('option', { name }).StatCards (Active requests, Completed requests, Completion tokens, Queued requests) withhintset to themeasured_attime andmetric.reason(provenance). A null value renders the card with value "unknown" and the reason as hint, never 0 and never hidden. Whilesnapshot.runtimeshas no entry for the selected model and the connection isready, render fourStatCard loadingskeletons rather than the "no authoritative sample is available"EmptyState.runtime.tsx:20with aBadgereading "12 unavailable" placed inside the<summary>of the existingdetails.activity-measurement-details, so activating it opens the per-metric list. The badge is omitted when the count is 0. The memory-scope and timing notes (text.memory,text.timing) stay inside that disclosure.<section>s with a compactDataTable(the typed export inprimitives.tsx, required for Activity bydocs/webui/ui-common.md) with columns slot, state (StatusBadgeProcessing or Idle), occupancy (ProgressBarwithprompt_tokens / request_context_tokens tokens), decoded, cached prompt. Rules, in this order: ifrequest_context_tokensis null or 0, every occupancy cell shows the existing "unknown denominator" text and no bar; else ifprompt_tokensis non-null, bar plusN / D tokens; else ifprocessingis false,0 / D tokenswith an empty bar, because the server derives null from the absence of a task (slots_state.rs:194), not from a failed measurement, and an empty slot holds no context; else (processing with null count) "unknown". Decoded and cached cells show the number, "unknown" for null on a processing slot, and stay empty for an idle slot with no task. The0 / Didle case is a documented exception to "null is never shown as zero" indocs/webui/ux-contract.md:23; add the sentence there and indocs/webui/activity.md:11. "Effective parallelism", "Request context window" and "Shared pool context" become one line above the table; a null value still reads "unknown".SmoothHeight(activewhen the list length changes): kind label from the catalog (six keys, one perOperationKind:catalog_refresh,model_load,model_unload,download,model_removal,settings_patch), target label (unchanged logic fromoperations.tsx:22),StatusBadgewith the existing state mapping (operations.tsx:31) but a catalog label perOperationStateinstead of the enum word, relative time via a newrelativeTime(iso, now, locale)inwebui/src/features/activity/format.tsbuilt onIntl.RelativeTimeFormat(no such helper exists inwebui/src/design-system/format.ts), the downloadProgressBarand the cancelButtonfor non-terminal rows. Operation id and model id stay behind the "Operation details" disclosure; nothing else prints them. Therole="status" aria-live="polite"count moves into the section heading as "1 active · 0 failed". The retention sentence (text.session) becomes theEmptyStatebody when there are no operations and aTooltipon the count otherwise.history.tsxbecomes a small always-rendered sparkline passed to the Active requestsStatCardsparklineslot (StatCardProps.sparklineindist/components/StatCard/StatCard.d.ts), keeping the discrete-dot SVG so gaps stay blank; rendered only whenruntimeHistoryholds two or more samples with a non-nullactive_requests.historyNoteand the per-point list move into the measurements disclosure.catalog.tskey withtests/fixtures/webui/strings.jsonin sync;strings.tsshrinks to whatever refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913 has not already moved, and the inlinelocale === 'ko'ternaries atoperations.tsx:14go. Nowindow.confirm.Rejected: computing a percentage for a processing slot with null
prompt_tokensfromdecoded_tokens(invents occupancy,docs/webui/activity.md:11); summing memory scopes into one tile (they overlap on unified memory).Scope
In scope:
webui/src/features/activity/{index,operations,runtime,history}.tsx,format.ts,strings.ts,activity.css; catalog keys inwebui/src/i18n/catalog.tsandtests/fixtures/webui/strings.json; testsactivity.test.tsx,format.test.ts,webui/tests/activity.spec.ts;webui/scripts/activity-performance.mjsselectors if the DOM change requires;docs/webui/activity.mdanddocs/webui/ux-contract.mdfor the idle-slot rule.Out of scope: server changes (#1911 owns the counters;
src/server/webui/runtime.rsis untouched); the shell and PageHeader (shell child); catalog display names (names child); the diagnostics allowlist informat.ts:17-30, which keeps its shape and its test.Implementation Notes
metricValueandoperationProgress(format.ts),isTerminal(webui/src/state/observation.ts:24),StatusBadge/ProgressBar/DataTable/EmptyStateadapters incommon-adapters.tsx,downloadDiagnosticsunchanged./ui-api/v1contract change; keyboard order, visible focus, axe clean andexpectSafeLayoutat 1440 and 390 (activity.spec.ts); the observation schedule and the 2 percent median-degradation budget ofscripts/webui/verify_activity_performance.pyandwebui/scripts/activity-performance.mjsare unchanged, so no new polling or timers; sparkline SVG hasrole="img"and a catalogaria-label.slots.available === falsewith a reason (fixturetests/fixtures/webui/examples/runtime.snapshot.json) renders the reason in place of the table;slots.reason"showing the first 256 observational slots" still appears above the table (existing test atactivity.test.tsx:59-64); more than 8 slots scroll inside the table, not the page; a failed operation keeps itsErrorBannerwitherror.codeonly, nevererror.message(activity.test.tsx:36-43); stale connection disables cancel and shows the warning banner as today.cancelFailedbanner; a runtime fetch error leaves the previous tiles with the stale banner rather than blanking them.Acceptance Criteria
--slotsand four idle slots, every idle row shows0 / 40960 tokens; "unknown" appears only whenrequest_context_tokensis null, covered by a unit test that fixes the mislabelled-idle case and was shown failing before the change.document.documentElement.scrollHeight <= 900in a browser test using the mock API; the value is recorded in the PR with the before number 1,584.runtime.tsx:20no longer exists.strings.tsholds nolocale === 'ko'ternary.index.tsx), not a parallel component;pnpm --dir webui run lint,typecheck,unit,browser,verify-generatedgreen; the Activity performance script passes invisible-only-headedmode at minimum against the bundled binary.Verification
Technical Considerations
Depends on #1911 (counters), #1902 (StatCard, Badge, SmoothHeight, Skeleton, Tooltip through
primitives.tsx), #1914 (PageHeaderactions) and #1913 (catalog consolidation). Sibling of the Models, Chat and Settings redesign children in #1910 Phase 2.