fix(webui): print the checkpoint's real name as its display name - #1920
Conversation
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
Also checked: no client code rewrites names; the preset rule matches |
|
Security and performance review of f6a1db7. One fix pushed in 3082e1d. Fixed (HIGH, merge-blocking): Checked, no finding:
Left (pre-existing, out of scope): operator-supplied names are not bidi-isolated, so U+202E can reorder the rest of the delete sentence (the id fence still holds). One inference id over 256 code points still rejects the whole catalog page.
|
The catalog built identity.display_name by taking the last path segment and replacing every - and _ with a space, so qwen3-0.6b-4bit printed as "qwen3 0.6b 4bit", a hyphen directory and an underscore directory collapsed to one label, and cache entries lost their owner/. No surface showed the name a user passes to -m or the API.
display_name is now the inference id verbatim. Cache and preset names are kept whole (a preset's inference id is its section name, and an overlay-only preset re-sources a cache owner/name); models-dir and single-model names keep their last / segment. A trailing / now falls back to the whole name: the old unwrap_or never fired because rsplit yields Some(""), so the old helper returned an empty string.
ModelIdentity.display_name.maxLength widens from 128 to 256 to match inference_id, since a cache owner/name can reach 193 characters. schema_version stays webui.ui-api.v1. The delete dialog keeps identity.id as the confirmation token; its body and field label now say to type the model ID shown in the dialog rather than the model name, and the body paragraph wraps. Fixtures and tests that encoded space-separated names use hyphenated ids.
New tests: a catalog contract test that fails on the old helper with every row rewritten, a policy test for the hyphen and underscore spellings, a schema bound test at 193 and 257 characters, and a delete-dialog wording test.
Refs #1912
Output of python3 scripts/webui/build_bundle.py after the display-name, delete-dialog wording and test changes; build_bundle.py --verify passes on the result. Refs #1912
The capacity recovery dialog lists every loaded model by display_name in plain list items with no wrap rule. While names were humanized, the spaces gave the browser break points; a verbatim name such as Meta_Llama_3.1_8B_Instruct_abliterated_4bit_mixed_quant_v2 has none, so at a 390px viewport the modal scrolled horizontally (dialog scrollWidth 523 against clientWidth 356 in Chromium). Give each list item the same models-wrap treatment as the dialog body and the id, so the dialog wraps a long id as #1912 requires. With the class the same measurement is 356 against 356. The capacity recovery unit test now asserts the listed names and the wrap class; it fails with the class removed. The committed bundle is rebuilt with build_bundle.py and verified. Refs #1912
The api.yaml description and docs/webui/catalog.md said display_name is the inference id verbatim, but single-model mode keeps the last /-separated segment of the served id, as #1912 decided, so an --alias such as Qwen/Qwen3-8B displays as Qwen3-8B. Spell out the rule per source (cache and preset whole, models_dir the directory name, single_model the last segment) in api.yaml, catalog.md and catalog.ko.md so the contract says what the server does. Drop "불투명" from the Korean delete-confirmation label: Settings already uses that word for the visual Opaque material, and "위에 표시된 모델 ID (mdl_로 시작)" says what to type without it. The committed bundle is rebuilt with build_bundle.py and verified. Refs #1912
The 1000-entry catalog performance test searched for `0999`, which only matched the old zero-padded label `Catalog performance model 0999`. The verbatim display-name change renamed the fixture to `perf-model-${index}`, so the query matched no row and the test timed out waiting for one filtered row on chromium, firefox and webkit-headed-performance in CI, on every retry. The only 1000-entry search-latency budget had stopped measuring anything.
Search for `perf-model-999`, which matches exactly one of the 1000 entries by its real name. Locally on chromium all four performance.spec.ts tests pass, with search-to-paint at 14.4 ms against the 100 ms budget; the same test fails at f6a1db7 with the original timeout.
Refs #1912
3082e1d to
371c20f
Compare
…verbatim-display-name # Conflicts: # src/webui/assets/assets/code-highlight-CS7S6vIv.js # src/webui/assets/assets/code-highlight-CzWARrNf.js # src/webui/assets/assets/code-highlight-DYgjCuFT.js # src/webui/assets/assets/history-B1QtkSeo.js # src/webui/assets/assets/history-BhdDGTqa.js # src/webui/assets/assets/history-DTnpX_xH.js # src/webui/assets/assets/index-CWwuIjIj.js # src/webui/assets/index.html # src/webui/assets/mlxcel-webui-manifest.json
…1923) Closes #1903. Part of #1910. ## What changes - **Theme ids.** `data-theme` always holds `<family>-<scheme>`: `mlxcel-light`, `mlxcel-dark` (the approved baseline, still the default), `glass-light`, `glass-dark`. `webui/src/design-system/theme.ts` resolves it; the stored `themeFamily` and `colorScheme` (`system`/`light`/`dark`) land in `data-theme-family` and `data-color-scheme`. The old flat `theme` field is migrated, and `system` follows host changes live. Settings gets Theme (Standard, Glass) and Color scheme selects. - **ui-common mechanism.** `main.tsx` imports one theme entry (`themes/index.css`) after `base.css`, and no ui-common theme file. The color, shadow and button `--token-*` values moved from `:root` into `[data-theme]` blocks unchanged; `common-tokens.css` keeps the theme-independent half. - **Glass theme.** Translucent chrome with a light reflection, tinted buttons, highlighted primary and danger fills, near-opaque content surfaces, no per-button blur. It reads the existing intensity, material, transparency, backdrop support, contrast and motion switches plus the `prefers-*` queries; any transparency or contrast preference makes it opaque at any intensity. Values derive from lablup/backend.ai-go (derivation line, `NOTICE`); the tokens stay here because they bind to this product's semantic token layer. - **No flash.** `webui/public/theme-bootstrap.js` is a classic render-blocking script in `<head>`, external because the CSP is `script-src 'self'`. `build_bundle.py` rejects a bundle where it is missing, duplicated, deferred in any way, or after the app module, and any inline script. - **Gate.** `check:theme-selectors` (in the `webui-bundle` job and `make verify-webui-frontend`) evaluates every `data-theme`/`data-theme-family` selector against the shipped ids and rejects `data-color-scheme` selectors, `prefers-color-scheme` outside the resolver, stray theme imports and ids with no theme block; exemptions need a written reason. Against `main` it reports `tokens.css:47`, `:48`, `:50`, `:51`, `:75`. ## Found along the way - Chrome blur never rendered in Chromium or Firefox (Lightning CSS drops a standard `backdrop-filter` that precedes its `-webkit-` twin), and the primary and danger fills were hard-coded over ui-common's rules. Both are one-line fixes in `components.css`, and both survive the merge with #1902's rewrite of that file, alongside its `:where(:not(:disabled))` hover rules. - `features/chat/style.test.ts` passed vacuously (Vitest blanks CSS imports); it now reads from disk. `models.css` read two undefined tokens; inspector labels are now muted and the selected row highlighted. - Review fixed rendered glass fills and a bootstrap check that let `type="module"` through; the security pass fixed the gate skipping its check through a symlink and two super-linear patterns. ## Validation Rebased onto `fd33ff15` (#1919, #1921, #1922) with an unchanged source diff, then merged with main at `5616f86b` (#1924, #1902's component adoption); `app.tsx` keeps #1902's PageHeader Settings screen with the theme selects, and the bundle was regenerated. On `cdb80fe7`: typecheck, lint, the gate, unit (35 node:test, 470 Vitest), Chromium suite (88, including the glass button fill case and #1902's button hover cases; axe on all four ids), `build_bundle.py --verify`, contract, binary-asset and cross-repo checks. The engines smoke on Chromium, Firefox and WebKit passed before the rebase. Each new check was reverted to confirm it fails. ## Bundled-binary checks Playwright Chromium against `mlxcel-server` built from `c26e3247` (before the rebase and the #1924 merge) with the embedded bundle and no model loaded. 1. **No flash: pass.** `theme-bootstrap.js` is served 200, `text/javascript`, `no-cache`, under the strict CSP, ahead of the app module. With the app module held, all nine cases (cold profile under light and dark hosts, each id stored under the opposite host, glass `system` under both hosts, legacy `{theme: dark}`) had the right `data-theme` and canvas with nothing mounted, and no CSP violation. 2. **Settings: pass.** Glass then Dark gives `glass-dark`, persists without the legacy field, is restored before mount on reload; `system` follows a host flip live. 3. **390x844: pass** in both families: 0 px overflow on all four routes; the sheet opens, navigates, closes on Escape and returns focus. 4. **200 percent zoom: not native** (headless Chromium cannot zoom; a half-width CSS viewport at deviceScaleFactor 2 stood in). 320 and 640 CSS px: 0 px overflow everywhere. 195 CSS px (390 px window): 125 px on every route from `body { min-width: 320px }`; main's bundle measures the same, so it predates this PR. 5. **Keyboard: pass** for `mlxcel-light` and `glass-dark`: 18 Tab stops in DOM order, each with a visible indicator; Escape restores focus from the command palette (button and Cmd+K), a Select popup and the sheet. 6. **Contrast: pass.** axe WCAG 2.2 AA on all four routes for all four ids at intensity 100: 0 violations normally, under `prefers-contrast: more` and with High contrast on; the chrome drops to `blur(0px)` with no gradient. 7. **Served-CSP spec: pass** (both variants). ## Merge with main `origin/main` 31c0459 (#1920, verbatim display names) was merged into this branch as 83fc058 by the epic #1910 orchestrator. Only the generated bundle conflicted; it was regenerated with `scripts/webui/build_bundle.py` (`--verify` digest 986294f2) and not hand-merged. On the merged tree: `typecheck`, `lint`, `check:theme-selectors` (152 files, 4 theme ids), `unit` (Vitest 473 in 51 files) and the Chromium Playwright suite on a private port (88 passed) all pass. The live-server script above was also re-run against a server built from cdb80fe (after #1924 was merged in): 23 of 25 cases pass, and the two failures are the pre-existing 195 CSS px overflow from `body { min-width: 320px }` described above.
Part of epic #1910.
Summary
identity.display_nameis now the model's inference id with no character rewriting. The catalog no longer turns-and_into spaces. Cache and preset entries keep their full name (mlx-community/Qwen3-4B-4bit), models-dir entries keep the directory name, and single-model entries keep the last/segment of the served id, as the issue decided.qwen3-0.6b-4bitandqwen3_0.6b_4bitare two distinct rows again. Every surface that printsdisplay_name(library row, inspector heading, toolbar pill, Chat and Activity selectors, operation titles, settings selectors, dialogs) now shows the name passed to-mand the API. The Rust change is thedisplay_namehelper insrc/server/webui/catalog_metadata.rsand its two call sites.ModelIdentity.display_name.maxLengthindocs/webui/api.yamlgoes from 128 to 256 to matchinference_id, because a cacheowner/namecan be 193 characters. The field's new description gives the rule for each source.schema_versionstayswebui.ui-api.v1, and the generatedui-api.d.tsis unchanged.identity.idas its token, and the fence test is byte-identical to main. The body now says to type the model ID shown below, not the model name. The field label reads "Opaque model ID shown above (starts with mdl_)" in English and "위에 표시된 모델 ID (mdl_로 시작)" in Korean. Thetests/fixtures/webui/strings.jsoncopies of both strings, which had drifted, now matchstrings.ts.models-wrap, like the id already did. Without it, a verbatim underscore name has no break point: at 390px the capacity dialog measured scrollWidth 523 against clientWidth 356.identity-collision.json, which still collides oninference_id, and the performance fixtures and specs. Themodels.long_namegallery string is now a long hyphenated id, the same in en and ko.docs/webui/catalog.mdanddocs/webui/catalog.ko.mddescribe the new meaning. The committed bundle is regenerated.Premises checked
inventory()and the serverqfilter both matchinference_id, which already heldqwen3-0.6b-4bit. What changes is that the matched row now prints that name./to fall back to the whole name through the existingunwrap_or(name), but it never did."x/".rsplit('/').next()isSome(""), so the old helper returned an empty string. The new helper skips the empty segment, and the test covers that case.config.model_alias = name), and an overlay-only preset takes over a cacheowner/name, which the last-segment rule would shorten.--aliasor the checkpoint directory'sfile_name(), never a path, so the rule only changes an alias that contains/(for example--alias Qwen/Qwen3-8Bdisplays asQwen3-8B). The docs and the api.yaml description say so. Showing the alias whole would be a one-line change if that is preferred.Changes during review
performance.spec.tssearched the 1000-entry catalog for'0999', which only matched the old padded label, so theWebUI bundleengine smoke timed out on every engine. It now searches forperf-model-999. The review found no injection or path exposure: names render as React text or escaped attributes, anddisplay_nameis always equal toinference_idor a substring of it. The delete fence still requires the exact id and revision.Revert-and-fail
With the
catalog_metadata.rschange alone reverse-applied, the new contract test fails:With the patch re-applied, all 34 catalog tests pass. The check was run twice, once by the implementer and once independently. The frontend check was done the same way. Reverting
api.yaml,strings.tsanddialogs.tsxmakes the new 193-character schema-bound test and the delete-dialog wording test fail. Removing the capacity-list wrap class makes the capacity-recovery test fail.Test plan
This branch is rebased onto origin/main at fd33ff1, which includes #1921 (catalog backend column) and #1922 (i18n catalog consolidation). The committed bundle was rebuilt by
build_bundle.pyat each rebased commit rather than merged by hand. One rebase conflict needed both sides: the performance spec's Chat option now readsperf-model-0 · Ready, combining this PR's name with #1922's localized state label. No test or fixture added by #1922 uses a space-separated model name. Results on the rebased head:cargo test --profile test-fast --features metal,accelerate -p mlxcel --lib server::webui: 78 passed, 0 failed, includingdisplay_name_is_the_inference_id_verbatimcargo clippy -p mlxcel --lib --tests --features metal,accelerate -- -D warningsandcargo fmt --all -- --check: cleanpnpm --dir webui run typecheckand eslint: cleanpnpm --dir webui run unit: node 18/18, vitest 340/340 in 39 filesengines.spec.tsandperformance.spec.ts), chromium: 7/7. All runs used a private port with--strictPort, never the preview server shared with other worktreescheck_webui_contract.py(49 fixtures),build_bundle.py --verify,check_binary_assets.py,check_cross_repo_refs.py: passBefore the rebase, the same gates also passed
server::router_server(63 passed, 1 ignored) and the engines config in firefox (7/7).Real-binary acceptance
The orchestrator built
mlxcel-serverfrom 3082e1d, the pre-rebase head carrying the same change (--profile test-fast --features metal,accelerate), and ran it with--webui --models-dir models/mlx --no-models-autoloadagainst the real checkpoint store and the default managed cache. I drove the bundled WebUI in Chromium at 1440px with a read-only Playwright script that aborts any load, unload, download, delete or refresh request. It recorded no mutating request.qwen3-0.6b-4bitprints asqwen3-0.6b-4bitin the library row, the inspector heading and the toolbar pill (qwen3-0.6b-4bit · Unloaded).stablelm-2-1_6b-chat-4bitprints unchanged in the same three places.mlx-community/Qwen3-0.6B-4bitprints as the fullowner/namein the row, the inspector heading and the pill. It also appears asmlx-community/Qwen3-0.6B-4bit · unloadedin the Chat "Model for next turn" picker and asmlx-community/Qwen3-0.6B-4bitin the Activity selector (225 options).qwen3-0.6b-4bitreturns theqwen3-0.6b-4bitrow, plusmlx-community/Qwen3-0.6B-4bit, which contains the same text in a case-insensitive match.qwen3_0.6b_4bitreturns no rows ("No matching local models").mdl_id is printed below it, the field label reads "Opaque model ID shown above (starts with mdl_)", and Confirm stays disabled while the field is empty. The long cache name wraps inside the dialog.Merge with main and CI
origin/main5616f86 (refactor(webui): adopt every shared @lablup/ui-common component #1924, ui-common components) was merged into this branch as 7e40c01 by the epic epic: make the bundled WebUI usable, on the shared ui-common foundation #1910 orchestrator. Only the generated bundle conflicted; it was regenerated withscripts/webui/build_bundle.py(--verifydigest 711cbf2a) and not hand-merged. On the merged tree:typecheck,eslint,unit(Vitest 362 in 47 files), and Chromium Playwright on a private port (models,ui-common-shell,ui-common-components,browser,chat,activity,settings: 56 passed) all pass. The merged Rust tree was checked earlier:server::webui78 passed,models::registry10 passed.WebUI installed artifactjob's Activity performance gate reportedinvestigateon four consecutive attempts at the pre-merge heads while every other step passed:two-visiblemedian decode degradation -0.35 (baseline CV 5.63), +0.20 (CV 5.45), +2.74 (CV 3.38), andone-visible+3.38 (CV 4.22). The gate flags a median above 2 percent or a baseline CV above 5 percent over five pairs. On the same runner,mainafter refactor(webui): key screen copy in the catalog and drop window.confirm #1922 failed the same gate (two-visible-6.76, CV 7.45) and passed on rerun, and fix(server): record streamed chat completions in Metrics #1919 failed it once (+3.19) and passed on its next head. This change only alters the catalog label string and a few UI strings, which do not touch decode or the Activity polling path, so these are read as runner noise. Per the maintainer's decision for this epic, the PR merges with this evidence recorded rather than with the gate relaxed; stabilizing the gate is tracked in ci(webui): the Activity performance gate fails on runner noise #1925.Closes #1912