Imported from ZJU-REAL/ageval (
apps/viewer/AGENTS.md). Install upstream withnpx skills add ZJU-REAL/ageval --skill viewer. Copyright stays with the author.
ageval Viewer — Agent constraints
This directory is the local dataset / suite-results Web UI. Python serves the built SPA; React app lives here.
Authority
| Doc | Role |
|---|---|
docs/design/13-web-ui-tokens.md |
Visual constitution (tokens, focus, motion) |
apps/shared/DESIGN.md |
SPA token YAML (Hub/Viewer share it) |
| DESIGN.md | Taste (anti-slop) + which component to reuse |
| This file | Product scope + stack + API |
src/ageval/viewer/ |
HTTP API + static file serving (stdlib) |
When UI conflicts with taste: docs/13 for language; DESIGN.md Taste for
anti-slop; DESIGN.md role table for which control. Evidence tabs use
UnderlineTabs. Jobs/tasks stay tables.
Product surface (scope lock)
ageval view takes a dataset root, a registry ref, or a directory that is not
itself a dataset. A non-dataset directory is scanned one level: children
whose ageval.yaml is ageval.dataset/1 are opened; everything else is
skipped. The server still starts when that list is empty.
Header destinations, same chrome either way: Datasets, Jobs, Agents, Plugins. No sidebar. Lists are tables, not catalog cards. Dataset choice on Jobs is a Select.
Landing: more than one dataset → Datasets. Exactly one (including
ageval view <dataset>) → Jobs. The Datasets table still lists that one row.
--open wins over the landing.
In scope:
- Datasets — one row per opened package, grouped by the organization in
dataset_id. Displaydataset_id@version. The switch key is the directory name (two siblings may sharedataset_id). Opening a row is read-only: README when present,ageval.yaml,profiles.yamlwhen present, and a file tree with a size-capped preview..ageval/runsand.ageval/suite-runsare not on that tree. - Jobs list — search; filter dropdowns; sortable columns; row click.
Jobs = that package's
.ageval/suite-runs/and.ageval/runs/only. Routes carry the directory key so two packages cannot share ajob_id. Kind, Source, harness, model, and the suite chart are query parameters on that path. Back restores them. Dataset stays in the path. Kind suite shows finished jobs over the jobs listed and keeps the Trials column visible. The list refreshes while a suite is still running. - Job → tasks — task table with scores / status / agent-model meta. A suite job shows finished/total tasks above the task table, and a horizontally scrolling plot of task duration (height) and result (dot color). Time left is the recent gap between finishes times the tasks still open. The page refreshes while tasks are still open.
- Task detail — trials/run row(s), status/error coloring, copyable CLI
- Attempt / trial detail —
Jobs > job > task > run_id; Outcome + actors (Role / Harness / Model / Time / Usage) + tabs from real evidence only (Trajectory · Agent · Verifier · Artifacts · Lock · Runtime). Top bar may show framework / environment kind /provenance.upstream.url. Multi-role groups Trajectory and Agent tree byprofile_id. - Breadcrumb —
Jobs > jobId > taskId > runIdwith>separators; click to navigate - Delete a local Job — Jobs row menu or bulk selection. Preview paths /
bytes / cascade
run_ids, then confirm. Suite delete always removes referenced Attempts. No delete control on an inner trial. Does not call Registry. Deletes only under the selected dataset root. Same Application use case asageval jobs delete --local … --yes. - Pin / note (this browser) —
localStoragekeyed bydataset_id+job_id. Not written to evidence, lock, or Registry. Pinned rows sort first. Action icon (always on): note, else pin, else hover settings. Hover a note icon to read the note. Click the settings/note control to open the menu (do not open on hover). - Plugins — rows from
$AGEVAL_HOME/pluginsindex.json(id, version)plus first-party contrib short ids from this CLI, marked built-in. Read-only: README when present,plugin.yaml, declared slots, file tree, size-capped preview. The table shows description; source stays a filter. - Agents — same for
$AGEVAL_HOME/agentsand the builtin Agent catalog (agent.yaml). - Snapshot share — from an opened suite or one job, upload a read-only
Hub link
/s/{token}. A suite can be frozen or live. A job stays frozen. Does not change visibility orboard_listed. Browsing still reads only local files.
Out of scope unless user asks:
- Public catalog, OAuth, Postgres, Leaderboard SPA (#22)
- Catalog publish,
upload-suite, release, and remote delete - Viewer sidebar, catalog-card lists, install / uninstall / editing yaml
- Scanning more than one directory level, opening
$AGEVAL_HOMEas a file tree, readingcredentials, or merging the cwd Registry cache into Datasets - Scanning the git checkout
plugins/tree (builtins come from the running CLI) - A gold tab.
tasks/*/evaluation/may appear as ordinary package files - Marketing mesh gradients, dark neon skins, custom CSS component kits
- Fabricating Harbor-only files or empty evidence tabs
- Soft-delete trash, live cancel,
l1-workcleanup, deleting one Attempt inside a suite
Stack (mandatory)
| Layer | Choice |
|---|---|
| Build | Vite + React + TypeScript |
| Package manager | pnpm only (packageManager field; not npm/yarn) |
| Styling | Tailwind CSS v4 (or v3 if tooling forces) |
| Components | shadcn/ui (Radix primitives) — overlap under apps/shared/components/ui/ |
| Icons | lucide-react (shadcn default) — one family only |
| Table | shadcn Table patterns (sortable heads in-app) |
| Routing | react-router-dom (client routes under SPA) |
| Fonts | Geist if vendored; else Inter + system mono |
Forbidden:
- Hand-rolled full CSS design system replacing shadcn
- Second component library (MUI, Ant, Chakra, Bootstrap)
- Inline style soup for layout chrome
- Google Fonts CDN in production (prefer self-host / system stack)
Design discipline
- Read
docs/design/13-web-ui-tokens.mdand DESIGN.md (Taste + role table) before changing colors, type, or density. - Do not describe or invent page layout in this file. Copy a shipped control.
- Tabular nums for scores, rates, durations, trial fractions.
- Mono only for commands, digests, technical IDs.
- No em-dash (
—) in UI strings. Nouppercase trackingsection eyebrows. - Prefer density of a results console, not an art gallery landing page.
- Semantic tokens only. No
slate-/zinc-/gray-utilities.
UI reuse (mandatory)
Same stack as Hub: overlap primitives in apps/shared/components/ui/. Role → component:
DESIGN.md.
- Copy an existing instance, including focus classes. Version / filter /
action lists go through
@ageval/shared/components/ui/selector@ageval/shared/components/ui/dropdown-menu. Match HubVersionSwitcher(label + trailing date onSelectItem). - No native
<select>/<option>and no hand-rolled dropdown for product chrome. If the primitive is missing a slot (e.g.trailing), extendapps/shared/components/ui/select.tsxso Hub and Viewer stay aligned. - Scan vs edit focus (docs/13): Jobs search keeps
hairlineon focus. Do not acceptInput's defaultborder-linkfor a new search. - Operator-facing list text is a short label plus time. Slot history:
patch N+formatDate/formatDay. Do not putrun_idor sha256 in the trigger or the menu. Digests stay on the trial heading / breadcrumb. - Do not add a second component library or a one-off styled native control.
- A failed write uses
toast(message, { tone: "error" }). Do not write that message into the page. Inlinetext-erroris for a field, or for a row whose result is an error. Confirmations stay in the existing dialog. Docs/13: do not also toast when the control already shows success. - Do not restyle Button at the call site. Hover, border, fill, and
radius come from the variant.
classNameis layout only. Header destinations usesidebarHoverClass(the bar iscanvas-soft). Button groups useSegmentedControl. In-app links useINTERNAL_LINK_CLASS. See docs/13.
Backend contract
Python API under /api/* (see src/ageval/viewer/):
| Path | Purpose |
|---|---|
GET /api/health |
Liveness |
GET /api/session |
Opened datasets, landing (jobs when exactly one, else datasets) |
GET /api/datasets |
Dataset rows (key = directory name, label dataset_id@version) |
GET /api/datasets/{key} |
README / manifest / profiles names for that package |
GET /api/datasets/{key}/tree |
Package file tree. Omits .ageval/runs and .ageval/suite-runs |
GET /api/datasets/{key}/file?path= |
Size-capped preview. Refuses .., credentials, and paths outside the root |
GET /api/plugins |
Builtin contrib rows plus $AGEVAL_HOME/plugins index rows |
GET /api/plugins/package?source=&id=&version= |
Plugin package names (README, plugin.yaml) |
GET /api/plugins/tree |
Same query. Tree confined to that package root |
GET /api/plugins/file?path= |
Preview. Same refusal rules |
GET /api/agents |
Builtin catalog plus $AGEVAL_HOME/agents index rows |
GET /api/agents/package |
Agent package names (README, agent.yaml) |
GET /api/agents/tree |
Tree confined to that package root |
GET /api/agents/file?path= |
Preview. Same refusal rules |
GET /api/jobs?dataset= |
Job list for that directory key. Omitted when exactly one dataset is open |
GET /api/jobs/{id} |
Job detail + task rows |
GET /api/jobs/{id}/delete-preview |
Paths, bytes, cascade run ids, confirm token; refuse reasons |
DELETE /api/jobs/{id}?confirm= |
Hard-delete after preview token (suite cascades Attempts) |
GET /api/jobs/{id}/tasks/{task_id} |
Task detail + enriched trials list + commands |
GET /api/jobs/{id}/tasks/{task_id}/trials |
Trials list (suite + local evidence) |
GET /api/jobs/{id}/tasks/{task_id}/trials/{run_id} |
Attempt meta + available_tabs |
GET .../trials/{run_id}/tree?scope= |
File tree under evidence (agent/verifier/runtime/…) |
GET .../trials/{run_id}/file?path= |
File preview (size-capped; secret-like names redacted) |
GET .../trials/{run_id}/trajectory |
Parsed trajectory.jsonl steps (observational) |
GET /api/jobs/{id}/share |
Snapshot share status for that suite, or one job when run_id is set |
POST /api/jobs/{id}/share |
Create the link. JSON may set run_id and live (boolean) |
DELETE /api/jobs/{id}/share |
Revoke that link. Optional run_id query |
Trial meta returns framework / docker / upstream_url (and related provenance
fields) plus actors[] from lock + invocation metadata: role · agent · model ·
Time (time_label / latency_ms_sum) · Usage (usage_label / normalized last-inv
usage). Cache hit rate uses inclusion/disjoint heuristics; never treat
UsageUpdate.used as billable tokens.
| Tab | Disk scope | Meaning |
|---|---|---|
| Artifacts | artifacts/, harness/, agent/artifacts/ |
Harness-published product files (JSON results, terminal, etc.) |
| Runtime | root effects.jsonl, cleanup.json, summary.json, agent.json, harness.json |
Attempt bookkeeping — not publishable outputs |
| Agent | agent/ |
Invocation trees, trajectory raw, backend_raw |
| Verifier | evaluation/, eval_staging/, result.json |
Evaluator side |
SPA file preview: JSON/JSONL pretty-print + lightweight syntax highlight (no extra deps).
Job and evidence paths stay inside the dataset chosen by ?dataset= (or the only opened dataset). job_id / task_id / run_id are single-segment; file paths reject ... Package preview roots are only: that dataset directory, an indexed plugin or Agent package under $AGEVAL_HOME, or a builtin tree shipped in the CLI. Never credentials. Never the whole $AGEVAL_HOME. A basename that contains env and ends in .example, .sample, .template, or .dist previews as text. .env, .env.local, .env.production, and other .env.* basenames stay redacted. Browsing does not call Registry. Snapshot share does, through /api/jobs/{id}/share.
Evidence roots: {dataset}/.ageval/runs/{run_id} or task-local .ageval/runs/; lock task_id must match when present.
Old /api/dataset and /api/tasks/... browse routes stay absent.
Build & serve
# from apps/viewer — pnpm only (not npm)
pnpm install
pnpm build # → dist/
# from repo root
uv run ageval view <dataset> --no-browser
Python must serve apps/viewer/dist/ (production build). There is no separate static/ source tree.
Dev: ageval view --dev starts the API and tries to spawn Vite. If that cannot run, it prints pnpm --dir apps/viewer dev. Same API as production ageval view. Deep-link with --open /jobs/....
Theme
- Modes: light | dark | system (default system).
- Toggle in top-right header; persist
localStoragekeyageval-viewer-theme. - Tokens via
data-theme+ CSS variables insrc/index.css.
CLI command strip
- Render with shell-style highlighting (not flat link-blue).
- Dark code surface (
code-bg); token kinds: cmd / flag / string / path / plain.
Delivery rules
- Prefer phase commits by functional slice (e.g. API → UI → tests and doc sync); update the relevant documentation when wrapping up a phase rather than making standalone doc-only commits at the start.
- Do not claim #22 Leaderboard done.
tests/viewer/is the Python HTTP API. Keepuv run pytest tests/viewer/ -qandpnpm buildgreen. Website, Hub, and Viewer UI is checked by a person on the rendered page. Do not add a component, snapshot, or browser test file underapps/orwebsite/. Do not add a pytest that asserts visible copy, class names, DOM ids, or markup the test itself wrote.- Column labels map ageval fields for operators, e.g.
Result≈mean_score/pass_rate,Trials≈ task counts.
Anti-patterns (reject in review)
- Visual slop: DESIGN.md Taste (hero / purple / eyebrows / em-dash / search IKB focus)
- Recreating ad-hoc full-page CSS SPA as default
- Ignoring breadcrumb click-through
- Unsortable tables when columns are metric-like
- Shipping unbuilt
src/only (always producedist/forageval view) - Native
<select>for a product list when shadcnSelectalready exists - Digest /
run_idas the visible label in a version or filter menu
