Skip to content

Commit 2a25933

Browse files
CharlesthebirdclaudeEItanya
authored
feat(ui): rewrite the web interface on Vite, and give conversations a first-class API (#2569)
--- *🤖 written by Claude (start)* ### Summary Rebuilds the web interface on **Vite + React 19** — React Router, SWR, antd 6, Emotion. It is a static bundle behind nginx with no server process; settings come from `window.environmentVariables`, rewritten by the container on every start, so one image serves every deployment. The application API is reached over **gRPC-Web**: `grpcserver.WebHandler` wraps the existing `*grpc.Server`, and the HTTP server routes gRPC-Web requests to it ahead of its middleware chain. > [!NOTE] > **Reading the diff.** 616 of the 696 files are `ui/`, which is the rewrite's tree — read it as a new app, not as a diff. The other 80 are three things: > - **`helm/`** — the UI pod stops running a Next.js server and becomes nginx serving a static bundle, so `nginx.conf`, `supervisord.conf`, `ui-deployment.yaml` and the UI values change together with their tests. > - **`go/` and `proto/`** — the gRPC-Web seam (`grpcserver/grpcweb.go`, `httpserver/server.go`, `app.go`) and the five `AgentInstance` changes below, plus generated proto and sqlc output. > - **`CLAUDE.md` and `.nvmrc`** — the repo guide's UI section, and the pinned Node version. ### Testing this PR One command builds a Kind cluster and installs **this checkout** on it — controller, UI and agent runtime all built from the working tree, over the chart's published images. It ends holding two port-forwards, the UI on 8080 and the controller on 8083, so the last thing it prints is a working URL and `cd ui && yarn dev` needs nothing configured. ```sh ./scripts/setup-cluster/setup-cluster.sh # ~25 min, mostly image builds ``` **http://localhost:8080** > [!TIP] > The script also leaves one agent on the cluster — an `assistant` template on a `kagent` harness — so **Agents** has something in it and you can send a message straight away, without creating anything first. > [!WARNING] > `make create-kind-cluster && make helm-install` does **not** work, and fails silently five different ways — including that the chart installs published images, so none of your changes are on the cluster while everything looks healthy. [`scripts/setup-cluster/README.md`](https://github.com/kagent-dev/kagent/blob/charlesthebird/uiRewrite/scripts/setup-cluster/README.md) covers each one, and the dev-server loop for iterating. ### UI Extensions The app declares vendor extension points anyone can use to add to it or restyle it, all in one configuration object: | Point | Contributes | | --- | --- | | `navItems` | Sidebar entries, positioned by `order` | | `navOverrides` | Hide, rename, re-order or redirect the app's own entries | | `routes` | Whole pages, merged into the router | | `routeHandles` | Shell data attached to the app's own routes | | `slots` | Components at named points inside existing pages | | `formFields` | Extra fields on existing forms | | `tableColumns` | Extra columns on existing tables | | `api` | Operation and endpoint overrides, payload transforms | | `providers` | App-level React providers | | `theme` | Colour, radius and font tokens | | `shell` | Whole shell regions replaced outright | | `branding` | Name and mark wherever the shell states its identity | | `providerIcons` | Provider icons for the model-config form | | `agentLinks` | Agent destinations | Installing one is two edits: build a `VendorExtensionConfig`, then point `src/vendorExtensions/activeConfig.ts` at it. Overriding theme tokens restyles the app's own components, not just the extension's. > [!NOTE] > 📖 [`ui/docs/vendor-extensions.md`](https://github.com/kagent-dev/kagent/blob/charlesthebird/uiRewrite/ui/docs/vendor-extensions.md) — every extension point and what it receives. Worth reading before reviewing the `vendorExtensions/` tree. ### Substrate The pages follow the CRDs. An **Agent** is derived, not a resource — a `Harness` × `AgentTemplate` pair read from `AgentTemplate.status.harnesses[]`, so there is no "New agent" button. The landing page explains the four concepts over three tabs; an agent's page lists its conversations, and a conversation is an `AgentInstance`. Five additive server-side changes, none affecting an existing caller: | Change | Note | | --- | --- | | `AgentInstance` gains a `name` | End to end, via `UpdateAgentInstanceName`. A column, so the write touches only the column. Renamed from the conversations table, the rail's action menu or the details modal; the rail lists newest first. | | `ListAgentInstances` takes a query | Narrows to one agent, resolved through `prepared_revision`. | | A2A gateway reads in any state | A suspended conversation still opens. | | A share resolves to its owner | The token widens what the reader's account may read. | | `app.go` defaults the A2A gateway | Used when no extension supplies one; an extension's handler still wins. | ### Test Coverage ```sh cd ui && yarn typecheck && yarn lint && yarn test && yarn test:pw cd go && go test ./... make proto-generate && make -C go sqlc-generate # then confirm `git status` is clean ``` 385 unit tests. The browser suite runs in **Chromium and Firefox**, plus a Chromium pass with the example extension installed, and in parallel in CI. ### Follow Ups - **The browser suite runs against the dev server, not the built image and a real controller.** The `UI Playwright E2E` workflow that did that is removed here, along with the harness it drove (`playwright/scripts/setup.sh`, `playwright/mocks/server.mjs`); restoring it is follow-up work. `yarn test:pw:live` covers a few journeys against a real cluster in the meantime. - **Storybook and the Chromatic workflow are removed**, since the rewrite ships no stories. - [`ui/playwright/DEFERRED.md`](https://github.com/kagent-dev/kagent/blob/charlesthebird/uiRewrite/ui/playwright/DEFERRED.md) lists the rest, and the surface each spec waits on. Nothing is committed as a skipped test. > [!NOTE] > `go test ./...` fails one pre-existing test on macOS — `TestFetchSourceReusesExistingMaterialization`, `/var` vs `/private/var`, in a package this change does not touch. ### Upgrade Notes > [!CAUTION] > **Breaking.** The chart no longer sets `NEXT_PUBLIC_BACKEND_URL`, `BACKEND_INTERNAL_URL` or `BACKEND_GRPC_URL`, and drops `ui.backendInternalUrl`, `ui.backendGrpcUrl` and `ui.volumes.nextjsCache`. The oauth2-proxy `skip-auth-regex` now names `/assets/` and `env-config.js` instead of the Next.js paths, which no longer exist. **Also included, unrelated to the rewrite:** `helm/tools/grafana-mcp` now passes `-allowed-hosts`. The server rejects any Host header it was not told about, so one reached over the cluster network answered the MCP handshake with `Forbidden` — the `RemoteMCPServer` sat `Accepted=False` and every agent using it failed to build its tool set. Found while testing the tools pages; happy to split it out if preferred. --- *🤖 written by Claude (end)* --------- Signed-off-by: Nicholas Bucher <behappy54321@gmail.com> Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io>
1 parent 426a133 commit 2a25933

698 files changed

Lines changed: 66795 additions & 66505 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yaml

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -358,33 +358,45 @@ jobs:
358358
uses: actions/setup-node@v7
359359
with:
360360
node-version-file: ui/.nvmrc
361-
cache: "npm"
362-
cache-dependency-path: ui/package-lock.json
363361

364-
# Honor the pinned npm from ui/package.json "packageManager" so npm ci
365-
# resolves the lock file with the same npm version it was generated with.
362+
# Before any cache step that shells out to yarn: the pinned version in
363+
# ui/package.json "packageManager" is Yarn 4, and without corepack the shim
364+
# on the runner is a different one that cannot read this lock file.
366365
- name: Enable Corepack
367366
run: corepack enable
368367

368+
- name: Cache Yarn downloads
369+
uses: actions/cache@v4
370+
with:
371+
path: ui/.yarn/cache
372+
key: yarn-${{ runner.os }}-${{ hashFiles('ui/yarn.lock') }}
373+
restore-keys: yarn-${{ runner.os }}-
374+
369375
- name: Install dependencies
370376
working-directory: ./ui
371-
run: npm ci
377+
run: yarn install --immutable
378+
379+
- name: Typecheck
380+
working-directory: ./ui
381+
run: yarn typecheck
372382

373383
- name: Run lint
374384
working-directory: ./ui
375-
run: npm run lint
385+
run: yarn lint
376386

377-
- name: Run unit tests (Jest)
387+
- name: Run unit tests
378388
working-directory: ./ui
379-
run: npm run test
389+
run: yarn test
380390

381-
- name: Install Playwright browser (Chromium)
391+
# Both engines the suite declares. Installing only one leaves that project
392+
# failing to launch, which reads as a broken app rather than a missing browser.
393+
- name: Install Playwright browsers
382394
working-directory: ./ui
383-
run: npx playwright install --with-deps chromium
395+
run: yarn playwright install --with-deps chromium firefox
384396

385-
- name: Run Storybook tests (Vitest + Playwright)
397+
- name: Run browser tests
386398
working-directory: ./ui
387-
run: npm run test:vitest
399+
run: yarn test:pw
388400

389401
# This job builds the Docker images for the controller, UI, ADKs, and CLI on arm64.
390402
build:

.github/workflows/ui-chromatic.yaml

Lines changed: 0 additions & 55 deletions
This file was deleted.

.github/workflows/ui-playwright.yaml

Lines changed: 0 additions & 119 deletions
This file was deleted.

CLAUDE.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,85 @@ Common commands:
108108
- Do not commit or push unless asked.
109109
- Keep PRs focused. Explain non-obvious invariants and operational tradeoffs, not line-by-line implementation details.
110110

111+
## The web interface (`ui/`)
112+
113+
A Vite single-page app. It is a static bundle served by nginx: there is no server
114+
process, so there are no server components, no server-side data fetching and no
115+
file-system routing.
116+
117+
**Stack:** Vite + React 19, TypeScript, antd 6 for components, Emotion for styling
118+
(the `css` prop, via `jsxImportSource`), SWR for reads, Yarn 4. React Router owns
119+
routing; there is no file-system routing and no server rendering.
120+
121+
### Commands
122+
123+
Run these from `ui/`:
124+
125+
| Task | Command |
126+
|------|---------|
127+
| Dev server | `yarn dev` |
128+
| Unit tests | `yarn test` |
129+
| End-to-end, no cluster needed | `yarn test:pw` (Chromium and Firefox) |
130+
| End-to-end against a real cluster | `yarn test:pw:live` |
131+
| Type check | `yarn typecheck` |
132+
| Lint | `yarn lint` |
133+
134+
Only lint **errors** gate a change; a handful of warnings are pre-existing.
135+
136+
`scripts/setup-cluster/setup-cluster.sh` builds a Kind cluster with kagent on it in one
137+
command, for work that needs a real backend.
138+
139+
### Settings reach the app at runtime, not at build time
140+
141+
Configuration is read from `window.environmentVariables`, which the container
142+
rewrites from its own environment on every start. So one image serves every
143+
deployment, and a setting is an operator's decision rather than something frozen
144+
into a build. Locally the same values come from `ui/.env` (git-ignored;
145+
`ui/.env.example` documents each one).
146+
147+
Two consequences worth knowing before touching that code:
148+
149+
- The script that supplies them is **synchronous** in `index.html`. Several modules
150+
read settings at import time, so anything awaited would be read before it arrived.
151+
- `import.meta.env` is for build-time flags only. A value that an operator should be
152+
able to change belongs in `window.environmentVariables`.
153+
154+
### Fixtures are opt-in
155+
156+
`ENABLE_MOCK_UI=true` serves the whole API from an in-browser mock (MSW) with no
157+
cluster at all, and `?mock=ok|empty|error|slow` picks which scenario the fixtures
158+
play. **It is off unless asked for**, in a dev server exactly as in a built image: a
159+
page that quietly serves fixtures when the backend is down looks healthy while
160+
showing data that was never real.
161+
162+
When mock mode is on it overrides every backend setting, and anything reporting who
163+
is signed in correctly reports nobody — there is no backend to have signed in to.
164+
165+
### Extension points
166+
167+
One `VendorExtensionConfig` contributes navigation entries and overrides, routes and
168+
route handles, slots, form fields, table columns, API overrides, providers, theme
169+
tokens, shell regions, branding, provider icons and agent links. Components read
170+
every colour, radius and font from those tokens, so overriding them restyles
171+
components an extension never touches. When adding a feature, check whether it
172+
belongs behind an extension point rather than as a branch inside a shared component.
173+
174+
The full guide is [ui/docs/vendor-extensions.md](ui/docs/vendor-extensions.md).
175+
176+
### Conventions specific to this codebase
177+
178+
- **Say when data is not real.** A page showing fixtures says so on the page. Never
179+
suppress an error because a mock flag is set — a broken backend must not render as
180+
healthy mock data.
181+
- **Normalise at the client boundary.** Go marshals a nil slice as JSON `null`, so
182+
any collection the controller has nothing for arrives as null. Fix it once where the
183+
response is parsed, not at each use.
184+
- **Fixtures must match the controller, not each other.** A fixture, a type and a
185+
test can agree perfectly and all three be wrong; that has happened here more than
186+
once and each time only a real cluster objected. Check the CRD.
187+
- **Prefer a smaller honest test suite** over a green one that proves nothing.
188+
Coverage debt belongs in `playwright/DEFERRED.md`, not in skipped specs.
189+
111190
## 9. References
112191

113192
- [STYLE.md](STYLE.md)

go/api/database/client.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ type Client interface {
9494
GetSessionShareByToken(ctx context.Context, token string) (*SessionShare, error)
9595
ListSessionSharesBySession(ctx context.Context, sessionID string) ([]SessionShare, error)
9696
DeleteSessionShare(ctx context.Context, token, sessionID, userID string) error
97-
RecordShareAccess(ctx context.Context, userID string, shareID int64) error
9897

9998
// Agent memory (vector search) methods
10099
StoreAgentMemory(ctx context.Context, memory *Memory) error
@@ -124,12 +123,19 @@ type Client interface {
124123
CreateAgentInstance(context.Context, *apiv1alpha1.AgentInstance, string) (*apiv1alpha1.AgentInstance, bool, error)
125124
ForkAgentInstance(context.Context, string, string, string, string, string) (*apiv1alpha1.AgentInstance, bool, error)
126125
GetAgentInstance(context.Context, string, string, string) (*apiv1alpha1.AgentInstance, error)
127-
ListAgentInstances(context.Context, string, string, bool, map[string]string, string, int) ([]*apiv1alpha1.AgentInstance, error)
126+
ListAgentInstances(context.Context, AgentInstanceQuery) ([]*apiv1alpha1.AgentInstance, error)
127+
// UpdateAgentInstanceName sets the instance's display name, scoped to its owner.
128+
// Takes namespace, id, owner and the new name.
129+
UpdateAgentInstanceName(context.Context, string, string, string, string) (*apiv1alpha1.AgentInstance, error)
128130
MarkAgentInstanceReady(context.Context, string, string) (*apiv1alpha1.AgentInstance, error)
129131
TransitionAgentInstance(context.Context, *apiv1alpha1.AgentInstance, apiv1alpha1.AgentInstanceState, apiv1alpha1.AgentInstanceOperation) (*apiv1alpha1.AgentInstance, error)
130132
DeleteAgentInstance(context.Context, string) error
131133
CreateAgentInstanceShare(context.Context, AgentInstanceShare) (*AgentInstanceShare, error)
132134
ListAgentInstanceShares(context.Context, string, string, string, string, int) ([]AgentInstanceShare, error)
135+
// GetAgentInstanceShareByTokenHash resolves a share token to its share and the
136+
// owner of the instance it grants access to. Takes the digest, because only the
137+
// digest is stored.
138+
GetAgentInstanceShareByTokenHash(context.Context, []byte) (*AgentInstanceShare, error)
133139
DeleteAgentInstanceShare(context.Context, string, string, string) error
134140
// CreateAgentInstanceTask reserves the instance's single active-task slot.
135141
CreateAgentInstanceTask(context.Context, string, []byte, *a2a.Task) (*a2a.Task, bool, error)

go/api/database/models.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,38 @@ type RuntimeRevision struct {
270270
GoldenSnapshot string
271271
}
272272

273+
// AgentInstanceQuery narrows a page of AgentInstances. Zero values mean "do not
274+
// filter on this", so an empty query lists the caller's own instances in the
275+
// namespace.
276+
type AgentInstanceQuery struct {
277+
Namespace string
278+
UserID string
279+
AllUsers bool
280+
MatchLabels map[string]string
281+
// AgentTemplate and Harness name the agent whose conversations are wanted.
282+
// They are matched against the (AgentTemplate, Harness) pair the instance's
283+
// prepared revision was built from, not against its labels, so they select
284+
// instances stored before either field existed.
285+
AgentTemplate string
286+
Harness string
287+
AfterID string
288+
Limit int
289+
}
290+
273291
type AgentInstanceShare struct {
274292
ID string
275293
Namespace string
276294
InstanceID string
277-
Creator string
278295
Permission string
279296
TokenHash []byte
280297
CreatedAt time.Time
298+
// OwnerUserID is the user the shared AgentInstance belongs to.
299+
//
300+
// Populated only by the token lookup, which joins it in — that is what the
301+
// share grants. A visitor is authenticated as themselves and the token widens
302+
// what their account may reach to what the *owner* can see, so the instance
303+
// read has to run as the owner or it finds nothing.
304+
OwnerUserID string
281305
}
282306

283307
// AgentInstanceTaskSnapshot identifies the immutable Substrate snapshot at a

0 commit comments

Comments
 (0)