Repository navigation
epic: make the bundled WebUI usable, on the shared ui-common foundation #1910
Description
Problem / Background
Epic #1834 shipped a bundled WebUI whose contracts, security and accessibility gates pass, but whose screens are not usable: the Settings page is one 5,720 px scroll of 129 inputs, the Models page shows four rows per viewport, the Chat transcript has no role distinction, Korean mode is mostly English, and the Activity counters read zero for every UI chat because the server never records streaming completions. The four follow-ups filed on 2026-09-16 (#1900, #1901, #1902, #1903) and the ROCm catalog bug #1886 become children of this epic rather than standalone work.
Goal
Turn the bundled WebUI into a product surface: dense, task-first screens on the shared @lablup/ui-common foundation, every user-facing string in the catalog, honest metrics measured on the path the UI actually uses, and no internal state leaking into copy.
Evidence (review of 2026-09-17, release build of main 0ef0a1a4)
The review ran the real binary (mlxcel-server --webui --models-dir models/mlx --api-key-file ... --no-models-autoload --settings --props --metrics --slots) against 212 local checkpoints and drove login, load, streaming chat and unload with Playwright at 1440x900 and 390x844. Measured:
- Settings page: 5,720 px tall, 129 form inputs, 763 words, 11
h2headings, one uninterrupted scroll (webui/src/app.tsxSettingsScreen pluswebui/src/features/settings/server-settings.tsx). - Models page: 3,132 px tall at 1440x900 with four table rows visible in the viewport; each row is about 100 px because the name cell stacks a ghost button, "Selected", and a
source · quantizationline (webui/src/features/models/screen.tsx:186-242). At 390 px the page is 5,936 px tall. - Chat page renders prompt, an "Edit and regenerate" button, the response, "Copy response" and a "Response details" disclosure as one flat article with no role distinction (
webui/src/features/chat/transcript.tsx); conversations are a<select>; the composer is not pinned; the transcript is a separate 60vh scroll box (webui/src/features/chat/chat.css). - Server defect: streaming chat completions are never recorded in
Metrics::record_request(src/server/state.rs:62-75);src/server/routes/chat.rs:776records only the non-streaming path (the other call in that file,chat.rs:1349, is the audio transcription stream), whilesrc/server/routes/anthropic.rs:667andsrc/server/routes/responses.rs:794record their streaming paths. Verified: one streaming plus one non-streaming/v1/chat/completionsrequest yieldscompleted_requests_total = 1andcompletion_tokens_total = 19from/ui-api/v1/runtime. The WebUI chat always streams, so Activity shows 0 completed requests and 0 completion tokens for every UI session. - Localization:
webui/src/features/chat/chat.tsxcontains 49 hard-coded English literals andwindow.confirmin 4 places (chat.tsx,privacy.tsx); the settings screens use a localwords(en, ko)helper at 10 sites;webui/src/features/activity/strings.tsandwebui/src/features/models/next-load-profile.tsxbranch onlocale === 'ko'. In Korean mode the Chat screen shows only its title in Korean. This violatesdocs/webui/ux-contract.md(every user-facing string has a key with English and Korean values intests/fixtures/webui/strings.json). - The toolbar shows the browser's selected model, not the server's loaded models (
webui/src/provider-surfaces.tsxselectedModelLabel); a fresh tab reads "No model selected" while a model is Ready on the server. The Chat model picker lists all 212 catalog entries with a lifecycle suffix. - Internal state exposed as copy: the inspector prints opaque
mdl_...ids, raw reason strings such as "parameter count is not measured during metadata-only catalog scans", "Worker exit observed: No" for a Ready model, capability rows likechat · pre_load · Yes(webui/src/features/models/inspector.tsx); the Models page header prints "Backend router_pool; build 0.7.0; state streaming; catalog 212; operations 1; snapshot 3." and a "Library operations" list withop_model_load_000001that duplicates Activity (webui/src/features/models/operations.tsx). - Catalog display names replace
-and_with spaces (display_nameinsrc/server/webui/catalog_metadata.rs:465-470), soqwen3-0.6b-4bitshows asqwen3 0.6b 4bitand the inference id is hidden. - Activity: slot 0 shows a
21 / 40960 tokensbar while slots 1 to 3 print "context denominator is unknown" althoughrequest_context_tokensis the same 40960; the bar is drawn only whenprompt_tokensis non-null (webui/src/features/activity/runtime.tsx). "Unavailable measurements: 12" is a headline. - Live settings render f32 values through f64 (
0.800000011920929,0.949999988079071), raw names (default_dry_multiplier) and the stringnull(webui/src/features/settings/live-settings.tsx). - Theme:
data-theme="system"is written verbatim under the default preference so[data-theme="dark"]and[data-theme="light"]never match (already the subject of feat(webui): adopt the ui-common theme system and add a glass theme #1903). Two token systems coexist (--color-*inwebui/src/design-system/tokens.css,--token-*inwebui/src/design-system/common-tokens.css) andwebui/src/design-system/components.cssoverrides ui-common classes. - Global shortcuts: the shell implements only Cmd/Ctrl+K and
?(webui/src/design-system/shell.tsx:34-39); the contract also lists Cmd/Ctrl+N and Cmd/Ctrl+Enter.
Contract for this epic
- Keep every gate from epic: ship a bundled macOS-style model control interface #1834: keyboard-first navigation, visible focus, focus restoration, 200 percent zoom, off-canvas sidebar at 390 px with no horizontal page scroll, WCAG 2.2 AA, IME-safe inputs, CSP without inline scripts, and the
/ui-api/v1contract unchanged unless a child says otherwise. - Foundation before screens: theme (feat(webui): adopt the ui-common theme system and add a glass theme #1903) and components (refactor(webui): adopt every shared @lablup/ui-common component #1902) land first so every screen redesign composes the shared components once.
- Every user-facing string goes through
webui/src/i18n/catalog.tswithtests/fixtures/webui/strings.jsonin sync; nowindow.confirm,window.alertorwindow.prompt. - Internal identifiers, raw reason strings and enum names are not user copy. They may appear inside an explicit details disclosure or a diagnostics export.
- Every metric shown must be measured for the path the UI actually uses (streaming chat).
- Each child is one branch and one PR, closes only its child and references this epic. Verify against the running bundled binary, not only mocks; screenshots of mocks do not prove acceptance.
Sub-issues
Filed 2026-09-18. #1886, #1911 and #1912 have no dependency on any other child and sit in Phase 1 so they are dispatched at once; #1911 and #1912 are server-side changes the Phase 2 screens consume.
Phase 1
- feat(webui): adopt the ui-common theme system and add a glass theme #1903 theme system and dead-selector gate
- refactor(webui): adopt every shared @lablup/ui-common component #1902 adopt every shared ui-common component
- refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913 string catalog consolidation and native dialog removal
- fix(server): record streaming chat completions in Metrics so Activity stops reporting zero #1911 streaming chat completions recorded in Metrics (server)
- fix(webui): catalog display names must keep the checkpoint's real name #1912 catalog display names keep the inference id (server)
- fix(rocm): the WebUI catalog reports every architecture unsupported on ROCm #1886 ROCm catalog reports every architecture unsupported (independent of the other children)
Phase 2
- feat(webui): app shell shows loaded models, uses PageLayout/PageHeader, and completes the global shortcuts #1914 app shell shows loaded models, PageLayout/PageHeader, remaining global shortcuts (depends on refactor(webui): adopt every shared @lablup/ui-common component #1902, feat(webui): adopt the ui-common theme system and add a glass theme #1903, refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913)
- fix(webui): make the whole model library row open the inspector #1901 row activation (depends on refactor(webui): adopt every shared @lablup/ui-common component #1902)
- feat(webui): redesign the model library as a dense, task-first list with row actions #1918 model library redesign (depends on fix(webui): make the whole model library row open the inspector #1901, fix(webui): catalog display names must keep the checkpoint's real name #1912, refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913, feat(webui): app shell shows loaded models, uses PageLayout/PageHeader, and completes the global shortcuts #1914)
- feat(webui): rebuild Chat as a conversation view with a conversation list and a pinned composer #1915 chat redesign (depends on refactor(webui): adopt every shared @lablup/ui-common component #1902, refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913, feat(webui): app shell shows loaded models, uses PageLayout/PageHeader, and completes the global shortcuts #1914)
- feat(webui): make Activity read as a status page, with consistent slot occupancy and honest counters #1916 activity fixes (depends on fix(server): record streaming chat completions in Metrics so Activity stops reporting zero #1911, refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913, feat(webui): app shell shows loaded models, uses PageLayout/PageHeader, and completes the global shortcuts #1914)
- feat(webui): restructure Settings into scoped sections with schema-typed inputs #1917 settings restructure (depends on refactor(webui): adopt every shared @lablup/ui-common component #1902, refactor(webui): move every user-facing string into the i18n catalog and remove native confirm dialogs #1913, feat(webui): app shell shows loaded models, uses PageLayout/PageHeader, and completes the global shortcuts #1914)
Phase 3
- docs(webui): ship release-time user docs and surface --webui in --help #1900 release documentation and
--webuiin--help, written against the final UI (depends on feat(webui): app shell shows loaded models, uses PageLayout/PageHeader, and completes the global shortcuts #1914, feat(webui): rebuild Chat as a conversation view with a conversation list and a pinned composer #1915, feat(webui): make Activity read as a status page, with consistent slot occupancy and honest counters #1916, feat(webui): restructure Settings into scoped sections with schema-typed inputs #1917, feat(webui): redesign the model library as a dense, task-first list with row actions #1918)
Definition of done
- All children closed by merged PRs, each verified against the installed bundled binary with a real checkpoint.
-
pnpm --dir webui run lint,typecheck,unit,browser,verify-generatedgreen;cargo test --workspace --profile test-fast --features metal,accelerate, clippy and fmt green. - A final walkthrough at 1440x900 and 390x844 records: find a model, load it, chat, stop, unload, all reachable from the Models list without opening the inspector by name; Activity shows non-zero completed requests after one streaming chat; Korean mode has no English copy outside proper nouns and identifiers.
- Page heights and word counts for Models, Chat, Activity and Settings are recorded before and after in the closing comment.
Technical Considerations
/epic-impl reads the ## Sub-issues section as the execution DAG: items inside one ### Phase N heading run in parallel unless an explicit (depends on ...) annotation narrows the wait. The two server-only children (#1911, #1912) sit in Phase 1 with no annotation so they start immediately; #1918 and #1916 name them as dependencies.
Activity
fix(server): record streamed chat completions in Metrics (#1919)
fix(webui): harden Chat confirmations and tighten the i18n drift gate
fix(rocm): select the catalog backend column from the resolved backend (
refactor(webui): key screen copy in the catalog and drop window.confi…
refactor(webui): adopt every shared @lablup/ui-common component (#1924)
fix(webui): print the checkpoint's real name as its display name (#1920)
feat(webui): adopt the ui-common theme system and add a glass theme (#…
fix(webui): make row re-selection a no-op and harden row delegation
fix(webui): open the model inspector from anywhere in a library row (#…
feat(webui): show loaded models and compose routes on the app shell (#…
fix(webui): localize failed-operation error codes on Activity
feat(webui): split Settings into scoped tabs with typed live controls (…
feat(webui): rebuild Chat as a conversation view (#1931)
feat(webui): make Activity read as a status page (#1929)