Addresses review nits on the sidebar shell. The Meter primitive now owns its bar color through a tone variant, so the usage card passes a tone instead of a bg-* class, and the near-limit tone goes back to 80% to match the previous usage indicator. The enterprise usage card is now explicitly gated on an active license, so it never renders for an unlicensed proxy rather than relying on seat/team data being null. The collapsed-rail control and the sidebar menu button render through the shadcn and Base UI button primitives instead of raw button elements; the menu button gains render/nativeButton passthrough while its group toggles stay native buttons, so the nav link leaves keep their link semantics. Adds a regression test that the card stays hidden without a license even when seat limits exist
* refactor(ui): colocate the skills view, keeping the AIHub-shared skill components
Group-1 split for the skills (claude_code_plugins) segment. It is a file-plus-
folder combo where part of the folder is shared with AIHub and the public model
hub: the skill types, skill_detail, MakeSkillPublicForm, and helpers stay in
@/components/claude_code_plugins (imported by AIHub's ModelHubTable and
SkillHubDashboard, plus public_model_hub, networking, and skill_hub_table_columns),
while the plugins-management view moves to skills/_components.
claude_code_plugins.tsx becomes skills/_components/index.tsx; add_plugin_form and
plugin_table move alongside it. The moved files' imports of the retained shared
files become @/components/claude_code_plugins paths, other escaping imports are
absolutized, and lint suppressions are re-keyed for moved files only (the staying
components keep their src/components keys). The shared components did not move, so
their consumers are untouched. No behavior change.
* refactor(ui): name the agents/guardrails view files instead of index.tsx
The moved view files were renamed to _components/index.tsx, which reads like a
barrel of re-exports rather than the single view component each actually is.
Rename them to match their default export (AgentsPanel.tsx, GuardrailsPanel.tsx)
and their colocated tests, and point page.tsx at the named file.
login and internalUserIdentity used an unscoped "Virtual Keys" text check that now also matches the top-bar breadcrumb, and adminSettings navigated via antd menuitem roles the shadcn nav no longer emits. Scope these to the sidebar's complementary landmark and drive the Settings group by role (expand the button, then click the child link), matching the fixes already applied to the migration and navigation specs
The dashboard lint-budgets step ran check-lint-budgets.mjs in --check mode,
which fails and tells you to run `npm run lint:metrics` and re-stage by hand.
Add a --write mode that rewrites eslint-metrics.json from the same eslint
report, and have pre-commit use it, then flag drift via git diff so you
re-stage; this mirrors how the block below regenerates schema.d.ts. CI keeps
using --check, so it still fails on a stale committed metrics file.
The redesigned sidebar + header shell is now scoped to the ai-gateway dashboard. Agent-control-plane (plugin) mode falls back to the original full-width Navbar, which carries the account menu, so preferences and sign-out stay reachable there; Chat and the public model hub already use that Navbar
Update the e2e specs the antd Menu -> shadcn nav swap broke. Sidebar items are now links/buttons rather than antd menuitems, and the top bar renders a breadcrumb whose current-page item is also a "Virtual Keys" link, so the migration and navigation smoke checks now scope their sidebar lookups to the complementary landmark and expand the new collapsible groups by data-slot. Also harden the collapsed-rail unit test to assert the icon and link rather than the CSS-hidden label
knip was producing garbage locally and was never wired into CI, so nobody
trusted it. Two structural problems: it silently degrades when deps are
missing (a partial worktree install flagged all 436 test files as unused),
and its config had blind spots that surfaced as false positives.
Fixes so a knip run means something:
- Register every playwright config (serverRootPath + migration variants), not
just the main one. serverRootPath.config.ts is invoked via --config in
test_server_root_path.yml, which knip can't see; it was falsely flagged as
an unused file
- Treat src/components/ui/** as entry points. These are shadcn design-system
primitives, intentionally part of the palette before every one is consumed;
knip was flagging not-yet-used ones (e.g. select.tsx) as dead files and
their sub-exports as unused. Marking the directory as the design-system
surface is the correct fix, not deleting components someone is about to use
- Declare @ant-design/icons as a direct dependency. It was imported in ~198
files but only resolved via antd hoisting, so every one showed up as an
"unlisted dependency"
- Add an explicit vitest plugin block so test-file classification no longer
rides on auto-detection
- Stage severities via rules: gate the now-clean categories (files,
dependencies, unlisted, unresolved) as errors and keep exports/types/
duplicates as warnings, so CI enforces what's at zero today while the
remaining findings ratchet down in follow-ups
- Run npm run knip in the frontend-lint CI job, which installs with npm ci so
it never sees a partial tree
knip now exits 0 with the gated categories clean
* refactor(ui): colocate agents and guardrails views, keeping shared selectors and types
The last two group-1 colocation splits. Both are file-plus-folder combos: the
page view is a top-level file (agents.tsx / guardrails.tsx) sitting beside a
supporting folder of the same name, and part of that folder is shared.
agents: agents/types (imported by the agents hook) stays in @/components/agents;
agents.tsx and the rest of the agents/ folder move into agents/_components (the
view file becomes _components/index.tsx).
guardrails: GuardrailSelector (imported by the Playground, the key edit view,
and the agents add-guardrail form) and types stay in @/components/guardrails;
guardrails.tsx and the other 47 folder files (including the tool_permission,
custom_code, content_filter, and llm_judge subfolders) move into
guardrails/_components.
Moved files' imports of the retained shared files become @/components paths,
other escaping relative imports are absolutized against @/, and moved test
files have their from/vi.mock/vi.importActual paths rewritten to match. Lint
suppressions are re-keyed for moved files only (the staying selector/types keep
their src/components keys). The shared selectors did not move, so their external
consumers are untouched. No behavior change.
* fix(ui): move the orphaned agents/guardrails view tests and drop dead GuardrailItem
Greptile follow-ups on the agents/guardrails colocation:
- agents.tsx and guardrails.tsx moved to their _components/index.tsx, but their
sibling test files (src/components/agents.test.tsx, guardrails.test.tsx) were
left behind still importing ./agents and ./guardrails, which broke a full
vitest run. Move them to the matching _components/index.test.tsx and rewrite
their view import to ./index, folder mocks to local ./ siblings, and
networking to @/components/networking.
- Remove the unused GuardrailItem interface and its GuardrailDefinitionLocation
import from the guardrails view (dead code carried over from before the move;
state is typed as Guardrail[]).
Follow-up to the full-height sidebar shell. Replaces hand-rolled markup in the sidebar, top bar, and account/usage docks with shadcn primitives so the surface reuses shared components instead of one-off elements
Adds three Base-UI-native primitives that follow the existing conventions (cva + data-slot, no Radix): Meter, Avatar, and Breadcrumb. Badge gains a render prop via Base UI useRender so it can render as an anchor, which activates the variant's existing [a&] styles
Wires the redesigned surface onto them: the version tag is an outline Badge linking to the release notes, the breadcrumb uses the Breadcrumb primitive, the Docs link is a ghost Button, the account initials use Avatar, and the Enterprise usage card uses Collapsible for its open state and Meter for the seat and team gauges. Meter is the correct element for a used-of-total measurement and Base UI ships it natively, so it replaces the hand-rolled bars
Sets the sidebar background to pure white by pointing the --sidebar token at oklch(1 0 0) in light mode, which keeps SidebarUsageCard consistent since it shares the token; the dark block is left untouched
Also repairs layout.test.tsx, which the shell refactor had left red: the dashboard shell now renders DashboardHeader in place of the old navbar, so the test mocks DashboardHeader and asserts on it instead of the navbar the layout no longer mounts
Move the admin dashboard to a standard fixed-viewport shell. The sidebar is now full-height with its own scrolling nav (fixed logo header, pinned footer) and the top bar sits only over the content, so the page can no longer scroll past the end of the sidebar
The brand, version, collapse toggle, and account menu move into the sidebar; the AI Gateway/Chat switch, docs/blog/community links, notifications, and worker switcher stay in the top bar. The sidebar is rebuilt on a new shadcn ui/sidebar primitive that uses the existing design-system tokens instead of the antd Menu
This is a pure move-around of the sidebar, header, and content with no behavioral change intended. Chat keeps its own shell and navbar and is deliberately out of scope
Both automated reviewers assumed the semantic filter 400 reaches the
browser as FastAPI's flat detail dict and would render as raw JSON in
the test panel banner. The proxy converts a pre-call hook HTTPException
into a ProxyException that serializes as {"error": {"message": ...}},
which deriveErrorMessage unpacks first; pin that contract with direct
tests
Resolves LIT-4284
When the embedding model exceeded its context window, the MCP semantic
tool filter silently passed all tools through and reported N->N success
in the filter header; when the overflow happened while embedding tool
descriptions at router build time, the hook was never registered at all
and filtering was silently disabled
Semantic filtering now fails closed on context window overflows: the
request is rejected with HTTP 400 and a message that names the embedding
model and advises switching to one with a larger context window or
disabling the filter. Build time overflows are recorded on the filter so
the hook still registers and blocks MCP tool requests with the same
actionable error while leaving native-only requests untouched. The
dashboard test panel renders the backend message in an error banner
instead of a success state. OpenAI's embedding overflow message
(maximum input length is N tokens) now maps to ContextWindowExceededError
* refactor(ui): colocate the policies view, keeping PolicySelector shared
Group-1 colocation split. The policies folder lived in the shared src/components
dump but is only partly shared: PolicySelector (used by the Playground,
ComplianceUI, and key edit view) and its types stay in @/components/policies,
while the policy-management view (27 files: index, tables, forms, modals,
pipeline builder, and their tests) moves to policies/_components.
Moved files' imports of the retained shared files become @/components/policies
paths; other escaping relative imports are absolutized against the @/ alias
(including @/data/... and the repo test-utils via @/../tests/...), and moved
test files have both their `from` imports and `vi.mock` paths rewritten so the
mocks keep matching the source. Grandfathered lint suppressions for moved files
are re-keyed. PolicySelector did not move, so its external consumers are
untouched. No behavior change.
* fix(ui): keep PolicySelector.tsx eslint suppression at its original path
PolicySelector.tsx stays in src/components/policies (only the management view
moved to _components), but the suppression re-key wrongly moved its
no-nested-ternary entry to the phantom _components path, orphaning the real
file's suppression. Revert that one key. (Greptile P1 on #32720.)
Two of the group-1 colocation splits. Each of these folders lived in the shared
src/components dump but is only partly shared: the page's management view is
segment-owned, while a selector widget is reused by other features. So this
splits them rather than moving wholesale.
tag-management: TagSelector (used by playground) and its types stay in
@/components/tag_management; the management view (index, tag_info, TagTable,
CreateTagModal) moves to tag-management/_components.
vector-stores: VectorStoreSelector (used by organizations and playground) and
its types stay in @/components/vector_store_management; the rest of the
management UI moves to vector-stores/_components.
The moved files' imports of the retained shared files are rewritten to absolute
@/components paths, escaping relative imports are absolutized, and moved test
files have both their `from` imports and `vi.mock` paths rewritten to match.
Grandfathered lint suppressions for moved files are re-keyed. The external
consumers of the selectors are untouched (the selectors did not move). No
behavior change.
Renames each segment's local components/ folder to _components/ (private to the
route, matching Next's _ route-exclusion). Both folders are imported only by
their own page.tsx via the folder index (verified zero external importers
across src, tests, and e2e_tests), so each is a straight rename plus repointing
that one index import; a folder rename keeps every file at the same depth, so
all internal and relative imports are unaffected.
Grandfathered lint suppressions under the two folders (31 entries: cost-tracking
15, prompts 16) are re-keyed to the new paths with counts unchanged. No behavior
change.
Addresses three issues in the migrated Team Info virtual keys table, all
pre-existing behavior carried over from the tremor version:
- Changing the sort now resets to page 1. Previously handleSortingChange
routed through handleFilterChange with skipDebounce=true, which skipped the
pageIndex reset, so sorting while on a later page asked the server for that
page of the newly sorted results (an arbitrary slice).
- Reset Filters now restores the default sort. It previously reset the filter
fields and page but never touched the sorting state that actually drives the
query, so the sort indicator and server order persisted.
- Removes the dead Sort By / Sort Order keys from the filters object; sort is
derived solely from the sorting state, so those keys were written but never
read. Sort now lives in one place.
Adds regression tests for the page-reset-on-sort and sort-reset-on-filter-reset
behaviors (both fail if either fix is reverted).
Colocation follow-up to the App Router migration: move each page's owned
components out of the shared src/components dump and into its route segment's
_components/ folder, draining the shared bucket. Convention: a component used
by exactly one segment goes in that segment's _components/ (private, matching
Next's _ route-exclusion); a component shared by 2+ segments stays in
@/components. No new _shared/ folder.
Rename-in-place (segment already had a local components/ folder):
- api-reference (also relocates the shared CodeBlock, used by playground and
cost-tracking, to @/components/CodeBlock)
- memory, budgets, access-groups
- caching, projects, guardrails-monitor
Extract from src/components (page view lived in the shared dump):
- AdminPanel -> admin-panel, organizations -> organizations,
general_settings -> router-settings, usage -> old-usage
Each folder/view was verified to have no importer other than its own page
(cross-checked across src, tests, and e2e_tests). Relative imports inside moved
single files are rewritten to absolute @/components/*; colocated tests move with
their subject and have their vi.mock paths rewritten to match. Grandfathered
lint suppressions (tremor, react-hooks, and similar, all pre-existing) are
re-keyed to the new paths with counts unchanged. No behavior change.
Drop the duplicate local DEFAULT_PAGE_SIZE_OPTIONS in DataTable.tsx and import
the one already exported from DataTablePagination.tsx, removing the divergence
risk if the canonical list changes.
Follow-up polish on the migrated Team Info virtual keys table: widen the Key
ID column by 20px (100 -> 120), nearly double Created By (70 -> 130) so the
name and popover fit, and remove the Last Active header info icon (and its now
unused InfoCircleOutlined import).
The preview endpoint infers client_credentials when the inherited client_id, client_secret, and
token_url are all present (common once DCR or discovery filled them) and then strips the forwarded
bearer to preview as M2M, so the staged interactive token was silently unused; sending
oauth2_flow=authorization_code bypasses the inference. spec_path now rides along so OpenAPI servers
take the spec-based preview path the create form gets. clearHeldOAuthToken also empties the tool
list, mirroring the create form's clearTools, so a preview fetched with the discarded token never
lingers while the refetch is in flight
The create and edit submit paths for true_passthrough and oauth_delegate persist only the tool
configuration: the parametrized create test authorizes, disables the allowlist, and asserts nothing
is persisted before submit, then that the create payload carries allowed_tools but no credentials
and no occurrence of the token anywhere in the serialized payload, no per-user DB credential is
written, and the token is committed to sessionStorage only, keyed to the created server. The edit
save test gains the same serialized-payload assertion
For authorization_code the edit preview listed tools by server_id only, relying on the stored
per-user DB credential, so a token authorized in the edit session gave an empty preview until the
admin saved; the create form previews the identical state through the config-based preview
endpoint, which takes the token explicitly. The edit fetch now routes through that same endpoint
when a staged interactive token is held, built from the form values with the saved record as
fallback, and keeps the by-server_id listing for every other case
The identity used to pick the audience from spec_path only when
values.transport was OPENAPI, but the create form keeps transport in component
state rather than form values, so spec_path edits on OpenAPI servers never
invalidated a held token. Comparing url and spec_path independently mirrors
the backend's mcp_oauth_token_identity and fires regardless of whether
transport is present. Invalidation now also wipes only credentials; the
admin-typed endpoint fields are kept
The staged access token never reaches formValues (it is not a registered form field), so the
assertion could not fail; the DCR client pair is the leak the test actually pins, proven by the
mutation run
Review follow-ups on the stale-token invalidation. The backend identity now decrypts client_id and
client_secret before comparing: the stored values are NaCl-encrypted with a fresh nonce on every
write, so comparing ciphertext flagged every routine save as a mint-relevant change and purged
per-user tokens that were still valid. The identity also gains spec_path, the audience for OpenAPI
servers, and parses credentials stored as a JSON string
The purge now routes each (user, server) through the manager's invalidate_user_oauth_token_cache,
which becomes the single invalidation point covering both the legacy per-user token cache and the
v2 per-user OAuth token store; previously the purge evicted only the legacy cache while the revoke
path evicted only the v2 store, so each path left the other cache serving a replaced token until
its TTL. A credential row racing in between the find and the delete is now detected via the
delete_many count and logged; its cache entry expires by TTL
On the dashboard, CLEARED_ON_INVALIDATION and the staleness check move to types.tsx as the single
shared implementation for both forms. The edit form's transport handler now rechecks the identity
after its programmatic setFieldsValue calls, which antd does not report through onValuesChange, so
a token no longer survives a transport switch that clears the mint target. The create form rebuilds
formValues from the post-reset form state after an invalidation instead of publishing the pre-reset
snapshot, so the tool preview can no longer refetch with the discarded DCR client. Both transport
handlers now share the recheck, which also stops the create form from over-invalidating on an
http to sse swap that keeps the same url and therefore the same audience
An admin who ran Authorize & Fetch and then changed a field that determines which upstream OAuth
token gets minted kept using the stale token for tool preview, sessionStorage, and (on the backend)
the stored per-user credential and its cache. Grounded in RFC 8707/8693 and the MCP auth spec, a token
is bound to one tuple: resource/audience (url), OAuth mode/grant (auth_type, oauth_flow_type), the
authorization-server endpoints, and the OAuth client + scopes. A shared getOAuthAuthorizationIdentity
captures exactly those fields; transport (http/sse on the same url is the same audience) and
delegate_auth_to_upstream (a downstream-usage toggle never sent to the authorize request) are excluded.
UI: both the create and edit forms now discard the held token (React state / sessionStorage / hook,
plus the fetched token + DCR client in form.credentials) whenever the identity diverges from the one it
was authorized against, re-applying the admin's in-flight edit so it is never wiped. The check lives in
one shared helper so the two forms cannot drift.
Backend: editing an MCP server now compares the pre/post identity and, on a mint-relevant change, purges
every stored per-user OAuth credential for the server (DB row + per-user token cache) so no user
forwards a token minted for a resource/AS/client that no longer matches. Best-effort; a purge failure
never fails the update.
Second proof-of-concept consumer for the shared DataTable. Replaces the
hand-rolled tremor table in the Team Info Virtual Keys tab with DataTable in
server-sort and server-pagination mode plus column resizing; the file drops
about 150 lines. Sortable headers now use DataTableSortHeader, pagination is
a detached DataTablePagination driven by the page state, the id-cell still
opens the key drawer, and the body scrolls under a sticky header via
maxBodyHeight. Two behavior changes: the pagination control is the
standardized bar (row range plus page-size select) rather than the old
Previous/Next buttons, and a sort header cycles ascending/descending without
a third unsorted state, which also removes a latent case where clearing the
sort left the server sorted.
Updates the TeamVirtualKeysTable and TeamInfo tests to the new pagination,
adds a test that a sort-header click routes to useKeys as a server sort, and
lowers the no-large-inline-object-arg metric by one and the file's
no-nested-ternary suppression from two to one to match the leaner code.
Proof-of-concept consumer for the shared DataTable added in the previous
commit. Swaps the antd Table in the Workflow Runs page for DataTable in
client-pagination mode, keeping the existing cell renderers, row-click
drawer, and empty state. Adds a focused test that the rows render through
DataTable, a row click routes the detail fetch to the correct run, and the
empty state shows.
Phase 0 of the dashboard table-standardization effort: one composable
DataTable built on TanStack react-table and the shadcn-style primitives in
components/ui/table.tsx (Base UI, Tailwind v4), plus its behavioral test
suite. No existing tables are migrated in this change.
The component owns the TanStack instance and a shadcn shell, and exposes
composable slots (toolbar, pagination, footer) plus DataTableToolbar,
DataTablePagination, DataTableViewOptions, and DataTableSortHeader. Sorting
and pagination each use a single mode enum (none/client/server) so server
modes only surface state via callbacks and never reorder or slice locally.
columnMeta.ts defines the canonical ColumnMeta augmentation. The rendering
shell imports only components/ui/table primitives; no tremor or antd.
The edit form decided auth mode from the saved mcpServer.auth_type in fetchTools while the
authorize flow used the current form value, so a token authorized after switching the form to a
client-forwarded mode was never forwarded as the x-mcp header until the server was saved. A shared
getEffectiveAuthType (form value falling back to the saved record) is now the single decision point
for token receipt and tool loading
The save path classified the staged token with getMcpOAuthMode, which returns null for
true_passthrough and oauth_delegate, so the staged token was dropped on save instead of being
committed to sessionStorage the way the create form's submit path does. The passthrough branch now
also covers the client-forwarded modes; the token still never enters the server row
The App Router migration is complete: every page is a path route and the
legacy `?page=` switch is gone from the index. This closes it out.
The `/ui/` index (page.tsx) kept its own duplicate copy of teams state, a
teams fetch, and keys/addKey plumbing solely to feed a second `UserDashboard`
render for the `invitation_id` case. That was redundant: `ApiKeysDashboard`
already renders `UserDashboard` sourcing its own data, so the index is thinned
to just render `<ApiKeysDashboard />`. The login redirect, the legacy `?page=`
deep-link redirect for old bookmarks, and the post-login return-URL handling
stay on the index.
The invitation entry point now resolves in one place. Modern invitation links
already point at the dedicated `/onboarding` route; the dashboard layout now
redirects legacy `/ui/?invitation_id=` links there too (via `migratedHref`,
the same base-aware redirect the index uses for `?page=`), instead of
re-rendering that route's page component inline. This removes an import of one
route's `page.tsx` into another module, and lets the now-unreachable
`if (invitation_id) return <Onboarding/>` branch in the shared
`user_dashboard.tsx` be deleted along with its dead `Onboarding` import and
`searchParams` read. A layout test asserts the redirect and fails if it
regresses.
`legacyPageHref` and the sidebar's migrated-vs-legacy href fallback are left
in place; they are still live for the parent-category nav nodes (agentic,
tools, experimental, settings) that are not page routes.
eslint-metrics.json is resynced: -2 no-explicit-any from the removed `any`
casts, plus pre-existing drift the gate requires the snapshot to match.
* fix(ui): forward refs through ui primitives and fail tests on swallowed refs
Under React 18 a ref passed to a plain function component is dropped
with only a dev console warning, so Base UI render-prop triggers
composed over our shadcn-style primitives silently stop working (the
tooltip just never opens; ui/badge.tsx hit exactly this on the shared
DataTable branch). Label, Separator, Skeleton, UiLoadingSpinner and the
Table family now use React.forwardRef like Button and Input already
did, a contract test pins ref delivery for each, and setupTests turns
React's ref warning into a test failure so the next primitive that
swallows a ref fails CI instead of shipping a dead tooltip
* fix(ui): include captured ref warnings in the tripwire error
The afterEach tripwire threw a fixed message and discarded the collected
React warnings, so a failure never said which component swallowed the ref.
Append the captured warnings (component name + stack) to the thrown error.
The expanded reasoning block did not constrain its width or break long unbreakable tokens, so its inline-block bubble grew past its max width and pushed the whole page wider (#32481). Mirror the message body handling by capping the container width and breaking long words/code.
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>