From 150aecd6acb84af0af24a118fd6ac5c7c5aaa2d9 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 00:27:02 -0700 Subject: [PATCH 1/2] Add tests for agents roster copy (CL-6748) The name slot is display name only, status is Live not Running shout, and the model column maps a canonical id to the catalog displayName. --- apps/web/test/agents-page.test.tsx | 107 ++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/apps/web/test/agents-page.test.tsx b/apps/web/test/agents-page.test.tsx index d53f83d30..a26156df5 100644 --- a/apps/web/test/agents-page.test.tsx +++ b/apps/web/test/agents-page.test.tsx @@ -20,10 +20,11 @@ import { import type { AgentDefinitionWithDisplayName } from "../src/agents-directory"; import type { AgentInstance } from "../src/agents-api"; -// `name` is the immutable kebab identifier; `displayName` is what -// `withDisplayNames` (CL-6413) derives from the definition's description -// (or, absent one, a humanized reading of `name`) — the page renders both, -// never the slug alone. +// `name` is the immutable kebab identifier (the URL slug); `displayName` is +// what `withDisplayNames` (CL-6413) derives from the definition's +// description (or, absent one, a humanized reading of `name`). The roster +// name slot is displayName only (CL-6748) — the slug stays the URL, not a +// second line under the name. const triage: AgentDefinitionWithDisplayName = { id: "wfd_1", tenantId: "tnt_1", @@ -108,28 +109,62 @@ describe("runsInLast7Days", () => { describe("agentModelSettledContent (CL-6848)", () => { test("a fetch failure is an error, never the same label as an unset model", () => { expect( - agentModelSettledContent({ - status: "error", - message: "Something went wrong loading this agent's model. Try again.", - }), + agentModelSettledContent( + { + status: "error", + message: + "Something went wrong loading this agent's model. Try again.", + }, + [], + ), ).toEqual({ kind: "error", message: "Something went wrong loading this agent's model. Try again.", }); expect( - agentModelSettledContent({ - status: "ready", - data: { name: "triage-bot" }, - }), + agentModelSettledContent( + { + status: "ready", + data: { name: "triage-bot" }, + }, + [], + ), ).toEqual({ kind: "model", label: "Default" }); }); - test("a ready model name passes through", () => { + test("an unset model stays Default even when the catalog has a tenant default", () => { expect( - agentModelSettledContent({ - status: "ready", - data: { name: "triage-bot", model: "claude-sonnet-4" }, - }), + agentModelSettledContent( + { + status: "ready", + data: { name: "triage-bot" }, + }, + [{ canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" }], + ), + ).toEqual({ kind: "model", label: "Default" }); + }); + + test("a ready model maps to the catalog displayName, not the raw id", () => { + expect( + agentModelSettledContent( + { + status: "ready", + data: { name: "triage-bot", model: "claude-sonnet-4" }, + }, + [{ canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" }], + ), + ).toEqual({ kind: "model", label: "Claude Sonnet 4" }); + }); + + test("a canonical with no catalog displayName is shown as-is, never an invented name", () => { + expect( + agentModelSettledContent( + { + status: "ready", + data: { name: "triage-bot", model: "claude-sonnet-4" }, + }, + [], + ), ).toEqual({ kind: "model", label: "claude-sonnet-4" }); }); }); @@ -143,6 +178,7 @@ describe("AgentModelCellView (CL-6848)", () => { message: "Something went wrong loading this agent's model. Try again.", }} + catalog={[]} />, ); const unsetMarkup = renderToStaticMarkup( @@ -151,6 +187,7 @@ describe("AgentModelCellView (CL-6848)", () => { status: "ready", data: { name: "triage-bot" }, }} + catalog={[]} />, ); expect(errorMarkup).toContain("text-destructive"); @@ -164,6 +201,23 @@ describe("AgentModelCellView (CL-6848)", () => { // a fetch failure must not reuse it. expect(errorMarkup).not.toContain(">—<"); }); + + test("a ready model renders the catalog displayName, not a monospaced id", () => { + const markup = renderToStaticMarkup( + , + ); + expect(markup).toContain("Claude Sonnet 4"); + expect(markup).not.toContain("claude-sonnet-4"); + expect(markup).not.toContain("font-mono"); + }); }); describe("agentRunsSettledContent (CL-6842)", () => { @@ -276,7 +330,7 @@ describe("AgentsPage", () => { expect(markup).toContain("No agents yet"); }); - test("renders one row per definition with its status, name, and slug", () => { + test("renders one row per definition with its display name and Live status, not the slug", () => { const markup = renderToStaticMarkup( { />, ); expect(markup).toContain("Triage bot"); - expect(markup).toContain("triage-bot"); + expect(markup).not.toContain("triage-bot"); expect(markup).toContain("Sorts inbound issues."); - expect(markup).toContain("Running"); - // Running carries the live dot (react-ui's StatusDot, `live` prop) — + expect(markup).toContain("Live"); + expect(markup).not.toContain("Running"); + // Live carries the live dot (react-ui's StatusDot, `live` prop) — // the spec's liveness marker for an actively-running agent. expect(markup).toContain('aria-label="Live"'); + // Badge must not shout via CSS uppercase (same as CL-6747 skills). + expect(markup).toContain("normal-case"); }); - test("renders a humanized display name for a definition with no description, alongside its slug", () => { + test("renders a humanized display name for a definition with no description, not its slug", () => { const undescribed: AgentDefinitionWithDisplayName = { ...triage, id: "wfd_undescribed", @@ -325,7 +382,7 @@ describe("AgentsPage", () => { />, ); expect(markup).toContain("Research Analyst"); - expect(markup).toContain("research-analyst"); + expect(markup).not.toContain("research-analyst"); }); test("selecting a definition links each workbench instance to its own settings Agents tab", () => { @@ -381,6 +438,10 @@ describe("AgentsPage", () => { ); expect(markup).toContain('href="/agents/triage-bot"'); expect(markup).toContain("Open"); + expect(markup).toContain("Deployed"); + expect(markup).not.toContain(">deployed<"); + expect(markup).not.toContain(">DEPLOYED<"); + expect(markup).toContain("normal-case"); }); test("offers New agent as the top-bar create action, per the top-nav page-action contract", () => { From 9c4d68add3abefd9425406e30e4ee60867a29abd Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 00:57:01 -0700 Subject: [PATCH 2/2] Show roster display names, Live status, and catalog labels (CL-6748) The name slot is the display name only; status badges are sentence-case Live/Idle (not Running shout); the model column maps a stored canonical id through the tenant catalog the same way settings already does. --- apps/web/src/pages/agents-page.tsx | 91 ++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/apps/web/src/pages/agents-page.tsx b/apps/web/src/pages/agents-page.tsx index a5eeceae2..5398e1335 100644 --- a/apps/web/src/pages/agents-page.tsx +++ b/apps/web/src/pages/agents-page.tsx @@ -43,6 +43,7 @@ import { type AgentCapabilities, type AgentDefinition, type AgentInstance, + type CatalogModel, } from "../agents-api"; import { purposeAgentDefinitions, @@ -63,16 +64,21 @@ const DEFINITION_STATUS_TONE: Record = { stopped: "neutral", }; +const DEFINITION_STATUS_LABEL: Record = { + deployed: "Deployed", + stopped: "Stopped", +}; + /** The roster's Status column folds a definition's own deployed/stopped * state together with its live instances' statuses — a stopped definition - * always reads Archived; a deployed one reads Running while any instance + * always reads Archived; a deployed one reads Live while any instance * is actively running, Blocked while any instance is erroring, otherwise * Idle. `instances` is expected to already be a tenant's top-level runs * (`listTopLevelRuns`), never the folded per-workbench-host noise. */ export type AgentRosterStatus = "running" | "idle" | "blocked" | "archived"; const AGENT_ROSTER_STATUS_LABEL: Record = { - running: "Running", + running: "Live", idle: "Idle", blocked: "Blocked", archived: "Archived", @@ -176,32 +182,60 @@ export type AgentModelCellState = | { readonly status: "ready"; readonly data: AgentCapabilities } | { readonly status: "error"; readonly message: string }; +function catalogModelLabel( + canonical: string, + catalog: readonly { + readonly canonicalName: string; + readonly displayName?: string | null; + }[], +): string { + const match = catalog.find((model) => model.canonicalName === canonical); + return match?.displayName ?? canonical; +} + /** Settled Model-column content — an unset model reads as "Default"; a - * fetch failure is a distinct error, never the same label. */ + * fetch failure is a distinct error, never the same label. A set model + * maps through the tenant catalog's displayName (CL-6748), the same + * `displayName ?? canonicalName` reading settings already uses — never + * an invented label, and never a rewrite of an unset model to the + * tenant default (CL-6782). */ export function agentModelSettledContent( state: | { readonly status: "ready"; readonly data: AgentCapabilities } | { readonly status: "error"; readonly message: string }, + catalog: readonly { + readonly canonicalName: string; + readonly displayName?: string | null; + }[], ): | { readonly kind: "model"; readonly label: string } | { readonly kind: "error"; readonly message: string } { if (state.status === "error") { return { kind: "error", message: state.message }; } - return { kind: "model", label: state.data.model ?? "Default" }; + const canonical = state.data.model; + if (canonical === undefined) { + return { kind: "model", label: "Default" }; + } + return { kind: "model", label: catalogModelLabel(canonical, catalog) }; } /** Presentational half of the Model column — exported so tests can assert * the failure glyph without waiting on the per-row fetch effect. */ export function AgentModelCellView({ state, + catalog, }: { readonly state: AgentModelCellState; + readonly catalog: readonly { + readonly canonicalName: string; + readonly displayName?: string | null; + }[]; }) { if (state.status === "loading") { return ; } - const settled = agentModelSettledContent(state); + const settled = agentModelSettledContent(state, catalog); if (settled.kind === "error") { return ( @@ -209,19 +243,17 @@ export function AgentModelCellView({ ); } - return ( - - {settled.label} - - ); + return {settled.label}; } function AgentModelCell({ tenantId, definitionId, + catalog, }: { readonly tenantId: string; readonly definitionId: string; + readonly catalog: readonly CatalogModel[]; }) { const [capabilities, setCapabilities] = useState({ status: "loading", @@ -247,7 +279,7 @@ function AgentModelCell({ }; }, [tenantId, definitionId]); - return ; + return ; } /** Settled Runs · 7d content — a failed top-level-runs fetch is never the @@ -306,10 +338,12 @@ function AgentDetailPanel({ tenantId, definition, workbenches, + catalog, }: { readonly tenantId: string; readonly definition: AgentDefinitionWithDisplayName; readonly workbenches: readonly DefinitionWorkbenchInstance[]; + readonly catalog: readonly CatalogModel[]; }) { const [capabilities, setCapabilities] = useState< | { readonly status: "loading" } @@ -357,8 +391,11 @@ function AgentDetailPanel({
Status
- - {definition.status} + + {DEFINITION_STATUS_LABEL[definition.status]}
@@ -372,7 +409,9 @@ function AgentDetailPanel({ {capabilities.message} ) : null} {capabilities.status === "ready" - ? (capabilities.data.model ?? "Default") + ? capabilities.data.model === undefined + ? "Default" + : catalogModelLabel(capabilities.data.model, catalog) : null} @@ -435,6 +474,7 @@ export function AgentsPage({ workbenches, instances, instancesError = null, + models = [], now = Date.now(), selectedId, onSelect, @@ -454,6 +494,10 @@ export function AgentsPage({ /** When the top-level-runs fetch failed — Status/Runs · 7d must not pretend * the history is empty (CL-6842). */ readonly instancesError?: string | null; + /** Tenant catalog used to map a stored canonical model id to its + * person-readable displayName. Empty when the catalog failed independently + * — the column then shows the canonical, never an invented name. */ + readonly models?: readonly CatalogModel[]; readonly now?: number; readonly selectedId: string | null; readonly onSelect: (id: string | null) => void; @@ -614,14 +658,9 @@ export function AgentsPage({ /> -
- - {definition.displayName} - - - {definition.name} - -
+ + {definition.displayName} +
{definition.description !== null && @@ -643,7 +682,10 @@ export function AgentsPage({ size="xs" /> ) : null} - + {AGENT_ROSTER_STATUS_LABEL[status]} @@ -654,6 +696,7 @@ export function AgentsPage({ ) : ( — @@ -681,6 +724,7 @@ export function AgentsPage({ tenantId={tenantId} definition={selected} workbenches={workbenches.get(selected.id) ?? []} + catalog={models} /> ) : null} @@ -783,6 +827,7 @@ export function AgentsRoute({ ? describeApiError(runsQuery.error, "loading run history") : null } + models={directory.data.models} selectedId={selectedId} onSelect={(id) => navigate(