Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f75f984
feat(webui): adopt the ui-common theme system and add a glass theme
inureyes Sep 19, 2026
393a08d
fix(webui): paint the glass button fills on rendered buttons
inureyes Sep 19, 2026
2ded38b
fix(webui): tighten the theme bootstrap checks and glass danger shadow
inureyes Sep 19, 2026
28c7fa5
fix(webui): harden the theme gate and keep glass off content controls
inureyes Sep 19, 2026
f3266c3
test(webui): cover glass intensity clamping in appearance preferences
inureyes Sep 19, 2026
1f28f7d
docs(webui): refresh WebUI test counts after rebasing on main
inureyes Sep 19, 2026
cdb80fe
Merge origin/main (#1924) into the theme-system branch
inureyes Sep 19, 2026
83fc058
Merge remote-tracking branch 'origin/main' into feature/issue-1903-th…
inureyes Sep 19, 2026
c4a71c6
feat(webui): show loaded models and compose routes on the app shell
inureyes Sep 19, 2026
b12ca28
Merge remote-tracking branch 'origin/main' into feature/issue-1914-ap…
inureyes Sep 19, 2026
4316635
fix(webui): keep loaded-model chips naming their models when tight
inureyes Sep 19, 2026
2fa91cb
fix(webui): say loaded models are unknown and scroll compact chips
inureyes Sep 19, 2026
0fe8899
Merge remote-tracking branch 'origin/main' into feature/issue-1914-ap…
inureyes Sep 19, 2026
e2fe6a4
docs(webui): refresh WebUI test counts after merging main
inureyes Sep 19, 2026
ad969c9
fix(webui): stop Ctrl+N wiping the composer draft on macOS
inureyes Sep 19, 2026
b9465f9
fix(webui): hold the 195 px floor and chip names with wide fonts
inureyes Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/webui/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Chat reads the canonical memory-only generation defaults saved in Settings. Open

## Interaction and cancellation

Enter sends outside IME composition; Shift+Enter inserts a newline. New, rename, delete, system prompt, copy, and explicit edit-and-regenerate are local conversation operations. Editing asks before discarding that response and subsequent turns. Reasoning is collapsed by default; tool calls are inspectable structured text and are never executed. A response must include a finish reason, nonempty content/reasoning/tool data, and a complete SSE stream before it becomes complete. Network errors, authentication loss, navigation and Stop do not retry generation or mark a partial answer successful.
Enter and Cmd/Ctrl+Enter send outside IME composition; Shift+Enter inserts a newline. In the composer, Cmd+N on Apple platforms and Ctrl+N elsewhere start a new empty conversation, leaving Ctrl+N as the native caret-movement binding inside a macOS textarea, and nothing fires while composition is active (`isComposing` or keyCode 229). The New conversation button sits in the Chat page header; the conversation and model pickers stay below it. A new conversation is refused at the 50-conversation cap and while a request, history operation or image load is busy. New, rename, delete, system prompt, copy, and explicit edit-and-regenerate are local conversation operations. Editing asks before discarding that response and subsequent turns. Reasoning is collapsed by default; tool calls are inspectable structured text and are never executed. A response must include a finish reason, nonempty content/reasoning/tool data, and a complete SSE stream before it becomes complete. Network errors, authentication loss, navigation and Stop do not retry generation or mark a partial answer successful.

Stop aborts the transport and retains cancelled partial output. A follow-up runtime observation is useful diagnostic context, not a per-request cancellation receipt. The isolated real-browser acceptance harness separately observes the server's request lease count rise and return to zero after Stop. Closing a page never unloads a shared model.

Expand Down
12 changes: 10 additions & 2 deletions docs/webui/design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,23 @@ All controls expose visible focus, disabled and busy states, minimum 24 px targe

## Shell and localization

`AppShell` defines the sidebar, top toolbar, content column (the shared PageLayout) and optional inspector. Primary navigation contains Models, Chat, Activity and Settings; the component gallery remains available as the direct `#gallery` artifact route and is listed in the command palette only while that route is already active. Cmd/Ctrl+K opens the command palette, `?` opens keyboard help, Escape closes the active dialog through the native dialog mechanism or the navigation drawer through its own handler, with focus back on the opener, and `[`/`]` move primary navigation only while the sidebar owns focus. Global shortcuts are suppressed inside edit controls, modal dialogs, the open navigation drawer, IME composition and Alt-key chords. Compact widths below 960 px use the shared Drawer as a left off-canvas sheet opened from the toolbar, with the page behind it inert while it is open; widening past 960 px while it is open closes it and moves focus to the desktop sidebar. No permanent mobile rail is rendered.
`AppShell` (`webui/src/design-system/shell.tsx`) defines the sidebar, top toolbar, content column (the shared PageLayout) and optional inspector. App hands it plain data (`loadedModels` as `ShellLoadedModel` records and a `connection` summary) plus callbacks, so the shell imports neither the provider nor the catalog. Primary navigation contains Models, Chat, Activity and Settings. The component gallery is a direct `#gallery` artifact route only; it is in neither the navigation nor the command palette.

The toolbar names the models the server holds, from `loadedModels(snapshot)` in `webui/src/provider-surfaces.tsx`: entries in `ready`, `loading`, `draining` or `unloading`, ready first, then by display name. It never shows the browser's selection, which stays visible only in the Chat model picker and the Settings target. The region shows a count, up to `TOOLBAR_CHIP_LIMIT` (3) chips with the truncated name and a lifecycle StatusBadge, and a "+n" chip that opens the command palette; with nothing loaded it says "No model loaded". `AppShell.loadedModels` is `null` when the loaded models are not known, and the region then says "Loaded models unknown" instead: App passes `null` until an authenticated session holds a catalog snapshot, which covers signed out, the window before the first catalog page and the reset after a server restart, so the toolbar never claims that nothing is loaded on a server it has not read. On a desktop toolbar short of room the chips share it equally, and each gives up space in order: the name truncates down to 5rem, the badge narrows to its state dot, then the name truncates further. Below 960 px the region is a toolbar row of its own that scrolls sideways inside itself instead of wrapping: chips keep their width up to `min(14rem, 80%)` with truncated names, so the sticky toolbar stays two rows high (title, chips) however many models are loaded, and three below 320 px (title, actions, chips). A chip reached with Tab is scrolled fully into view, since Chromium leaves an element that is already partly visible where it is. The full name stays in the chip's `title` and the state in its accessible name. A chip selects the model and opens its inspector on Models, or opens Models with no inspector if the model has left the catalog; it never loads or unloads. Opening the model that is already selected dispatches no selection (`modelSelectionFor` in `provider-surfaces.tsx` computes the target), because a selection change restarts observation and clears the runtime history. Chips take their accessible name from their visible text and carry no `aria-label`. The sidebar footer reads `{mode} · {status} · v{version}`; the server instance id and the event sequence appear only inside a collapsed "Connection details" disclosure under it.

Every route renders exactly one `PageHeader` as the first child of the shell's PageLayout, with the route's actions in its action slot and the route's stale or action error in its error slot. The adapter always makes that h1 the route's dialog focus fallback (`tabIndex=-1` and `data-dialog-focus-fallback`), so each route has exactly one fallback and `restoreModalFocus` lands on a named heading when a dialog closes after its opener is gone. Routes do not hand-roll headings, eyebrow lines or heading button rows.

The command palette (`webui/src/command-palette.tsx`) opens with an empty query. Its Commands section lists the four routes and "New conversation", matched on the localized label or the route id, so Korean input finds them too. Its Models section lists the loaded models while the query is empty, and otherwise the catalog entries whose display name or id contains the query (`paletteModelMatches`: case-insensitive, loaded first, at most 20). Selecting a model does what a chip does; the palette has no way to load a model.

`globalShortcuts` in `shell.tsx` is the single source for the keyboard help dialog, so the list cannot drift from the handlers: Cmd/Ctrl+K, Cmd/Ctrl+N, Cmd/Ctrl+Enter, Esc, `[`/`]` and `?`. The shell handles Cmd/Ctrl+K (palette), `?` (help) and Cmd/Ctrl+N, which opens Chat and, when Chat is available, raises `requestNewConversation()` in `features/chat/session.ts`; Chat consumes that request once on mount or on change and runs its own guarded `create()`. Escape closes the active dialog through the native dialog mechanism or the navigation drawer through its own handler, with focus back on the opener, and `[`/`]` move primary navigation only while the sidebar owns focus. Global shortcuts are suppressed inside edit controls, modal dialogs, the open navigation drawer, IME composition and Alt-key chords; the chat composer handles Cmd/Ctrl+Enter and Cmd/Ctrl+N itself outside composition. Compact widths below 960 px use the shared Drawer as a left off-canvas sheet opened from the toolbar, with the page behind it inert while it is open; widening past 960 px while it is open closes it and moves focus to the desktop sidebar. No permanent mobile rail is rendered. Below 320 px the layout holds down to 195 CSS px; see the [layout floor](ux-contract.md#visual-and-viewport-contract).

The production shell consumes the shared `WebUiProvider` snapshot and actions directly: LoginView submits through `actions.login`, logout uses `actions.logout`, and connection, catalog/operation freshness, selected model and schema recovery derive from the shared authority. It maintains no second session cache. Initial browsing makes no local API call before explicit login; authenticated route browsing does not load models or call inference endpoints. Models, Chat and Activity currently expose truthful staged guidance until their feature-page issues implement the workflows.

Every user-facing string has a typed key in `webui/src/i18n/catalog.ts` and a synchronized entry in `tests/fixtures/webui/strings.json` with English, Korean and a primary test ID. Locale-aware formatting helpers live beside the design system for bytes and tokens/s.

## Verification

The component gallery is available at `#gallery`. `pnpm --dir webui run unit` executes the activity-stream and theme-selector node:test specs (35 tests) and 470 Vitest tests across 51 files, covering the shared client/provider, shell and theme behavior and the shared-component adapters. `pnpm --dir webui run browser` executes 88 Playwright tests, 89 with `MLXCEL_WEBUI_FONT_DIAGNOSTICS=1`: 21 layout and accessibility cases (14 gallery and 7 product-shell variants), seven further `browser.spec.ts` cases covering provider/mock-API journeys, keyboard/focus/shortcut behavior, production-selector compact reflow, contrast/reduced-motion/visibility behavior and backdrop-filter fallback, the Activity, Chat, Models and Settings route specs, the shared-component specs `ui-common-components.spec.ts` (14 cases) and `ui-common-shell.spec.ts` (12 cases), and 23 `theme.spec.ts` cases covering the pre-mount theme, live host-scheme changes, the Settings theme controls, the unchanged mlxcel baseline and the glass intensity, transparency, contrast and motion rules. The variants in `webui/tests/browser-fixtures.ts` cover 390/1024/1440 widths, all four theme ids, tinted/opaque, high contrast and CJK labels. Each of the 21 cases runs axe with all violations rejected and asserts measured geometry: document and key-panel overflow, toolbar hiding above 960 px, 44 px compact hit targets, data-table column visibility, heading hierarchy across repeated tab remounts and progress/sample row alignment. No pixel baselines are committed. A baseline is specific to one renderer, font set and platform, so keeping one per platform required a hosted capture and a manual visual review on every font, Chromium or UI change, and it bought no assertion that the geometry checks do not already make. A failing case still writes its actual page image and a trace into `webui/test-results/`, which is git-ignored and uploaded as a CI artifact; run with `--headed` to watch a variant render. The CI job still pins `fonts-dejavu-core`, `fonts-dejavu-extra` and `fonts-wqy-zenhei` on Ubuntu 24.04, because the geometry assertions measure text and the CJK drawer variant needs a Korean face to lay out at all. The opt-in `MLXCEL_WEBUI_FONT_DIAGNOSTICS=1` test reports the fonts Chromium actually resolved, rather than trusting `fc-match` alone.
The component gallery is available at `#gallery`. `pnpm --dir webui run unit` executes the activity-stream and theme-selector node:test specs (35 tests) and 538 Vitest tests across 54 files, covering the shared client/provider, shell and theme behavior and the shared-component adapters. `pnpm --dir webui run browser` executes 108 Playwright tests, 109 with `MLXCEL_WEBUI_FONT_DIAGNOSTICS=1`: 21 layout and accessibility cases (14 gallery and 7 product-shell variants), eight further `browser.spec.ts` cases covering provider/mock-API journeys, keyboard/focus/shortcut behavior (including the six-entry help list and Control+N), production-selector compact reflow, contrast/reduced-motion/visibility behavior and backdrop-filter fallback, the Activity, Chat, Models and Settings route specs, 14 `shell.spec.ts` cases (the 195 and 320 CSS px layout floor in English and Korean, the loaded-model chip and its "+n" overflow, chips still naming their models in a tight 1024 px toolbar, the compact chip row scrolling in place with the toolbar held at two rows at 400 and 700 px, and palette model search), the shared-component specs `ui-common-components.spec.ts` (14 cases) and `ui-common-shell.spec.ts` (12 cases), and 23 `theme.spec.ts` cases covering the pre-mount theme, live host-scheme changes, the Settings theme controls, the unchanged mlxcel baseline and the glass intensity, transparency, contrast and motion rules. The variants in `webui/tests/browser-fixtures.ts` cover 390/1024/1440 widths, all four theme ids, tinted/opaque, high contrast and CJK labels. Each of the 21 cases runs axe with all violations rejected and asserts measured geometry: document and key-panel overflow, toolbar hiding above 960 px, 44 px compact hit targets, data-table column visibility, heading hierarchy across repeated tab remounts and progress/sample row alignment. No pixel baselines are committed. A baseline is specific to one renderer, font set and platform, so keeping one per platform required a hosted capture and a manual visual review on every font, Chromium or UI change, and it bought no assertion that the geometry checks do not already make. A failing case still writes its actual page image and a trace into `webui/test-results/`, which is git-ignored and uploaded as a CI artifact; run with `--headed` to watch a variant render. The CI job still pins `fonts-dejavu-core`, `fonts-dejavu-extra` and `fonts-wqy-zenhei` on Ubuntu 24.04, because the geometry assertions measure text and the CJK drawer variant needs a Korean face to lay out at all. The opt-in `MLXCEL_WEBUI_FONT_DIAGNOSTICS=1` test reports the fonts Chromium actually resolved, rather than trusting `fc-match` alone.

The text-scale case uses a test-only root attribute to enlarge text metrics to 200%; compact wrapping itself is implemented in production CSS, and a separate test checks it without text-scale layout selectors. These assertions cover document/key-panel overflow, full labels, visible compact focus and 44 px compact toolbar targets; they are not native browser zoom tests. Product-shell cases use explicit mock API responses, not a real backend session.

Expand Down
2 changes: 1 addition & 1 deletion docs/webui/ui-common.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The WebUI uses all 17 components that `dist/index.d.ts` exports. `grep -rn '@lab
| ErrorBanner | Adapter over common ErrorState that keeps the `ErrorBanner` name and props (`title`, `body`, `action`, `tone`, `testId`). A ref bridge removes the package's `role="alert" aria-live="polite"` from its inner block, so the section stays the only live region with `role="status"` for info and `role="alert"` otherwise. It also sets `role="none"` on the package's h2 title, so no heading lands inside sections that already own one. `action` stays a ReactNode rendered by the adapter because `primaryAction {label, onClick}` cannot express the chat anchor or icon buttons. Tones map error to danger, warning to warning and info to accent. Adapter CSS makes it the compact banner and keeps the `.ds-banner` root with its high-contrast border. |
| Card | Common BaseCard with `.surface-card` kept on the root. The adapter passes `role` (article, or region for a named focusable container) because BaseCard renders a role-less div, turns `hoverable` off for non-interactive cards, and restores the keyboard focus outline that BaseCard's `:focus-within` rule removes. Used by the gallery; StatCard builds on it. |
| PageLayout | Common PageLayout (wide variant, 1400 px cap) as the shell's content column, keeping `.app-content`. Adapter CSS drops the package's extra 0.5 rem inline padding below 768 px because `.app-content-grid` already pads the column. |
| PageHeader | Common PageHeader for Settings, the gallery and the ProductConnectionSurface states. A ref bridge restores the title and description test ids and, with `focusFallback`, puts `tabIndex=-1` and `data-dialog-focus-fallback` on the Settings h1, so a dialog closing without a usable opener lands on a named heading. It has no eyebrow slot. Adapter CSS keeps the product's bold page-title typography. The Models, Chat and Activity headings are not migrated yet. |
| PageHeader | Common PageHeader on every route (Models, Chat, Activity, Settings, the gallery and the ProductConnectionSurface states), always the first child of the shell's PageLayout. Route actions go in `actions`; the route's stale or action error goes in `error` and `errorDetail` with `onRetry`, typed so a Retry always carries a localized `retryLabel` (the package default is English, which is also why the dismiss control is not exposed). A ref bridge restores the title, description and error-block test ids (`errorTestId` lands on `.page-header__error`, re-applied on every render because the block mounts and unmounts with `error`) and always puts `tabIndex=-1` and `data-dialog-focus-fallback` on the h1, so each route has exactly one fallback and a dialog closing without a usable opener lands on a named heading. It has no eyebrow slot. Adapter CSS keeps the product's bold page-title typography, caps each action at the column width so labels wrap, and draws the error block like the product's error banner. |
| StatCard | Common StatCard for the Activity runtime tiles: a group named "label: value", with scope, observed time and reason in the hint slot. Values arrive as formatted strings. Adapter CSS undoes the package's label and value truncation, label uppercasing and hover lift. |
| LoadingStatus | Decorative common Skeleton rows under one visible, localized `role="status"` in the Models table loading slot. SkeletonRow and SkeletonText are not used because each mounts one status per row named "Loading"; SkeletonCard and SkeletonChart have no matching surface. |
| SmoothHeight | Common SmoothHeight around the Activity operations list. It is active only while an operation is in flight plus a 400 ms settle window, so a settled list is never height-pinned or clipped by its `overflow: hidden`. The settle window always runs the full 400 ms from the latest transition to false, so a false→true→false sequence within that window restarts it instead of releasing early. |
Expand Down
Loading
Loading