Skip to content

UI rework polish - #2666

Merged
Charlesthebird merged 8 commits into
kagent-dev:mainfrom
toreysoloio:ui-rework-polish
Sep 4, 2026
Merged

Charlesthebird merged 8 commits into
kagent-dev:mainfrom
toreysoloio:ui-rework-polish

Conversation

@toreysoloio

Copy link
Copy Markdown
Contributor
  • Substrate tables now have a unified header design and sort behavior - previously the actor/worker columns had a hand-rolled header to avoid antd default sorting (I assume); instead changed it to use antd's sorting controls while using the custom actor/worker sorter logic
  • Added * markers on required form fields on AgentTemplateForm and ModelForm that were missing it
  • MCP form stops inventing a server name when switching to "command" tab - previously switching would result in the name being set to "tool-server"/"remote-server"

substrate tables being more uniform:
image

agent template /model form inputs being marked required:
image

image

toreysoloio and others added 3 commits September 2, 2026 13:17
Two behaviours the page offered twice, both visible on the same screen.

Row hover is opt-in through `clickable-table-row`, and the rule that suppresses
it everywhere else was written as `tr:hover > td`. The actors and workers tables
are antd `virtual` tables, whose body is divs — no `tr`, no `td` — so those two
went on lighting up under the pointer while the pools and templates beside them,
and every other static table in the app, had stopped. The rule now matches the
cell class antd actually paints the hover from, which exists in both bodies; the
pointer and pressed rules for clickable rows are rewritten the same way so the
mirror bug cannot appear. Selected rows keep their own hover.

The actor and worker columns carried a header of the page's own: a button around
the title, a bare arrow beside it, nothing outside those few words to click. It
avoided antd's `sorter` because a comparator reorders the page in hand, and one
page out of 410,110 reordered is not the cluster sorted. The concern was right
and the remedy left a reader two headers to learn. `sorter: true` is antd's
header with no comparator behind it, so the whole cell is the target, the
chevrons show the direction, and the table still reorders nothing — the order
goes out with the next read.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Torey Scheer <[email protected]>
The agent-template form carried no required marker on any field while
refusing to save without a name and a model configuration, and the model
form's API key was required to create with nothing on screen saying so.

Both marks are conditional, because both fields are. The template form
renders as text on the details page, where an asterisk would be asking a
reader for something the template already has; the model form's API key is
needed to create and not to edit, which its label already says in words and
now says in the marker too.

Every other authoring surface was checked against its own validation rather
than for the presence of marks. The harness, MCP server and prompt forms
already agreed — including the MCP namespace, which is genuinely optional
there and rightly unmarked.

A new spec walks all six surfaces and asserts what is marked *and* what is
not: a test that only checked the marks would pass just as well on a form
that marked every field. It pins the two conditional cases too. `routes` in
the Playwright helper gains the two new-resource addresses it was missing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Torey Scheer <[email protected]>
`suggestName` fell back to `remote-server` or `tool-server` whenever it had
no host or package to derive a name from, and the new-server page re-applies
the suggestion on every field change while the name is untouched. So opening
the wizard and switching to Command wrote `tool-server` into the empty Name
field — as did typing a namespace or flipping TLS, with `remote-server`. What
had been a placeholder became a value the reader appeared to have chosen.

The suggestion is now empty when there is nothing behind it, which is what
the field's placeholder is for. Typing a URL or a package still fills the
name in, and switching kind now clears a suggestion the other branch cannot
support rather than swapping in a made-up one.

Two unit tests cover it: the derivation both kinds already had, and the empty
case that regressed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Torey Scheer <[email protected]>

@Charlesthebird Charlesthebird left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed db0b844. All three fixes work.

One blocking concern. Although sorter: true is the right choice here (a comparator would only reorder the current page), the comments written around it credit a server round-trip that never happens. operations.ts sorts the whole inventory in the browser.

Nothing else blocks.

Checked

  • ✅ Reverting the hover selector to > tr…:hover > td makes the new spec fail in both browsers (rgb(221, 215, 231) vs rgba(0, 0, 0, 0)).
  • ✅ The new <th> header carries tabindex="0", aria-description="sortable", aria-label, and aria-sort once sorted, and Enter activates it.

🤖 written by Claude

Comment thread ui/src/pages/SubstratePage.tsx Resolved
Comment thread ui/src/theme/GlobalStyles.tsx Resolved
Comment thread ui/src/pages/SubstratePage.tsx Resolved
Comment thread ui/src/pages/SubstratePage.tsx Resolved
Comment thread ui/src/pages/SubstratePage.tsx Resolved
Comment thread ui/src/theme/GlobalStyles.tsx Resolved

@Charlesthebird Charlesthebird left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this locally as well - I think the red required star on the form fields could say "Required" and be a bit larger.

And the "Remote URL"/"Command" tab buttons on the form are a bit too low contrast.

I have some other design notes/ideas for the UI but won't put them all in this review. Thanks for adding this!

…e rest

Eight review comments on kagent-dev#2666, the first of which is that the substrate sort
was never server-side. `GetSubstrateStatus` takes a namespace and nothing
else, and `localPage` orders the whole inventory in the browser before
slicing out a page — so three places asserted a sort that does not happen,
one of them text a reader sees: "Sorted by the server: actor, ascending".

`sorter: true` stays, because it is right for the opposite reason to the one
written down. The read has already ordered every row, so a comparator would
re-sort the hundred on screen. The strip beside each table now claims what is
true and worth claiming — the order holds across the whole inventory rather
than over the page in front of the reader — and `ServerOrder` is
`AppliedOrder`, since what it reports is what was applied and not who applied
it.

Carrying the order in the read key costs a full re-read of the inventory per
header click, to reorder rows the browser was just holding. That is now
written where the key is built instead of being left to be discovered.
Removing it means moving the sort and the slice out of the transport so a
reorder can be a re-render, which changes what the operation returns; it
belongs in its own change rather than in a header fix.

Each column's key is now typed as its sort field, so a key that is not one
fails to compile — the shape that let `pod` stand where `workerPod` belonged.
Actor `status` and worker `pool` resolve to the same ordering as `default`
because they *are* it; named rather than left to fall through, so a change to
the default has to decide what those headers mean.

Four smaller ones. The template form's name was marked required through a
condition that could not be false — it renders only while creating, and the
creating caller is never read-only — so it is unconditional and the
conditional rationale moves to the model configuration, which is the field it
describes. The hover rule's selected-row exception could never match: a row
is selectable only where it is also clickable, so this rule has already
passed it by. The expand-icon rule still keyed off `tr.clickable-table-row`,
the stale selector the rules above it stopped using. And the no-hover test
asserted a colour after a fixed wait, which passes just as well when the
pointer never arrived — it now asserts antd marked the cell hovered first, so
a missed hover fails instead of proving nothing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Torey Scheer <[email protected]>
@toreysoloio

Copy link
Copy Markdown
Contributor Author

I tested this locally as well - I think the red required star on the form fields could say "Required" and be a bit larger.

I think just the red asterisk is fine personally, since it lets us keep it before the label instead of having to move it after it (plus that much red would be distracting)

@Charlesthebird
Charlesthebird enabled auto-merge (squash) September 4, 2026 14:51
The Remote URL / Command toggle was reported as low contrast in dark mode, and
its text was not the problem: the labels measure 12.6:1 selected and 8.2:1
unselected. The control was. Its track measured 1.00:1 against the page — the
same colour — and its selected pill 1.12:1 against the track, behind an
elevation shadow antd paints at one percent white. Both halves were perfectly
legible and nothing said which one was chosen, or that the thing was a toggle.

The light theme is left alone, because its pill is white with a dark shadow
under it and that reads.

Fixed with an edge rather than a fill, the way the inputs are. A fill cannot
carry it at this end of the range: the page is near-black, so 3:1 on luminance
alone takes a mid-grey pill that reads as disabled. A one-pixel `borderStrong`
ring measures 3.5:1 against the track and 3.9:1 against the page, and is the
boundary every other control in the app is already outlined with, which leaves
the two fills with only the pill's shape to carry. Both toggles on the MCP form
are the only segmented controls in the app, and the fix is a theme token, so
neither page changes.

The spec gains a block of its own for non-text contrast, which is a separate
threshold that a check aimed at text walks straight past: 3:1 for the edge that
identifies the state, with the labels measured beside it so that edge cannot be
won by dimming them. Verified in both directions — without the theme change it
reports 1.01:1 and fails.

Its shadow colours are composited over what they sit on before being measured.
Read as opaque, a one percent white shadow measures as the most visible edge on
the page instead of the invisible one it is, which is the mistake this spec's own
note already records against page colours.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Torey Scheer <[email protected]>
auto-merge was automatically disabled September 4, 2026 15:07

Head branch was pushed to by a user without write access

@Charlesthebird
Charlesthebird merged commit b8d53d3 into kagent-dev:main Sep 4, 2026
27 checks passed
@toreysoloio
toreysoloio deleted the ui-rework-polish branch September 4, 2026 21:31
toreysoloio added a commit to toreysoloio/kagent that referenced this pull request Sep 4, 2026
Six upstream commits, two of them in the code this branch rewrites.

The four conflicts were all in the substrate UI and all artefacts of the squash
merge: `UI rework polish (kagent-dev#2666)` landed upstream as one commit while this branch
carries it as several, so git could not match the histories. Main's copy of all
four files is byte-identical to this branch's own starting point, so taking this
side loses nothing — checked by content rather than argued from history.

Two upstream changes needed real work rather than a resolution:

`fix: wire runtime revisions into substrate inventory (kagent-dev#2703)` replaced the
functional-options constructor with a direct `NewService(kubeClient,
observedNamespaces, authorizer, ateClient, revisions)`. Upstream's own tests came
across converted; the eleven construction sites added on this branch did not, and
`go build` does not compile test files, so the tree looked clean while the package
would not vet. They now use the new signature.

`refactor: standardize Go logging on slog (kagent-dev#2705)` moved the repository off
controller-runtime's logger, and `service.go` came out of the merge converted
while `substrate.go` — added on this branch, so untouched upstream — was still
calling `ctrllog`. It now uses `logging.FromContext(ctx).ErrorContext` with the
static lowercase messages and `error` field the new lint rules ask for.

kagent-dev#2703's wiring survives the refactor: the harness lookup it restored is still made,
now through `actorTemplateHarnesses`, which is what lets a database failure be told
apart from an ate-api one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Signed-off-by: Torey Scheer <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants