Repository navigation
feat(ui-web): draw each settings section's own shape while it waits - #682
Merged
Merged
Conversation
Every settings section waited behind the same dashed box with "Loading" in it. That box is a line of display text and 34px of padding, so the panel was a 90px strip that became whatever the section is the moment the snapshot landed, and a section is rarely one line: the usage page is four cards and a chart, the provider catalogue is a full-height two-column grid. The waits the pages own drew even less. The usage totals and the archive list each replaced a card's worth of rows with a single grey word inside it, and the card shrank to fit the word. Channels, schedules and the schedules' run history drew nothing at all, and an empty column beside an empty pane is what "there are no channels" looks like. So a section draws its own anatomy instead, in the section's own card, row and list classes: the skeleton is the page with its words replaced by bars, which is what makes the swap a fill-in rather than a resize. Twelve places in all: the settings panel, with a shape per section; the onboarding wizard's two step bodies; the five a page or a drawer fetches for itself (usage totals, archive list, skill body, plugin credential fields, vendor model list); and the four lists the two-pane frame and the run history hold. The bar is the one shape added, and it is added three times. A class belongs to one namespace (scripts/check-class-namespace.mjs) and the existing skel/sk pair is pinned at the two domains that already name it, so the settings sheet, the two-pane frame and the cron sheet each carry their own. All three are a translucent wash rather than a named surface, because these sit on the card, on the catalogue's rail and inside the model popover and no one named colour reads against all three; all three hold still under prefers-reduced-motion, which the new scripts/gates/wait-shimmer-css.test.mjs pins along with the shimmer. Also here: - _resetForTests clears the settings store's in-flight load. A case that mounted the dialog on a load it never resolved left refresh handing the same promise to every later caller, so one failed assertion turned the four cases after it into timeouts. - The run history says the schedules' own word (gui.cron.reading) rather than the hub's. gui.hub is said by nothing now, so its ALIAS row goes. - Both boot goldens grow the channels list's wait: that box is mounted and unentered in the settled boot tree, and hidden. Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
gloryfromca
reviewed
Sep 23, 2026
gloryfromca
left a comment
Member
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the complete one-commit diff against the refreshed github/main, the affected settings/two-pane/cron callers, and the relevant history. I also checked the repository rules and Web UI architecture vocabulary, backward compatibility of the replaced loading states, generated i18n and boot-golden changes, and that tests were added without weakening prior assertions.
Verification:
- focused loading-state suites: 122 passed
- full
ui-websuite: 2,695 passed npm run type-check: passednpm run lint: passed with four existing hook warnings and no errors- production build, both boot snapshots, and
check-page: passed - TUI generated i18n catalogue check: passed
- source-language, large-file, and diff-whitespace checks: passed
gloryfromca
approved these changes
Sep 23, 2026
LivXue
pushed a commit
that referenced
this pull request
Oct 12, 2026
…682) ## Summary Every settings section waited behind the same dashed box with "Loading" in it. That box is a line of display text and 34px of padding, so the panel was a 90px strip that became whatever the section is the moment the snapshot landed, and a section is rarely one line: the usage page is four cards and a chart, the provider catalogue is a full-height two-column grid. The waits the pages own drew even less. The usage totals and the archive list each replaced a card's worth of rows with a single grey word inside it, and the card shrank to fit the word. Channels, schedules and the schedules' run history drew nothing at all, and an empty column beside an empty pane is what "there are no channels" looks like. So a section draws its own anatomy instead, in the section's own card, row and list classes: the skeleton is the page with its words replaced by bars, which is what makes the swap a fill-in rather than a resize. Twelve places in all: - the settings panel, with a shape per section (general, usage, providers, models, skills, tools, plugins, archive, about); - the onboarding wizard's two step bodies, which wait on the same snapshot; - the five a page or a drawer fetches for itself: usage totals, archive list, skill body, plugin credential fields, vendor model list; - the four lists the two-pane frame holds (channels, schedules, memory) and the schedules' run history. Key decisions: - The usage range picker stays live through the page's own wait: the range is the reader's state, not the counter's, so only what the counter answers is drawn as bars. - The vendor list keeps the line naming the vendor being asked, with the rows under it: a shape cannot say which vendor. - The bar is the one shape added, and it is added three times. A class belongs to one namespace (`scripts/check-class-namespace.mjs`) and the existing `skel`/`sk` pair is pinned at the two domains that already name it, so the settings sheet, the two-pane frame and the cron sheet each carry their own. All three are a translucent wash rather than a named surface, because these sit on the card, on the catalogue's rail and inside the model popover and no one named colour reads against all three. - Channels and schedules draw the wait in the unentered state too: the condition is the same `!loaded && !rows.length` either way, the box is hidden until the section opens, and opening it starts the load. This is what grows both boot goldens by the channels list's wait. Also here: - `_resetForTests` clears the settings store's in-flight load. A case that mounted the dialog on a load it never resolved left `refresh` handing the same promise to every later caller, so one failed assertion turned the four cases after it into timeouts. - The run history says the schedules' own word (`gui.cron.reading`) rather than the hub's. `gui.hub` is said by nothing now, so its ALIAS row in the i18n gate goes. ## Type - [ ] Fix - [x] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [ ] Other ## Verification - `npm test --prefix ui-web` -- 194 files, 2695 tests, all pass - `make lint-ui` -- gen:check, eslint and tsc clean (4 pre-existing react-hooks/exhaustive-deps warnings, none in changed files) - `npm run lint:i18n --prefix ui-tui` -- generated catalogue up to date - `npm run --prefix ui-web build && python3 ui-web/build.py` -- both boot goldens match (264 and 263 nodes) - `node ui-web/scripts/check-page.mjs`, `check-css.mjs`, `check-class-namespace.mjs` -- all OK - Mutation check: 14 mutations applied one at a time (each wait reverted to its old word or to `null`, each shimmer and each reduced-motion rule removed, one section's shape deleted so it falls through to the default). Every one turns a case red. - Real browser: every wait photographed in light and dark through Playwright on the vite dev page, with the fixture transport's replies held open so the wait states stay on screen. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk User-visible: every settings wait changes from a word to a shape. No behaviour, no state and no wire shape moves; what a section draws once its values are in is unchanged. The three new bar classes are additive and scoped to their own sheets, and all three hold still under `prefers-reduced-motion`. Two changes reach past the waits: the settings store's test-only reset now clears its in-flight load, and the i18n gate loses the `gui.hub` ALIAS row that nothing measures any more. Rollback: revert the single commit. Nothing is persisted and no generated file other than the two boot goldens and the TUI catalogue moves. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues N/A Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
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.
Summary
Every settings section waited behind the same dashed box with "Loading" in it.
That box is a line of display text and 34px of padding, so the panel was a 90px
strip that became whatever the section is the moment the snapshot landed, and a
section is rarely one line: the usage page is four cards and a chart, the
provider catalogue is a full-height two-column grid.
The waits the pages own drew even less. The usage totals and the archive list
each replaced a card's worth of rows with a single grey word inside it, and the
card shrank to fit the word. Channels, schedules and the schedules' run history
drew nothing at all, and an empty column beside an empty pane is what "there are
no channels" looks like.
So a section draws its own anatomy instead, in the section's own card, row and
list classes: the skeleton is the page with its words replaced by bars, which is
what makes the swap a fill-in rather than a resize. Twelve places in all:
models, skills, tools, plugins, archive, about);
skill body, plugin credential fields, vendor model list;
schedules' run history.
Key decisions:
the reader's state, not the counter's, so only what the counter answers is
drawn as bars.
under it: a shape cannot say which vendor.
to one namespace (
scripts/check-class-namespace.mjs) and the existingskel/skpair is pinned at the two domains that already name it, so thesettings sheet, the two-pane frame and the cron sheet each carry their own.
All three are a translucent wash rather than a named surface, because these
sit on the card, on the catalogue's rail and inside the model popover and no
one named colour reads against all three.
condition is the same
!loaded && !rows.lengtheither way, the box is hiddenuntil the section opens, and opening it starts the load. This is what grows
both boot goldens by the channels list's wait.
Also here:
_resetForTestsclears the settings store's in-flight load. A case thatmounted the dialog on a load it never resolved left
refreshhanding the samepromise to every later caller, so one failed assertion turned the four cases
after it into timeouts.
gui.cron.reading) rather thanthe hub's.
gui.hubis said by nothing now, so its ALIAS row in the i18n gategoes.
Type
Verification
npm test --prefix ui-web-- 194 files, 2695 tests, all passmake lint-ui-- gen:check, eslint and tsc clean (4 pre-existingreact-hooks/exhaustive-deps warnings, none in changed files)
npm run lint:i18n --prefix ui-tui-- generated catalogue up to datenpm run --prefix ui-web build && python3 ui-web/build.py-- both bootgoldens match (264 and 263 nodes)
node ui-web/scripts/check-page.mjs,check-css.mjs,check-class-namespace.mjs-- all OKMutation check: 14 mutations applied one at a time (each wait reverted to its
old word or to
null, each shimmer and each reduced-motion rule removed, onesection's shape deleted so it falls through to the default). Every one turns a
case red.
Real browser: every wait photographed in light and dark through Playwright on
the vite dev page, with the fixture transport's replies held open so the wait
states stay on screen.
Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
Risk
User-visible: every settings wait changes from a word to a shape. No behaviour,
no state and no wire shape moves; what a section draws once its values are in is
unchanged. The three new bar classes are additive and scoped to their own
sheets, and all three hold still under
prefers-reduced-motion.Two changes reach past the waits: the settings store's test-only reset now
clears its in-flight load, and the i18n gate loses the
gui.hubALIAS row thatnothing measures any more.
Rollback: revert the single commit. Nothing is persisted and no generated file
other than the two boot goldens and the TUI catalogue moves.
Related Issues
N/A