feat(webui): show loaded models and compose routes on the app shell - #1927
Merged
Merged
Conversation
The WebUI wrote the raw preference into data-theme, so the default `system` matched neither the light nor the dark selector, and the --token-* contract was configured by hand on :root, where no theme file could take effect. data-theme now always holds a <family>-<scheme> id (mlxcel-light, mlxcel-dark, glass-light, glass-dark) resolved by theme.ts, with the stored family and scheme kept apart and the pre-change flat `theme` field migrated. main.tsx imports one theme entry after ui-common base.css; the baseline values moved into mlxcel theme blocks unchanged, and a glass family reads the existing intensity, transparency, contrast and motion switches. A classic script in index.html applies the id before first paint, since the CSP forbids inline scripts. check-theme-selectors fails on any theme selector no shipped id matches and runs in the webui-bundle job. The glass tokens derive from lablup/backend.ai-go, recorded in NOTICE. The .material-glass rule now writes -webkit-backdrop-filter first: the minifier kept only the prefixed form, so the chrome blur never rendered in Chromium or Firefox. models.css read two tokens no theme defines. Validated with typecheck, lint, the new gate, unit (32 node, 401 Vitest), the Chromium browser suite (59), the three-engine smoke, and build_bundle.py --verify. Refs #1903
The glass theme binds a highlighted gradient fill to --token-buttonPrimaryBg and --token-buttonDangerBg, but the rendered primary and danger buttons never showed it. components.css hard-coded #245bd8 and #c6352b on .ds-button-primary and .ds-button-danger with the same specificity as ui-common's .button--primary and later in the bundle, so both glass schemes painted the mlxcel fill at rest and switched to the glass hue only on hover. The two rules now read the theme tokens. The mlxcel tokens resolve to the same two colors, so the baseline does not move. theme.spec.ts now checks the computed fill of the rendered buttons rather than only the tokens: the mlxcel baseline stays flat, both glass schemes paint the gradient over their own solid, and every opaque case paints no gradient. Put the two hex values back and the new glass case fails, because it receives background-image none. Typecheck, lint, the theme-selector gate, unit (32 node:test, 401 Vitest), the Chromium suite (60) and build_bundle.py --verify pass. The committed bundle is regenerated. Refs #1903
The classic-script checks in build_bundle.py and theme-bootstrap.test.ts used `\b(type="module"|async|defer)\b`, whose trailing word boundary after a quote never matches, so a `type="module"` bootstrap passed, and the ordering check then found the bootstrap's own module attribute. Both now reject any type, async or defer attribute and locate the app module by its src. Feeding build_bundle.py a module, deferred, late, missing or inline variant of index.html rejects each. The bootstrap parity cases now cover every family and scheme theme.ts ships, so a family added there but not to theme-bootstrap.js fails the unit gate (checked by adding one). The glass danger button gets its own red-tinted shadow instead of the primary blue, and glass.css cites the upstream files by URL. Refs #1903
From the security review. The gate compared a resolved argv path with import.meta.url, which Node resolves through symlinks, so running it through a symlinked path exited 0 without checking anything; it now compares real paths. The import and attribute-selector patterns backtracked cubically and quadratically on long whitespace runs and are now linear, attribute names match case-insensitively as they do in HTML, and .jsx, .cjs, .mts and .cts files are scanned. build_bundle.py and the unit test also reject a nomodule or duplicated bootstrap tag. Each new spec case fails against the old code. Glass no longer gives ui-common secondary buttons a backdrop blur. Those buttons sit on transcript, table and form surfaces, which the epic keeps stable, and in WebKit each one became its own blur layer repainted during streaming. They keep the tinted translucent fill. Refs #1903
data-glass-intensity has one discrete CSS rule per integer 0-100 in glass-intensity.css, so a value outside that range or shape (negative, over 100, a fraction, a non-finite number, a non-numeric string) previously reached loadAppearance and applyAppearance untested; an unclamped write there would silently drop the glass blur and saturation with no visible error. Adds two preferences.test.ts cases covering clampIntensity's boundary behavior through both entry points, and updates the Vitest count in docs/webui/design-system.md's Verification section from 425 to 427. Regenerates src/webui/assets/mlxcel-webui-manifest.json (source_digest_sha256 only; asset bytes are unchanged) since the test file lives under webui/src. Validation: pnpm --dir webui run typecheck/lint/check:theme-selectors/unit (35 node:test, 427 Vitest across 40 files), pnpm exec playwright test against the full theme/browser/activity/chat/models/settings suite (60 passed), python3 scripts/webui/build_bundle.py --verify, check_webui_contract.py --self-test, check_binary_assets.py and check_cross_repo_refs.py.
Brings in #1902's ui-common component adoption. components.css merged cleanly with both sides intact: the .material-glass rule keeps -webkit-backdrop-filter ahead of backdrop-filter, .ds-button-primary and .ds-button-danger keep reading --token-buttonPrimaryBg and --token-buttonDangerBg, and #1902's :where(:not(:disabled)) hover rules are unchanged. app.tsx takes #1902's PageHeader Settings screen with this branch's Theme and Color scheme selects and the system-scheme listener. The design-system and ui-common docs keep both texts, with the test counts recomputed. The bundle was regenerated, not hand-merged. theme.spec.ts (including the glass button fill case) and ui-common-components.spec.ts (including both button hover cases) pass in the full Chromium suite (88), with unit (35 node:test, 470 Vitest), the theme-selector gate and build_bundle.py --verify. Refs #1903
…eme-system # Conflicts: # src/webui/assets/assets/code-highlight-BZ-vBiaX.js # src/webui/assets/assets/code-highlight-CzWARrNf.js # src/webui/assets/assets/code-highlight-DVOlMPN7.js # src/webui/assets/assets/history-BTvgVv_A.js # src/webui/assets/assets/history-Bh2B8HiI.js # src/webui/assets/assets/history-DTnpX_xH.js # src/webui/assets/assets/index-Bfja5tJs.js # src/webui/assets/assets/index-Bm7r1mqL.js # src/webui/assets/assets/index-Cw4PvI3b.js # src/webui/assets/index.html # src/webui/assets/mlxcel-webui-manifest.json
The toolbar printed the browser's selected model, so a fresh tab said "No model selected" while the server held a loaded model. It now shows the server's loaded models as up to three chips with a lifecycle badge plus a "+n" chip that opens the command palette; a chip selects the model and opens its inspector, never loads. The sidebar footer keeps mode, status and version and moves the instance id and event sequence into a collapsed disclosure. Every route renders one PageHeader with its actions and its stale or action error in the header. The adapter always makes the h1 the dialog focus fallback, and the hand-rolled heading markup, its CSS and the eyebrow strings are gone. globalShortcuts in shell.tsx feeds the help dialog. Cmd/Ctrl+N opens Chat with a one-shot new-conversation request; the composer also handles Cmd/Ctrl+Enter and Cmd/Ctrl+N outside IME composition. The palette moves to command-palette.tsx, drops gallery and finds catalog models by name or id. The layout holds at 195 CSS px (a 390 px window at 200 percent zoom): body loses min-width, single-column tracks use minmax(0, 1fr), and a narrow toolbar block stacks its rows. tests/shell.spec.ts pins 195 and 320 px in both locales. Typecheck, lint, unit (512 Vitest, 35 node:test), Playwright Chromium per spec, bundle verify, binary-asset and contract checks pass. Refs #1914
On a desktop toolbar short of room the loaded-model chips named nothing. At the 1024 px reference viewport with three long-named models plus "+1", each chip was about 100 px wide; the badge kept its full width and the name collapsed to 0 to 10 px, while the badge label itself was clipped to "Read". Flex shrinking also split the room by natural width, so the shortest name was starved first. The loaded region is now a grid whose chip columns start at zero and grow in equal shares up to their natural width, and each chip gives up space in stages: the name truncates down to 5rem, then the badge narrows to a pill holding only its state dot, then the name truncates further. The clipped label stays in the accessible name and the full name in the chip title. Compact widths keep the wrapping flex row, and roomy toolbars render as before. A shell.spec.ts case pins it at 1024 px: every chip keeps at least 40 px of name, the region stays clear of the toolbar actions, and the accessible name still carries the state. It fails on the previous CSS (names measured 0, 10 and 7 px). typecheck, lint, unit, check:theme-selectors, the bundle, binary-asset and contract checks pass, as do the eleven Chromium specs this PR runs, one per command. Refs #1914
Member
Author
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
Four follow-ups from the PR #1927 review. The toolbar said "No model loaded" while signed out and until the first catalog page arrived, which asserts a server state the UI has not read. `AppShell.loadedModels` is now nullable: App passes null unless an authenticated session holds a catalog snapshot (signed out, before the first page, after a server-restart reset), and the region then shows "Loaded models unknown" (`toolbar.loaded.unknown`, en and ko). Opening the already selected model from a chip or the palette dispatched `selectModel` again, which cancels observation, refetches and clears the runtime history. `modelSelectionFor` in provider-surfaces computes the target, and App skips the dispatch when it equals the current selection. Below 960 px the chip row wrapped, so the sticky toolbar grew a row per long-named chip: with four models it was 269 px tall at 400 px wide and 162 px at 700 px. The row now scrolls sideways inside itself with chips capped at min(14rem, 80%), which holds the toolbar at its one-chip height (124 px and 82 px). Chromium does not scroll an element that is already partly visible when it takes focus, so the region scrolls a keyboard-focused chip fully into view. `help.body` now says that Esc still closes an open dialog. Validation: typecheck, lint and theme selectors; 35 node:test and 517 Vitest tests; Playwright shell (14), browser (29), ui-common-shell (12), ui-common-components (14), models, chat, activity, settings and theme specs; bundle build and verify; binary-asset and contract checks. Each new test fails with its fix reverted.
Member
Author
Security and performance reviewNo CRITICAL or HIGH findings, so no commits were pushed. Reviewed at MEDIUM
LOW
Checked, no finding
Gates: typecheck, lint, check:theme-selectors and unit (528 Vitest, 35 node:test) pass. The bundle verify and binary-asset checks pass. In Playwright, shell (14), browser (29) and chat (2) pass. |
Security/performance follow-up for PR #1927 (issue #1914). The composer's new-conversation shortcut matched metaKey or ctrlKey on every platform, so macOS Ctrl+N, the native Cocoa/Emacs "move down a line" binding inside a textarea, cleared the draft and attached images instead of moving the caret. Add webui/src/design-system/keyboard.ts with isApplePlatform() and isPrimaryModifier(), and gate the composer's new-conversation shortcut on the platform's own modifier (Cmd on Apple, Ctrl elsewhere), leaving Ctrl+N untouched on Apple platforms. The Enter/Cmd/Ctrl+Enter send path is left unconditional since it has no platform-specific native binding to protect. Also stop a held key from flooding conversations: both the global shell shortcut and the composer now ignore event.repeat for the new-conversation combo (the global handler still calls preventDefault on the repeat so the browser does not act on it), and the command palette clears its query on close as well as on open, so a search typed before closing no longer keeps matching in the background and holding stale model buttons in the closed dialog. Regenerated src/webui/assets/ from the updated source. Updated docs/webui/chat.md, which claimed Ctrl+N worked in the composer the same way the global shortcut does, and refreshed the Vitest count in docs/webui/design-system.md.
CI runs the browser suite with DejaVu Sans, which is wider than the macOS system font, and three shell.spec.ts cases failed there although they passed locally. At 195 CSS px the English "Authentication" heading set the login form's min-content 32 px past the viewport, and the drawer title "Primary navigation" pushed the close button out of the 163 px drawer, where overflow: hidden clipped it. In a tight 1024 px toolbar the chip names shrank to 37 px, below the test's 40 px. The login heading and paragraph, and the drawer title, now wrap anywhere, and the drawer close button no longer shrinks. Chips use 8 px leading padding and a 4 px gap between name and badge, which gives each name 8 px more: 45 px with DejaVu and 54 px with the macOS font. Reproduced and verified in an Ubuntu 24.04 container with the CI font packages (fonts-dejavu-core/extra 2.37-8, fonts-wqy-zenhei 0.9.45-8) and Playwright Chromium: shell 14, browser 29, ui-common-shell 12, ui-common-components 14, models 8, chat 2, activity 3, settings 3, theme 23 passed. The same specs pass on macOS. The bundle is regenerated and verified. Refs #1914
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #1910. Closes #1914.
The toolbar showed the browser's selected model, so a fresh tab said "No model selected" while a model was Ready on the server. The shell now shows what the server holds. The #1915 to #1918 redesigns build on it.
What changes
{mode} · {status} · v{version}. The instance id and event sequence move into a collapsed disclosure.PageHeaderper route: Models, Chat and Activity move their actions and their stale or action errors into the shared header. The eyebrow keys,gallery.issueand the now-unusedmodel.selected.noneare deleted.globalShortcutsfeeds the help dialog (six entries).galleryis no longer listed.Decisions
PageHeaderh1, as refactor(webui): adopt every shared @lablup/ui-common component #1902 shipped, not on thePageLayoutwrapper as the issue planned.modal-focus.test.tsxis unchanged. A newmain.test.tsxcase checks that focus lands on that h1 when a dialog closes after its trigger is gone.body { min-width: 320px }overflowed every route there by 125 px.minmax(0, 1fr).max-width: 319pxtoolbar block.tests/shell.spec.tspins 0 px page overflow at 195 and 320 px, in en and ko, on every route.Validation
check:theme-selectors, bundle verify, binary-asset and contract checks pass.browser:allin the container: passes. One WebKit case was flaky under parallel load and passed 12 of 12 when repeated alone.Live acceptance (bundled binary)
I ran Playwright Chromium against a server built from
b9465f91(test-fast, metal and accelerate, embedded bundle), withqwen3-0.6b-4bitReady and 224 catalog entries. Each check used a fresh browser context.qwen3-0.6b-4bitchip and a Ready badge, and no model is selected. The footer readsrouter_pool · ready · v0.7.0with its details collapsed.#modelsand the inspector shows the model. No non-GET request was sent.#chatwith an empty "New conversation". The palette's New conversation also works. Stock browser windows keep Cmd+N for themselves, so the palette entry covers that case.compositionstart, withisComposing: true, or with keyCode 229. After composition, Cmd+Enter sent one short turn. On a macOS user agent, Ctrl+N in the composer kept the draft and moved the caret. On the Windows user agent, it started a conversation.?lists the shortcutsscrollWidthequalsinnerWidthon Models, Chat, Activity and Settings, and the drawer closes with Escape back to the menu button. At 195 CSS px (emulating 200 percent zoom), overflow is 0 on all four routes and the drawer's close button stays inside the panel.Note on check 4: my 16-token request cap did not match the
?autoload=falsechat URL. The single turn used the UI defaults, and the server's runtime counters report 1 completed request with 4 completion tokens.