* feat(ui): migrate the Create Organization form to shadcn and react-hook-form
* fix(ui): guard double submit, test escape-close reset, drop dead organizationCreateCall
* fix(ui): block org create dialog dismissal while a create is in flight
* fix(ui): render budget duration labels instead of raw values in the shadcn Select
Replaces antd and Tremor with shadcn primitives across the 18 files these three
routes exclusively own. Markup only: no behaviour, data flow or copy changed, and
no shared or form-bearing component is touched, so the blast radius stops at
these pages.
The 12 tests covering these components are unchanged from the previous commit and
still pass, which is the evidence that the rewrite preserved behaviour. Also
prunes the six antd no-restricted-imports suppressions these files no longer
need.
Markup-only migration of the 17 files these three routes exclusively own,
replacing antd and Tremor with the installed shadcn (base-vega) primitives and
lucide icons. No route behaviour changes; the tests written in the previous
commit are untouched here and pass against both the old and the new markup.
Colour now comes from tokens rather than from hardcoded utilities, so the
health-check button, the alerts and the badges no longer pin their own palette.
email_settings also loses an invalid DOM nesting (a table cell inside a div, and
a div inside a paragraph) that React had been warning about.
Two modals on the policies page moved from the Policies panel up to the panel
root. Base UI Tabs mounts only the active panel, unlike Tremor, and both are
opened from the Templates tab, so leaving them nested would have made "Use
Template" do nothing.
Retires 53 antd import suppressions from the eslint baseline.
Moves the nine files these three routes exclusively own off antd and Tremor onto the
shadcn primitives in src/components/ui. Scope came from the migration analyzer's import
closure, so nothing reached by a second route is touched and every file carrying an antd
Form is left alone until #34195 lands.
access-groups gets the page header, search box and the whole detail view; vector-stores
gets the tab shell, the store picker and the tester panel; organizations gets the
organization detail view and the three filter controls.
Two changes are behavioural rather than cosmetic. The vector-stores tab strip moves from
Tremor, which mounts every panel at once, to Base UI, which mounts only the active panel;
that is the correct behaviour and the reworked test now opens the tab it asserts on. The
antd Select on the Test Vector Store tab becomes a combobox rather than a plain select so
its showSearch type-ahead survives.
organization_view keeps one antd import, the ColumnsType used to build the extra columns
it hands to the shared MemberTable; that is dictated by the shared component's API and
goes away when MemberTable migrates. eslint-suppressions.json ratchets down accordingly:
eight files lose their no-restricted-imports entry and organization_view drops from three
to one.
Every test passes unedited across the migration, and the visual gate reports the three
migrated routes changed with the other 32 pixel-identical
Replaces antd and Tremor with the installed shadcn primitives on the three
route-exclusive panels: Tremor tabs, buttons and text on budgets; the antd
delete Modal and Tremor button on skills; the Tremor card, inputs and buttons
on ui-theme.
Markup only, no behaviour change. The characterisation tests added in the
previous commit are untouched and stay green, and the ui-theme inputs now
carry real label associations.
Shared components stay on antd; they are reached by other routes and are
migrated separately. The form-bearing files on these routes are left alone.
The dashboard already receives the requested model name as model_group on
every spend-log row, but LogEntry dropped the field, so nothing distinguished
an auto-routed request from a direct one.
Surface it precisely rather than by comparing requested against resolved:
model_group differs from model for plain aliases and wildcard deployments
too, so a bare mismatch tags almost every row and identifies nothing. The
indication is driven instead by which deployments are auto-routers, resolved
from every page of /v2/model/info and shared through context.
The request drawer header names the router in a badge next to the provider;
the session sidebar swaps the entry's leading icon. Rows that no auto-router
served render exactly as before.
* refactor(ui): extract shared tab-routing helpers
Every per-tab-routed page copy-pastes the same URL<->slug logic and the
same active-tab/redirect engine. Extract two reusable pieces:
- createTabRoutes(baseSegment, slugs) in utils/tabRoutes.ts returns
{ baseSegment, slugs, tabHref, slugFromPathname }, the trailing-slash
href builder (via migratedHref) and the pathname->slug reader.
- useTabRouting({ routes, baseTabKey, visibleKeys, ready }) derives the
active tab from the pathname, redirects an unknown/forbidden slug to
base once ready, and returns an onTabChange navigator.
visibleKeys + ready exist so a role-gated page can pass its filtered tab
set and defer the redirect until permissions resolve, rather than
bouncing a user off a still-loading valid tab. Both are pure/unit-tested.
No page consumes them yet.
* refactor(ui): migrate Models + Endpoints onto the shared tab-routing helpers
Replace the page's hand-rolled tabRoutes.ts (base segment + slug tuple +
href builder + slugFromPathname) with createTabRoutes, keeping the
existing named exports as thin re-exports so callers and tests are
unchanged. The layout drops its local activeSlug/isKnownSlug/activeKey
derivation, its redirect useEffect and its router.push onChange in favor
of useTabRouting, passing the role-filtered visibleKeys and a ready flag
(!teamsLoading && !uiSettingsLoading) so the permission-gated redirect
behavior is preserved exactly. The antd tab bar, role-gated tab set, the
refresh button and the ?model=/?team= drill-in overlay are untouched; the
file's pre-existing antd import is now recorded in the suppressions
baseline since editing it makes it a linted-as-changed file.
The existing models-and-endpoints layout.test.tsx and tabRoutes.test.ts
pass unchanged, which is the regression guarantee.
* test(ui): make the agents route's tests markup-agnostic before migration
Rewrites the two assertions that were coupled to antd's DOM and adds the
missing characterisation test for agent_cost_view, so the suite describes
behaviour rather than antd markup and can stay untouched across the shadcn
migration.
The skill selection test reached the checkbox with a querySelector on
input[type=checkbox]; antd renders an input while Base UI renders a
span[role=checkbox], so it now queries by role and accessible name, which
both libraries derive from the wrapping label.
The delete confirmation test queried role=dialog; antd Modal is a dialog
while Base UI AlertDialog is an alertdialog, so it now anchors on the
confirmation text and accepts either role.
agent_cost_view had no test at all; it gets one covering the null render,
the dollar-prefixed values, the omitted rows, and a zero cost that must not
be mistaken for unset.
All 55 tests pass against the current antd components.
* refactor(ui): migrate agents to shadcn
Replaces antd and Tremor with shadcn (base-vega) primitives across the five
files the agents route exclusively owns. Markup only; no behaviour, data
fetching or route structure changes.
Modal becomes AlertDialog, with a plain destructive Button in the footer
rather than AlertDialogAction, because that action is AlertDialog.Close and
would dismiss the dialog before the delete request settles, losing the
in-flight state. Alert, Tag, Spin, Space, Collapse, Descriptions, Typography
and the antd icons map onto alert, badge, ui-loading-spinner, flex/grid
utilities, collapsible, a definition list, semantic headings and lucide.
The shadcn CLI emits alert.tsx importing cva from class-variance-authority,
which this project does not depend on; it uses the cva object syntax from
lib/cva.config. The generated file fails to typecheck, so the adapted copy
lives in components/shared instead, per the convention that ui/ stays
CLI-managed.
Colour comes from tokens throughout, so the info callout is now the neutral
card style rather than antd's blue, and nothing hardcodes a colour in the way
of a later theme change.
The 55 tests in the route pass unchanged from the previous commit. The visual
gate re-baselined agents and all 34 other routes stayed pixel-identical.
* feat(ui): rebuild Organization Settings on react-hook-form + zod with a dirty-field PATCH
Replaces the antd Settings form in organization_view.tsx with OrgSettingsForm,
the first consumer of the shared RHF + zod form kit. The form derives a minimal
payload from RHF dirty tracking via pickDirty and sends it to the typed
PATCH /v2/organization/{organization_id}, so untouched fields are omitted,
emptied widgets clear with null ([] for lists), and the old full-send builder
with its length > 0 clear-dropping guards is deleted.
Adds src/lib/forms/useZodForm.ts so every form gets the z.input/z.output
generics and zodResolver wiring from one place, and forwardRefs ui/textarea
so RHF can register it under React 18
* fix(ui): forwardRef InputGroupTextarea to match the forwardRef'd Textarea
* test(ui): pin that an mcp server edit preserves existing org toolsets
* docs(ui): explain the useZodForm generics
* chore(ui): re-prune eslint suppressions after rebase onto staging
* refactor(ui): migrate request logs table onto the shared DataTable
Moves the Request Logs tab off the local view_logs/table.tsx clone and onto the
shared DataTable in server sort, pagination, and filter mode. The container is
split into RequestLogsPanel (data owner: the spend-logs query, the session dedup
and composition pipeline, and the detail drawer), a thin RequestLogsTable, and
RequestLogsTableColumns. The clone itself stays for now because TopModelView and
TopKeyView still consume it
The advanced filter bar moves into the shared DataTableFilterDrawer, so filters
commit on Apply and render as removable chips. That makes the per-keystroke
debounce in the query hook redundant, and the hook now takes ColumnFiltersState,
PaginationState, and SortingState directly instead of carrying its own filter
shape. Reset still restores the default 24 hour window alongside the filters
Adds shared/PaginatedSearchSelect, a Base UI combobox with server-side search and
infinite scroll, and uses it for the Key Alias and Model filters. That retires the
three logs-only antd pickers (PaginatedKeyAliasSelect, PaginatedModelSelect,
FilterTeamDropdown) and the FilterComponent molecule they plugged into. The shared
TeamDropdown is deliberately untouched: six other surfaces still render it, five of
them as a bare child of an antd Form.Item that injects value/onChange implicitly
* test(ui): pin team-scoped key alias filtering in the logs filter drawer
The Key Alias filter narrows its options to the team selected in the same
drawer, a cross-filter dependency carried over from the antd picker it
replaced. Nothing covered it: the live QA pass explicitly did not exercise
it either, so it was the one behaviour in this migration that could regress
silently
Asserts the selected team id reaches useInfiniteKeyAliases, that the lookup
stays unscoped when no team is picked, and that the scope does not leak into
the Model lookup, which shares the same combobox but takes no team
* refactor(ui): migrate models and endpoints table onto the shared DataTable
Rebuilds the All Models table on the shared DataTable, following the 2a
treatment from the Models + Endpoints design: one card holding search, the
Team and View selectors, refresh, columns and filters, with the active
filters on a chip row and the pagination footer at the bottom.
Retires the last hand-rolled tremor renderer (all_models_table.tsx) and the
antd/tremor column defs in molecules/models/columns.tsx, replacing them with
a thin AllModelsTable consumer plus AllModelsTableColumns built from the
shared cell library.
Behavior is preserved end to end. The server sort field mapping now lives
next to the column ids so the two cannot drift. Status keeps its column and
its sort, hidden by default behind the Columns menu because the design shows
nine columns. Access groups collapse into a "+N more" tooltip instead of a
per-row expand toggle, and the full reset moves into the filter drawer
footer where the design puts it.
Adds the shadcn hover-card primitive (Base UI PreviewCard in the base-vega
style) for the model information hover, which needs an interactive surface a
tooltip cannot provide.
* fix(ui): stop the models tab re-querying on mount
The mount-time effect fires the debounced search with the initial empty
value, and its callback rebuilt the pagination object unconditionally. That
produced a second render (and a second query) roughly 300ms after mount with
no user input, which on a slow CI machine swapped the table's row nodes
mid-interaction and made a click land on a detached node.
resetToFirstPage now returns the existing state when already on the first
page, so React bails out instead of re-rendering. Pinned with a test that
asserts no additional query after the debounce settles; it fails without the
fix.
* test(ui): characterise the workflow runs detail drawer before migrating it
Pins the drawer's behaviour against the current antd implementation: the
metadata fields it surfaces, the timeline ordered by sequence number, the
empty-events copy, the messages section staying collapsed until opened, the
in-drawer refresh refetching, and the close control dismissing it.
Every assertion is role/text based so the same file can stay green once the
component moves off antd, without being edited.
* refactor(ui): migrate workflow runs to shadcn
Replaces the antd Drawer, Collapse, Button, Spin, Tooltip and Empty on the
Workflow Runs page with the installed Base UI primitives (Sheet, Collapsible,
Button, UiLoadingSpinner, Tooltip) and lucide icons, and moves the page's
hardcoded hex colours, fonts and geometry onto design tokens and utility
classes so the page can be themed. The only inline styles left are the gantt
bars' computed left/width, which are runtime values.
Behaviour is unchanged: the drawer's characterisation tests were written
against the antd version in the previous commit and pass here without being
edited.
Retires the file's now-unused antd no-restricted-imports suppression.
Admits a keyless SSO user (no virtual key) at the aggregate /mcp endpoint from a gateway DCR
session bearer, resolving team/org/SCIM/budget authorization fresh on every call.
- Aggregate DCR front door: stateless /register (sealed llm_dcrc_ client ids), SSO-backed
/authorize + /authorize/complete, and /token minting identity-only session tokens with PKCE,
single-use codes/flows, and rotating refresh tokens.
- Admission: a session-shaped Authorization at the aggregate scope opens via _admit_gateway_session,
reloads the live user, and runs the centralized policy gate; failures return the RFC 9728
invalid_token challenge. Gated on the un-forgeable, server-only mcp_admitted_user_subject marker,
so virtual-key and JWT auth are unchanged.
- Authorization model: an admitted subject is resolved as one plain UserAPIKeyAuth per grant source
(its own grants, plus each team it is a live roster member of), each answered by the SAME resolver
virtual keys use, then unioned. That branch is the FIRST statement of BOTH public resolvers, so no
single-credential prelude runs for it and a fault in a lookup it never uses cannot deny its grants. A source team counts only while it is a live grantor: roster membership, not
blocked, and neither the team nor its owning org over budget (enforced through the SAME
_team_max_budget_check / _organization_max_budget_check owners common_checks uses for keys).
Each team source carries that team's own org, so the existing org
ceiling caps it; for a keyless source the org list only ever intersects (a ceiling must not become
a grant) and an unresolvable ceiling denies rather than silently uncapping, on both the server and
tool axes. _roster_team_object is the single owner of "which teams count": a team whose roster no
longer lists the user neither grants servers nor throttles, in one place.
- Rate limits: the subject is bounded by its user rpm/tpm AND by the per-server mcp_rpm_limit of
the team a call is ATTRIBUTED to — the same single source billing charges, from the same owner. A key charges its one pinned team's bucket; a keyless
subject has no team_id, so admission stamps each granting team's limit map onto the auth
(server-only field, stripped from validated input like the marker) and the limiter emits that
team's mcp_per_team descriptor. Charging every granting team instead would let one cross-team user
drain several teams' SHARED buckets on a single call and block their other members; and a server
the user's OWN grant reaches charges no team bucket at all, because no team provided it. Per-KEY
MCP limits do not apply because there is no key.
- Wrapper channels: the manager-level union treats the admitted subject by the same grant model.
The admin-role short-circuit and the absolute no_mcp_servers early-return are key-credential
rules and never apply to it (a session bearer is a third-party client credential, not the
dashboard, and the subject's opt-out silences only its own source). Operator-open channels
(allow_all_keys, the user's own BYOM submissions) are owned by one operator_open_server_ids
helper that BOTH the server union and the admitted tool resolution consult (suppress-BYOM-when-
explicitly-scoped is a key-credential rule and never applies to the subject, whose user row
carries the DB-default empty mcp_servers), so an open-channel
server is default-open for tools instead of listable but uninvokable.
- Redirect URIs: one owner, validate_redirect_uri_shape, decides redirect-URI hygiene (bad scheme,
fragment, missing host, userinfo, backslash host) and resolves allowlisted native callbacks, shared
by DCR registration and the OAuth endpoints. Registration keeps a deliberately wider trust policy
than validate_trusted_redirect_uri: public dynamic registration accepts any https client, and its
controls are mandatory S256 PKCE plus the consent screen.
- Egress leak-defense: a gateway admission credential (session bearer / bridge envelope) is scrubbed
from EVERY egress header context, anchored to the credential shape, so it can never be forwarded
upstream and replayed.
- Single-use guard: auth-code, refresh and connect-flow claims resolve the proxy's cross-worker redis
cache themselves rather than trusting the cache passed in, and fail CLOSED on a Redis fault instead
of falling back to a per-worker count that a captured id could replay through another worker.
- Sign-in return_to: one shared, never-raising helper persists a safe return_to for every sign-in
branch (SSO/Okta/generic and username/password), and every branch RESUMES through the same
_sso_return_to_redirect the SSO callback uses, so however a deployment signs in the stored value
is honored identically (same-origin path directly; control_plane_url via the one-time login-code
handoff). A stale cookie is ignored rather than failing a completed sign-in.
- Budgets, both halves: ENFORCEMENT (an already over-budget team or its owning org stops being a
grantor, in the source gate) and ACCOUNTING (a team-derived tool call is billed to the granting
team and ITS org, so that budget accumulates and the right organization is charged). A server the
user's own grant reaches bills the user; when several teams grant one server the pick is the
lowest team_id, stable and auditable. Billing rides a COPY, so authorization still sees the full
union, and it is inert when the target server cannot be resolved from the tool name.
Deferred (tracked): client-selected server scoping of the session token (LIT-4680).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the antd Drawer with ui/sheet and the antd Button, Typography and
Space usage with ui/button plus token utilities, and swaps the @ant-design
PlusOutlined icon for lucide's Plus. Toasts now go through the shared
MessageManager so the route no longer imports antd directly.
The route's tests were written against the antd components in the previous
commit and are unchanged here, so they pass on both implementations.
MemoryEditModal is left alone because it is built on antd Form; the table
already sits on the shared DataTable.
* test(ui): pin search-tools info view behavior before shadcn migration
Rewrite the markup-coupled copy-button assertions in SearchToolView to
role queries plus lucide icon-state, and add a role/text characterization
suite for SearchConnectionTest, which had none. Both are green against the
current antd/Tremor components so they can act as an unedited regression
net across the migration.
* refactor(ui): migrate search-tools info view to shadcn
Port the search-tools detail view and its two helpers off antd and Tremor
onto the installed shadcn primitives plus token utilities:
- SearchToolView (the info page reached by clicking a tool) now uses
ui/button, ui/card and a plain CSS-grid header instead of Tremor
Card/Grid/Title/Text and antd Button
- SearchToolTester swaps antd Input/Button/Spin and Tremor Card/Title for
ui/input, ui/button and UiLoadingSpinner, with no inline styles
- SearchConnectionTest swaps antd Button/Divider/Typography and the inline
keyframe spinner for ui/button, ui/separator and UiLoadingSpinner
Markup only; no behavior change. The list page (SearchTools) and its create
and edit forms stay on antd because they are Form-bearing and blocked until
the forms migration. Icons move from antd and heroicons to lucide. The
retired antd no-restricted-imports suppressions are pruned from the
baseline.
* fix(ui): drop redundant vertical padding in SearchToolTester card
The shadcn Card already applies py-6 and gap-6 to its flex children, so
the pt-6/pb-6/mb-6 added during the migration stacked on top of it and
roughly doubled the vertical whitespace. Keep only px-6 (Card has no
horizontal padding) and let the Card own the vertical rhythm, which
restores the original 24px spacing.
* style(ui): format SearchConnectionTest test file with prettier
* chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules
Wires up five error-level ESLint rules on the dashboard, grandfathering every
current offender into eslint-suppressions.json so the gate only bites new code
and ratchets down as files are fixed
- local/filename-pascal-case: new local rule requiring PascalCase .tsx names,
exempting Next.js reserved files (page, layout, route, ...) and test/spec files
(239 grandfathered)
- max-lines: 800 lines over src/**, excluding tests, src/data, and generated
schema.d.ts (20 grandfathered)
- local/no-complex-jsx-arrow: new local rule flagging inline JSX arrow handlers
with block bodies over two statements; each failure is a small extract-to-named
-handler refactor (65 grandfathered)
- prefer-const: flipped from off to error (103 grandfathered)
- no-restricted-imports: added antd to the phase-out ban alongside tremor, and
pointed both messages at shadcn/ui primitives (405 antd import sites grandfathered)
Both new local rules ship with RuleTester coverage
* fix(ui): preserve secondary extensions in filename-pascal-case suggestion
The suggestion text built the rename from only the head segment, so a
multi-dot file like my-component.utils.tsx was told to become
MyComponent.tsx instead of MyComponent.utils.tsx. Rebuild it from the
PascalCased head plus the untouched remaining segments, and add tests
covering multi-dot filenames and the hyphenated Next.js reserved names
(global-error, apple-icon, opengraph-image, twitter-image)
* test(ui): characterise transform-request panel behaviour before migration
* refactor(ui): migrate transform-request to shadcn
* fix(ui): keep transform-request panels within the fixed-height content fold
* fix(ui): let transform-request flow naturally so the shell scrolls instead of clipping
* test(ui): select the copy button by its accessible name
* test(ui): characterise the old usage page before migrating it
Role- and text-based coverage of the route as it behaves on Tremor, so the
shadcn migration has a regression net it did not get to write. Pins the
DISABLE_EXPENSIVE_DB_QUERIES branch (warning copy, the docs link and its
target, and that every expensive query is skipped), the admin vs non-admin
tab set, the cost cards, and the provider and customer tables
* refactor(ui): migrate old-usage to shadcn
Replaces Tremor with the installed shadcn primitives and the shared recharts
wrappers on the only file the route owns. Tabs, cards, tables, the key select
and the tag multi-select come from src/components/ui; the bar, area and donut
charts come from src/components/shared/charts. Tremor BarList has no shared
equivalent, so Total Spend Per Team is composed from ui/meter, which also means
the per-team totals stay numbers in state instead of pre-formatted strings; a
team total of 1,000 or more used to make the bar widths NaN.
The Database Query Limit Reached warning moves with it: same copy, same docs
link, still short-circuiting every expensive query.
Drops the file's no-restricted-imports suppression and the dead customTooltip,
getTopKeys, DataDict and UserData symbols. The characterisation test from the
previous commit is unchanged and green on both sides
* test(ui): pin prompts panel toolbar and delete behaviour before migration
* refactor(ui): migrate prompts list panel to shadcn
* fix(ui): resolve prompts environment label and hold the delete dialog while deleting
* test(ui): characterise the API reference page before the shadcn migration
Pins the behaviour the migration must preserve: the three SDK tabs and their
accessible names, the default selection, that selecting a tab surfaces that
SDK's snippet wired to the resolved base url, and the title, blurb and docs
link. Written against the current Tremor markup with role and text queries so
it carries over unedited.
* refactor(ui): migrate the API reference page to shadcn
Replaces the Tremor Grid, Text and Tab primitives on the API reference route
with the shadcn Tabs primitive and token utilities, and prunes the file's now
stale no-restricted-imports suppression. Markup only; the characterisation
test added in the previous commit passes unedited.
The wrapper keeps an explicit grid-cols-1 because Tremor's Grid defaults to
numItems=1 and emitted it; the implicit auto column that replaces it sizes to
the widest child and made the code block overflow the viewport.
* test(ui): scope the API reference snippet assertion to the selected tab panel
Asserts against the rendered tabpanel instead of searching every mounted code
block, so the check keeps proving the selected tab drives the snippet even if
the panels are ever kept mounted.
* refactor(ui): migrate Tool Policies table onto the shared DataTable
Splits the old components/ToolPolicies.tsx into a data-owning panel, a thin
DataTable consumer and a getToolPoliciesTableColumns module, all under
components/ToolPolicies/. The hand-rolled tremor table, sort dropdowns and
Prev/Next pager are replaced by the shared DataTable in client mode, so
sorting, pagination and filtering now come from TanStack rather than local
state. Search moves to the toolbar global filter and the four facets (input
policy, output policy, team, key) move into a filter drawer; the facets match
exactly instead of by substring, so filtering on "trusted" no longer also
matches "untrusted"
Inline policy editing is preserved. The two policy columns still render a
PolicySelect directly in the row, with the per-row-per-column saving state and
the in-place row update kept in the panel that owns the data
The 15s live-tail poll is removed in favour of the toolbar refresh action, which
takes the auto-refresh out of the write path of the inline edits. The green
live-tail banner goes with it. The panel now reads through React Query with
window-focus and reconnect refetching disabled, so refresh stays manual; that
also removes the effect that previously needed a set-state-in-effect suppression
The metric cards, the Needs Review banner and the detail swap are unchanged.
Review still scrolls to the row when it is on screen, but no longer jumps
across pages, since the paginated order now lives inside the table
Drops the unused userRole prop threaded from the route through the view into
the table, and prunes the suppressions stranded by the file move
* fix(ui): make Tool Policies inline saves safe against concurrent edits and refresh
Two races in the inline policy editing path, both found by review.
Saving state was a single tool name per column, so starting a second row's save
re-enabled the first row while its PATCH was still in flight, and whichever
save finished first cleared the indicator for whichever row was in the slot.
Track the set of tool names currently saving per column instead, so each cell
disables and re-enables on its own request
A list fetch already in flight when a save landed would resolve afterwards and
overwrite the row with its pre-save snapshot, silently reverting a policy the
user had just changed and the server had already accepted. Cancel in-flight
queries before writing the row, which is the documented React Query ordering
for this; the stale response is then discarded and the refresh can be retried
Tightens the test helpers that hid the second bug: policy values are now
compared exactly rather than with toHaveTextContent, which substring-matches
and so let "untrusted" satisfy an assertion for "trusted"
Both tables consume the shared DataTable's controlled row-selection API, so they
move together.
Users runs fully server-side (sorting, pagination, filtering) with the page,
sort and filter state lifted to ViewUserDashboard, which now also owns the
detail-view swap that used to live inside the table component. Sort controls are
restricted to the five keys the backend accepts so a header click can no longer
send an invalid sort_by. The hand-rolled checkbox column, select-all and
selectedUsers[] are replaced by controlled rowSelection keyed by user id, and the
per-row icon strip becomes an overflow menu.
Model health checks keep client-side sorting, including the custom status and
timestamp orderings, while pagination moves to the shared footer driven by the
grandparent's page state. Selection is cleared whenever the page changes, since
the rows underneath it are swapped out.
ModelDataTable had no consumers left once HealthCheckComponent stopped using it,
so it is removed along with dead local state it carried.
* fix(ui): bundle provider logos as static imports and unify fallback in Logo component
providerLogoMap values are now content-hashed bundle URLs emitted by
static imports instead of /ui/assets/logos/ path strings, so any
deployment that serves the app JS also serves the logos: dev server,
proxy /ui mount, server_root_path sub-paths, and the split-chart nginx
image where the old route 404d in production. A missing file is now a
build error instead of a silent runtime 404.
resolveLogoSrc passes /_next/ URLs through untouched so bundled values
never get double-prefixed with the server root path. The new Logo
molecule owns resolution and the letter-avatar fallback and warns with
the failing URL on load error; ProviderLogo delegates to it. The three
bare img sites in the agents wizard render through Logo, fixing their
broken-image bug.
Dashscope now uses qwen.png, RunwayML the on-disk runway.png, and the
GradientAI entry is removed (no plausible asset exists). soniox.svg and
ai21.svg drop a single mismatched intrinsic dimension attribute that
Turbopack's import-time image parser rejects. Dead logoSrc lookup in
AddModelForm deleted. Vitest resolves image imports to Next's
StaticImageData shape via a config plugin so tests exercise the same
/_next/ URLs as production.
* fix(ui): retry logo load when src changes after an error
Track which src errored instead of a boolean so a Logo instance whose
source changes in place (agents modal title) attempts the new URL
rather than staying on the letter-avatar until remount.
* refactor(ui): migrate inline provider logo lookups to the shared Logo component
Patterns B, C, and E from the logo consolidation: every inline
providerLogoMap lookup feeding a bare img with a hand-rolled DOM
fallback now renders through Logo (credential modal, vector store
create/info views, cost tracking margin and discount forms and tables).
getProviderDisplayInfo, handleImageError, and ProviderDisplayInfo are
deleted; getProviderLogoAndName is a strict superset of the exact-match
helper. The vector store logo map no longer duplicates provider logo
paths: shared entries reference providerLogoMap and the three
vector-store-only logos become static imports. The map itself stays
because milvus and s3_vectors have no Providers enum equivalent.
Sites that rendered nothing for an unmapped provider now render the
letter avatar. Representative tests per pattern assert the rendered img
src against providerLogoMap so a wrong provider-to-enum mapping fails,
plus letter-avatar fallbacks for unmapped providers.
* fix(ui): resolve vector store slugs through the vector store logo map
The vector store info provider badge fed backend slugs like pg_vector,
milvus, and s3_vectors to getProviderLogoAndName, which only knows LLM
providers, so those stores showed a letter avatar and a raw slug. The
pre-existing inline lookup had the same wrong-domain bug via
provider_map. Reinstate getVectorStoreProviderLogoAndName resolving
through vectorStoreProviderMap first with a fallback to the LLM
resolver, so vector-store-only providers get their own logo and display
name for the first time.
* fix(ui): distinguish response cache from provider prompt caching
The log detail drawer labeled LiteLLM's response cache result as
"Cache Hit" and rendered a red "false" tag next to provider prompt
cache token counts, which read as prompt caching being broken. The
row is now labeled "Response Cache" with an explanatory tooltip,
shows a neutral "Miss" tag instead of a red one, and the prompt
cache token rows are prefixed with "Prompt Cache" and get their own
tooltips. Cost breakdown line items get the same prefix.
The Caching dashboard only reports response cache analytics but never
said so; it is renamed to "Response Cache" in the sidebar, gains a
scope description pointing to the Usage page and Logs for prompt
caching, and the ambiguous "Cached Tokens" stat card is renamed to
"Cached Completion Tokens".
* test(ui): cover renamed Response Cache sidebar item in e2e
The sidebar navigation spec now clicks the renamed "Response Cache"
item and asserts it routes to /ui/caching, and the menu label fixture
maps the new label while keeping "Caching" as a legacy alias.
Verified by running sidebar.spec.ts through run_e2e.sh (full harness:
built UI served by the proxy, seeded postgres); both tests pass.
* feat(ui): link cache tooltips and dashboard description to docs
The Response Cache tooltip links to the proxy caching docs and the
two prompt cache token tooltips link to the prompt caching docs, so
users can jump straight to the explanation of whichever mechanism
they are looking at. The Response Cache dashboard description links
both docs pages the same way.
* refactor(ui): migrate agents table onto the shared DataTable
Replace the hand-rolled tremor table inside AgentsPanel with the shared
DataTable, splitting the surface into a data-owning panel, a thin
AgentsTable consumer, and a getAgentsTableColumns definition composed
from the shared cell library.
Row delete moves from an inline icon button into the per-row overflow
menu, and the health-check toggle moves into the table toolbar since it
controls which rows the server returns. The loading skeleton is now
initial-load-only, so refetches keep the current rows on screen.
Drops the last @tremor/react import from AgentsPanel, so its
grandfathered eslint suppressions are pruned from the baseline.
* fix(ui): keep agents ordering and token changes correct in the migrated table
Sorting by created_at went through a raw accessor, and TanStack places
undefined ahead of real values, so an agent with no created_at jumped to
the top of the newest-first list. The pre-migration sort coerced a
missing date to epoch 0 and sorted it last; restore that by sorting on a
derived timestamp.
Reload the list when the access token changes rather than leaving the
previous token's rows on screen: show the skeleton for the new token,
drop the rows if that load fails, and ignore a superseded response so a
slow earlier request cannot overwrite newer rows. Refetches triggered by
delete or the health-check toggle still keep their rows.
Tests also reset the networking mocks between cases so an unconsumed
mockResolvedValueOnce queue cannot leak into the next test.
The organizations admin table was a hand-rolled tremor/antd table in a single
snake_case file. This moves it onto the shared DataTable and cell library the other
migrated tables use, splitting it into a data-owning OrganizationsPanel, a thin
OrganizationsTable consumer, and a getOrganizationsTableColumns module
The models column no longer uses a per-row accordion whose expand state lived in the
parent; it renders the shared ModelsCell with truncation and a "+N more" tooltip,
matching every other table with a models column. Row actions (Edit, Delete) move into
a per-row overflow menu gated to proxy admins, while the detail view, create modal,
and delete modal stay in the panel. The server-side org id / org alias search stays
wired to the useOrganizations hook, and the table gains an initial-load skeleton plus
a search-aware empty state. The dead sort_by / sort_order filter fields, the misnamed
"Info" column that only ever showed a member count, and an unused refresh affordance
are dropped; the default created_at descending sort is preserved
Move the admin dashboard Memory table off the hand-rolled antd
<Table> onto the shared DataTable and cell library, matching the
pattern already used by Teams, Virtual Keys, and Guardrails.
MemoryView keeps the data (server useQuery, mutations) and owns the
detail drawer, edit modal, and delete modal; it now renders a thin
MemoryTable consumer plus a getMemoryTableColumns columns file. The
server pagination moves the full PaginationState up to the parent so
the shared footer's rows-per-page selector works, the key-prefix
search runs through the shared toolbar and resets the page on change,
and per-row view/edit/delete collapse into a single overflow menu.
Sorting stays off since the backend returns updated_at DESC.
The old page-reset effect is gone (the page now resets inside the
search handler), so its react-hooks/set-state-in-effect suppression
is pruned. The detail drawer moves into its own MemoryDetailDrawer
component to keep the parent under the complexity budget.
Move the Credentials panel off its hand-rolled tremor table onto the shared
DataTable and cell library, matching the SimpleTable design and the sibling
Vector Stores / Guardrails tables. Split the panel into a modal-owning parent
(CredentialsPanel), a thin client-mode DataTable consumer (CredentialsTable),
and a CredentialsTableColumns factory.
Credential Name and Provider render as shared cells (IdentityCell + provider
logo via getProviderLogoAndName); the per-row edit/delete icons become a right
-aligned overflow menu (Edit, Copy credential name, Delete). Admin-viewer read
parity is preserved: viewers still see the list but get no actions column.
Detail/edit and delete modals stay in the parent, so the public prop stays
uploadProps only.
Drops the now-unused tremor import (pruning its eslint suppression) and the
dead antd Form handle.
* refactor(ui): consolidate Add/Edit credential modals into one CredentialModal
AddCredentialModal and EditCredentialModal were ~90% identical: the same
provider select, ProviderSpecificFields, and submit/filter logic, differing
only in title, button text, edit-mode prefill, and the disabled credential
name. Replace both with a single CredentialModal driven by a mode: 'add' |
'edit' prop, and point the two call sites in credentials.tsx at it.
Removes ~120 lines of duplication and drops the no-explicit-any and
no-restricted-imports baselines. The two per-file tests merge into one
CredentialModal.test.tsx covering both modes (add: editable empty name;
edit: prefilled, disabled name; provider fields render).
* refactor(ui): derive credential name disabled state from mode, not data
The disabled flag on the credential name field was tied to whether
existingCredential?.credential_name is truthy, an artifact of the old
EditCredentialModal. Drive it from the isEdit flag like the rest of the
component so mode='add' with a stray existingCredential can't disable the
field and mode='edit' with an empty name can't leave it editable. Behavior
is unchanged for real call sites; adds a regression test for the edit-with-
empty-name case.
* refactor(ui): prefill credential form declaratively instead of via useEffect
The edit-mode form was seeded with an imperative form.setFieldsValue inside
a useEffect that also set React state (setSelectedProvider), an antd anti-
pattern carried over from the old EditCredentialModal. Both call sites mount
the modal fresh with existingCredential already present (conditional && plus
destroyOnHidden), so there is no 'prop arrives after mount' case to handle.
Replace it with antd's declarative initialValues on the Form and a lazy
useState initializer for the provider. Removes the effect, its
react-hooks/set-state-in-effect suppression and exhaustive-deps warning, and
one any cast; behavior is unchanged (edit now shows the real provider on
first paint instead of flashing the default). Existing tests cover prefill
and the disabled name field.
The value cell was a ternary chain over field_type; adding Select made it a fourth
level and tripped no-nested-ternary. Early returns read better than a deeper chain
and let the suppression baseline ratchet down.
* refactor(ui): migrate AI Hub, public hub, and MCP Toolsets tables onto shared DataTable
* test(ui): stub skillHubPublicCall in the public model hub networking mock
Migrate Projects, Project Keys, Vector-store Documents, Logging
Callbacks, and Pass-through Endpoints onto the shared DataTable and
cell library, each split into a thin container plus a columns file.
Row actions move into the unified overflow menu, empty states get the
rich icon+title+body treatment, and parents that never tracked loading
gain an initial-load-only isLoading flag so tables render skeletons
instead of flashing the empty state.
Project Keys switches from a hand-rolled antd Pagination header to
server pagination through the shared footer, and its section is
rewired into ProjectDetailsPage, which had regressed to a hardcoded
"No keys to display" empty card while the section sat orphaned.
pass_through_settings.tsx moves to PascalCase under
components/PassThroughSettings/ and drops its dead modelData prop and
tremor imports; its stale eslint bulk-suppressions are pruned.