fix(webui): open the model inspector from anywhere in a library row - #1926
Merged
Merged
Conversation
The Models library opened its inspector, and with it Load, Use in Chat, Unload and Delete, only from the name button in the first cell. Clicking anywhere else in the row did nothing, so users concluded unload was missing. The shared DataTable adapter gains an opt-in activateRowPrimary seam. A click in a body row focuses and clicks the row's primary control (marked with ROW_PRIMARY_CLASS), so pointer and keyboard run the same onClick. Clicks on other interactive elements, clicks that end a text-selection drag, and rows without an enabled primary control (including the loading and empty rows) are ignored. Rows keep their native row role and gain no tab stop; adapter CSS adds the pointer cursor, a low-specificity hover fill and a whole-row outline while the primary control has :focus-visible. The Models name button carries the marker, and selecting still never loads. ui-common alpha.19 onRowClick and isRowClickable are withheld from the adapter type: they render each tr as role="button", which fails axe aria-required-children and nested-interactive on the Models page and makes the router-real harness locator match two elements. New Playwright row-activation tests fail on the previous source and pass now, alongside adapter and screen unit tests, typecheck, lint, the unit suite, the models and ui-common browser specs, and a regenerated, verified bundle. Closes #1901 Part of #1910
Member
Author
Implementation Review SummaryIntent
Findings Addressed
Remaining Items
Verification
|
Member
Author
Security and Performance ReviewNo CRITICAL or HIGH findings; no code changes were made. Checked
Remaining items
|
Review follow-ups to PR #1926 (issue #1901, epic #1910). Re-selecting the already-open row now short-circuits before calling actions.selectModel, since selectModel unconditionally aborts the live /events stream, forces a full snapshot refetch and clears runtimeHistory even when the id is unchanged; a stray click on an already-selected row no longer pays that cost. screen.test.tsx and load-profile-integration.test.tsx are updated for the new no-op semantics, and the row-activation describe block gains a case that proves a same-row click and its name button both stay silent while a different row still selects exactly once. common-adapters.test.tsx gains two DataTable whole-row activation tests that were previously unexercised: a focusable ancestor outside the row (a tabIndex=0 scroll region, as #1918 may add) still lets a plain-cell click through, and a click that bubbles from a React portal rendered outside the delegation wrapper is ignored. Both were confirmed to fail against a temporarily weakened guard and pass again once restored. common-adapters.tsx renames the module-level handler activateRowPrimary to delegateRowClick and drops the prop-destructuring alias, and corrects the ROW_INTERACTIVE comment: it mirrors the common table's nested-interactive guard except that an anchor only counts as interactive with an href.
The DataTable<T> adoption-matrix row described whole-row activation but not the follow-up in 2c2efd6: the Models name button's onClick skips actions.selectModel when its entry is already selected, so re-activating the selected row (by the row or the name button) no longer restarts the live event stream and snapshot refetch. Validation: pnpm --dir webui run typecheck, pnpm --dir webui run lint, pnpm --dir webui run unit (373 passed) and pnpm exec playwright test --config playwright-report/local-4901.config.ts tests/models.spec.ts (7 passed) all pass; docs-only change needs no bundle rebuild. Refs #1901
Brings in the theme system from #1923. The only conflicts were in the generated bundle under src/webui/assets/, which was regenerated from the merged source with scripts/webui/build_bundle.py rather than hand-merged.
With #1923 the selected Models row now paints its --color-selection fill, and the accent Badge on it failed WCAG AA color contrast: ui-common alpha.19 draws --token-colorPrimary text on a fixed translucent blue fill, which measured 4.49:1 on the selected row in mlxcel-light and 3.73 to 4.49:1 in all four themes under high contrast, where the selection fill is 30 percent focus color. Row activation makes a selected row routine, and the new row-activation axe test caught it after the merge. The Badge adapter CSS now draws the accent tone as body text inside a --color-focus outline, which clears 4.5:1 on every surface it sits on. A new models.spec.ts test selects a row, hovers another, and runs the axe color-contrast rule on the table in mlxcel and glass, light and dark, with high contrast off and on. Without the CSS change it fails in five of those eight variants (mlxcel-light, and every theme with high contrast on).
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.
In the Models library a click anywhere in a row, not only on the name button, now opens the inspector for that entry, so Load, Use in Chat, Unload and Delete are reachable without discovering the name button. Selecting still only calls
selectModeland never loads; clicking the row that is already selected does nothing, becauseselectModelalways aborts the live event stream and forces a full snapshot even for the same id.The behavior is an opt-in seam in the shared
DataTableadapter,activateRowPrimary, for #1918 to build on. A body-row click focuses and clicks the row's primary control (the element withROW_PRIMARY_CLASS, here the "Inspect {name}" button), so pointer and keyboard run the same handler. Clicks on another interactive element in the row, clicks that end a text-selection drag, and rows without an enabled primary control (including the loading and empty rows) are ignored. Rows keep their nativerowrole and gain no tab stop; while the name button has:focus-visiblethe whole row is outlined.Why not ui-common
onRowClickThe issue suggested alpha.19's
onRowClick/isRowClickable. Measured on the Models page, they render every body<tr>asrole="button"withtabIndex=0, which fails axearia-required-children(critical: the rowgroup holds buttons, not rows) andnested-interactive(serious: the name button sits inside the row button), doubles the tab stops per row, and names the rowInspect alpha cache 4bit chat, ..., so therouter-real.harness.tslocatornew RegExp('Inspect .*' + name)matches two elements and its.click()throws. The adapter's props type omits both props so they cannot come back by accident;docs/webui/ui-common.mdrecords this.Changes during review
--token-colorPrimarytext on a fixed translucent blue fill. The Badge adapter now draws the accent tone as body text inside a--color-focusoutline. A new test checks a selected and a hovered row for contrast in all four themes with high contrast off and on; without the CSS change it fails in five of the eight variants.Test-first evidence
The row-activation tests against the unmodified source (origin/main 31c0459):
Reverting only
screen.tsxon the final branch fails all five row-activation tests (the four above plus the contrast test) while the three name-button tests still pass. Every adapter guard was removed in turn and each removal fails a unit test; removing only the selection guard fails the drag test, which ends with a control click on the same point so it cannot pass by missing the row.Validation
typecheck,lint,unit(51 Vitest files, 484 tests, plus 35 node tests including the i18n drift and theme-selector checks),check:theme-selectors.build_bundle.py --verify,check_webui_contract.py,check_binary_assets.pyandcheck_cross_repo_refs.pypass.browser:all(multi-engine) is left to CI.Real-binary acceptance
Run at head e7f3db0 against the bundled server (
--webui --models-dir models/mlx --no-models-autoload --settings --props --metrics --slots, 224 catalog entries), Chromium at 1440x900, with the library filtered toqwen3-0.6b-4bit(two rows: thatmodels_direntry and themlx-community/Qwen3-0.6B-4bitcache entry). All pass:qwen3-0.6b-4bitopens the inspector for that entry; the row getsmodels-selected, its selection fill and the "Selected" badge; no POST is sent. Clicking the other row switches the inspector to it.:focus-visible); the row shows a 2px solid focus-colored outline and has noroleortabindex. Space opens that row; Tab moves to the next row (outlined) and Enter opens it.loadingthenready), Use in Chat (enabled, navigates to#chat), then back on Models, Unload through the confirmation dialog (unloadingthenunloaded).GET /ui-api/v1/catalog?q=qwen3-0.6b-4bitthen reportsstate: unloaded,worker_exit_observed: true. Exactly two POSTs were sent, both to/ui-api/v1/model-actions. The model is left unloaded.Real-router harness
Run by the epic #1910 orchestrator on this head (
e7f3db03):cargo test --profile test-fast --features metal,accelerate --lib server::router_server::router_webui_playwright_harness_tests::real_router_browser_harness -- --ignored --exact --nocapture(themake verify-webui-integration-faketarget) passed. Its Playwright log shows the spec ran rather than skipped:tests/router-real.harness.ts"secured Rust router drives download, load, chat, Stop/drain, unload, remove and negative security cases" passed in 5.9 s, including the unchangedInspect {name}locator atrouter-real.harness.ts:321.Closes #1901
Part of #1910