fix(panel): AI Providers mix grid derives from the live roster (#613)

The per-agent override grid rendered a hand-maintained AGENT_GROUPS literal
that had drifted: ux-dev-2 and all four PR reviewers were absent, so their
model overrides could not be viewed or edited at all (the backend accepts
any slug). The grid now derives its sections from useAgentDefinitions()
with the same team helpers the Fleet page uses, org-ranked ordering, a
loading skeleton, and group HelpTips. The static offline-fallback maps
(agent-utils, use-agents, mock-data) get the missing agents too.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-20 20:38:23 +02:00
committed by GitHub
co-authored by Renn F
parent 7248e5b722
commit fbec679878
5 changed files with 550 additions and 210 deletions
@@ -73,6 +73,187 @@ vi.mock("@/lib/api/providers", () => ({
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
// Full live roster (minus CEO/system) — mirrors foundation/identity.py so the
// per-agent override grid is exercised against the real 25-agent org chart,
// including the four PR reviewers a prior hard-coded literal dropped.
const { useAgentDefinitions } = vi.hoisted(() => ({
useAgentDefinitions: vi.fn(),
}));
vi.mock("@/hooks/use-agents", () => ({ useAgentDefinitions }));
const FULL_ROSTER = [
{
id: "main-pm",
uuid: "u-main-pm",
name: "Main PM",
role: "main_pm",
team: "main_pm",
},
{
id: "product-owner",
uuid: "u-po",
name: "Product Owner",
role: "product_owner",
team: "board",
},
{
id: "head-marketing",
uuid: "u-hom",
name: "Head of Marketing",
role: "head_marketing",
team: "board",
},
{
id: "auditor",
uuid: "u-auditor",
name: "Auditor",
role: "auditor",
team: "board",
},
{
id: "intake-1",
uuid: "u-intake",
name: "Intake",
role: "prompter",
team: "board",
},
{
id: "secretary-1",
uuid: "u-secretary",
name: "Secretary",
role: "secretary",
team: "board",
},
{
id: "pr-reviewer-1",
uuid: "u-prr",
name: "PR Reviewer",
role: "pr_reviewer",
team: "board",
},
{
id: "be-dev-1",
uuid: "u-be1",
name: "Backend Developer 1",
role: "developer",
team: "backend",
},
{
id: "be-dev-2",
uuid: "u-be2",
name: "Backend Developer 2",
role: "developer",
team: "backend",
},
{
id: "be-qa",
uuid: "u-beqa",
name: "Backend QA",
role: "qa",
team: "backend",
},
{
id: "be-pm",
uuid: "u-bepm",
name: "Backend PM",
role: "cell_pm",
team: "backend",
},
{
id: "be-doc",
uuid: "u-bedoc",
name: "Backend Documenter",
role: "documenter",
team: "backend",
},
{
id: "be-pr-reviewer",
uuid: "u-bepr",
name: "Backend PR Reviewer",
role: "pr_reviewer",
team: "backend",
},
{
id: "fe-dev-1",
uuid: "u-fe1",
name: "Frontend Developer 1",
role: "developer",
team: "frontend",
},
{
id: "fe-dev-2",
uuid: "u-fe2",
name: "Frontend Developer 2",
role: "developer",
team: "frontend",
},
{
id: "fe-qa",
uuid: "u-feqa",
name: "Frontend QA",
role: "qa",
team: "frontend",
},
{
id: "fe-pm",
uuid: "u-fepm",
name: "Frontend PM",
role: "cell_pm",
team: "frontend",
},
{
id: "fe-doc",
uuid: "u-fedoc",
name: "Frontend Documenter",
role: "documenter",
team: "frontend",
},
{
id: "fe-pr-reviewer",
uuid: "u-fepr",
name: "Frontend PR Reviewer",
role: "pr_reviewer",
team: "frontend",
},
{
id: "ux-dev-1",
uuid: "u-ux1",
name: "UX/UI Developer 1",
role: "developer",
team: "ux_ui",
},
{
id: "ux-dev-2",
uuid: "u-ux2",
name: "UX/UI Developer 2",
role: "developer",
team: "ux_ui",
},
{ id: "ux-qa", uuid: "u-uxqa", name: "UX/UI QA", role: "qa", team: "ux_ui" },
{
id: "ux-pm",
uuid: "u-uxpm",
name: "UX/UI PM",
role: "cell_pm",
team: "ux_ui",
},
{
id: "ux-doc",
uuid: "u-uxdoc",
name: "UX/UI Documenter",
role: "documenter",
team: "ux_ui",
},
{
id: "ux-pr-reviewer",
uuid: "u-uxpr",
name: "UX/UI PR Reviewer",
role: "pr_reviewer",
team: "ux_ui",
},
] as unknown as import("@/hooks/use-agents").AgentDefinition[];
import { toast } from "sonner";
import { AIRoutingCard } from "../ai-routing-card";
@@ -96,6 +277,10 @@ describe("AIRoutingCard", () => {
saveSelfHostedConfig.mockClear();
testSelfHosted.mockClear();
getSelfHostedModels.mockClear();
useAgentDefinitions.mockReturnValue({
data: FULL_ROSTER,
isLoading: false,
});
});
afterEach(() => {
vi.clearAllMocks();
@@ -105,7 +290,9 @@ describe("AIRoutingCard", () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Grok (xAI) API key");
const grokSection = screen.getByText("Grok (xAI) API key").closest("section");
const grokSection = screen
.getByText("Grok (xAI) API key")
.closest("section");
const ollamaSection = screen
.getByText("Ollama Cloud API key")
.closest("section");
@@ -166,7 +353,7 @@ describe("AIRoutingCard", () => {
await waitFor(() => expect(ollamaInput.value).toBe(""));
});
it("groups the per-agent override list by org structure with two-column rows inside each group", async () => {
it("groups the per-agent override list by org structure with two-column rows inside each group, including all four PR reviewers", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Per-agent override (mix mode)");
@@ -176,34 +363,54 @@ describe("AIRoutingCard", () => {
"Backend Cell",
"Frontend Cell",
"UX/UI Cell",
"Intake / Secretary",
"Intake / Secretary / PR Review",
]) {
expect(
screen.getByRole("heading", { level: 4, name: title }),
).toBeInTheDocument();
}
// Spot-check a row from each end of the org chart.
// Spot-check a row from each end of the org chart, including the agents
// the old hard-coded literal dropped: ux-dev-2 and all four reviewers.
expect(screen.getByText("product-owner")).toBeInTheDocument();
expect(screen.getByText("be-dev-1")).toBeInTheDocument();
expect(screen.getByText("secretary-1")).toBeInTheDocument();
expect(screen.getByText("ux-dev-2")).toBeInTheDocument();
expect(screen.getByText("pr-reviewer-1")).toBeInTheDocument();
expect(screen.getByText("be-pr-reviewer")).toBeInTheDocument();
expect(screen.getByText("fe-pr-reviewer")).toBeInTheDocument();
expect(screen.getByText("ux-pr-reviewer")).toBeInTheDocument();
// The Backend Cell group renders its 5 rows inside a 2-column grid.
// The Backend Cell group renders its 6 rows (5 + PR reviewer) inside a
// 2-column grid.
const backendHeader = screen.getByRole("heading", {
level: 4,
name: "Backend Cell",
});
const rowGrid = backendHeader.nextElementSibling as HTMLElement;
expect(rowGrid.className).toContain("sm:grid-cols-2");
expect(rowGrid.querySelectorAll(":scope > div")).toHaveLength(5);
expect(rowGrid.querySelectorAll(":scope > div")).toHaveLength(6);
// All 20 agents still render exactly one override select each.
// All 25 agents (the full roster minus the CEO) still render exactly one
// override select each.
const mixSection = screen
.getByText("Per-agent override (mix mode)")
.closest("section")!;
expect(mixSection.querySelectorAll('[role="combobox"]')).toHaveLength(25);
});
it("shows a loading skeleton (not an empty grid) while the agent roster is still loading", async () => {
useAgentDefinitions.mockReturnValue({ data: undefined, isLoading: true });
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Per-agent override (mix mode)");
expect(
mixSection.querySelectorAll('[role="combobox"]'),
).toHaveLength(20);
screen.queryByRole("heading", { level: 4, name: "Board" }),
).not.toBeInTheDocument();
const mixSection = screen
.getByText("Per-agent override (mix mode)")
.closest("section")!;
expect(mixSection.querySelector(".animate-pulse")).toBeInTheDocument();
});
it("tooltip-wraps the Grok/Ollama key labels and status badges, not the raw Switch", async () => {
+243 -194
View File
@@ -42,80 +42,106 @@ import {
Zap,
} from "lucide-react";
import { toast } from "sonner";
import { AssignmentScope, ModelProvider } from "@/types";
import { AssignmentScope, AgentRole, ModelProvider } from "@/types";
import type { SelfHostedModel } from "@/lib/api/providers";
import type { RoutingMode, SelfHostedTestResult } from "@/lib/api/providers";
import { SelfHostedSection } from "@/components/settings/self-hosted-section";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { HelpTip } from "@/components/ui/help-tip";
import { Skeleton } from "@/components/ui/skeleton";
import { useAgentDefinitions } from "@/hooks/use-agents";
import {
getBoardAgents,
getMainPm,
getBackendAgents,
getFrontendAgents,
getUxAgents,
getSupportAgents,
type AgentDefinition,
} from "@/lib/agent-definitions";
// Matches the roboco agents_config AGENT_ROLE_MAP / AGENT_TEAM_MAP.
// Hard-coded so Mix mode shows a stable 18-row picker without an extra
// server round-trip. Grouped + ordered to mirror the org chart in CLAUDE.md.
//
// NOTE: CEO is explicitly excluded — it's the human-in-the-loop seat
// (Renzo), not an LLM-backed agent. Routing it anywhere would be a
// no-op in spawn_agent but confusing in the UI.
const AGENT_GROUPS: { title: string; agents: { slug: string; label: string }[] }[] = [
// Grouped to mirror the org chart in CLAUDE.md, sourced from the live
// `/api/agents` roster (useAgentDefinitions) instead of a hand-maintained
// literal — that literal drifted (missing ux-dev-2 + all 4 PR reviewers).
// CEO is excluded by construction: none of these selectors ever match it.
const AGENT_GROUP_DEFS: {
title: string;
titleHint: string;
select: (agents: AgentDefinition[] | undefined | null) => AgentDefinition[];
}[] = [
{
title: "Board",
agents: [
{ slug: "product-owner", label: "Product Owner" },
{ slug: "head-marketing", label: "Head of Marketing" },
{ slug: "auditor", label: "Auditor" },
],
titleHint: "Product Owner, Head of Marketing, Auditor",
select: getBoardAgents,
},
{
title: "Main PM",
agents: [{ slug: "main-pm", label: "Main PM" }],
titleHint: "Coordinates all three delivery cells",
select: getMainPm,
},
{
title: "Backend Cell",
agents: [
{ slug: "be-pm", label: "Backend PM" },
{ slug: "be-dev-1", label: "Backend Dev 1" },
{ slug: "be-dev-2", label: "Backend Dev 2" },
{ slug: "be-qa", label: "Backend QA" },
{ slug: "be-doc", label: "Backend Documenter" },
],
titleHint: "2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer",
select: getBackendAgents,
},
{
title: "Frontend Cell",
agents: [
{ slug: "fe-pm", label: "Frontend PM" },
{ slug: "fe-dev-1", label: "Frontend Dev 1" },
{ slug: "fe-dev-2", label: "Frontend Dev 2" },
{ slug: "fe-qa", label: "Frontend QA" },
{ slug: "fe-doc", label: "Frontend Documenter" },
],
titleHint: "2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer",
select: getFrontendAgents,
},
{
title: "UX/UI Cell",
agents: [
{ slug: "ux-pm", label: "UX/UI PM" },
{ slug: "ux-dev-1", label: "UX/UI Dev" },
{ slug: "ux-qa", label: "UX/UI QA" },
{ slug: "ux-doc", label: "UX/UI Documenter" },
],
titleHint: "2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer",
select: getUxAgents,
},
{
title: "Intake / Secretary",
agents: [
// Interactive (held-open chat) roles. Claude (SDK driver) and Grok
// (grok CLI) are the supported runtimes; assigning a Grok model routes
// them to the grok-prompter / grok-secretary image.
{ slug: "intake-1", label: "Intake (Prompter)" },
{ slug: "secretary-1", label: "Secretary" },
],
title: "Intake / Secretary / PR Review",
titleHint:
"On-demand CEO-facing roles (Intake, Secretary) plus the root PR reviewer — reviews root→master PRs and inbound external/fork PRs.",
select: getSupportAgents,
},
];
// Stable within-group ordering (PM/lead first, devs, QA, doc, reviewer last)
// so the picker doesn't churn alphabetically as the live roster loads —
// ties (e.g. dev-1/dev-2) break on slug, which already sorts correctly.
const ROLE_RANK: Partial<Record<AgentRole, number>> = {
[AgentRole.PRODUCT_OWNER]: 0,
[AgentRole.HEAD_MARKETING]: 1,
[AgentRole.AUDITOR]: 2,
[AgentRole.MAIN_PM]: 0,
[AgentRole.CELL_PM]: 0,
[AgentRole.DEVELOPER]: 1,
[AgentRole.QA]: 2,
[AgentRole.DOCUMENTER]: 3,
[AgentRole.PR_REVIEWER]: 4,
[AgentRole.PROMPTER]: 0,
[AgentRole.SECRETARY]: 1,
};
function byOrgOrder(a: AgentDefinition, b: AgentDefinition): number {
const ra = (a.role && ROLE_RANK[a.role]) ?? 99;
const rb = (b.role && ROLE_RANK[b.role]) ?? 99;
return ra !== rb ? ra - rb : a.id.localeCompare(b.id);
}
export function AIRoutingCard() {
const { data: catalog = [] } = useCatalog();
const { data: keyStatus } = useOllamaKey();
const { data: snapshot } = useRoutingMode();
const { data: selfHostedModels = [] } = useSelfHostedModels();
const { data: agentDefs, isLoading: agentsLoading } = useAgentDefinitions();
const agentGroups = useMemo(
() =>
AGENT_GROUP_DEFS.map((g) => ({
title: g.title,
titleHint: g.titleHint,
agents: g.select(agentDefs).slice().sort(byOrgOrder),
})).filter((g) => g.agents.length > 0),
[agentDefs],
);
const setKey = useSetOllamaKey();
const applyMode = useApplyMode();
@@ -231,7 +257,9 @@ export function AIRoutingCard() {
return;
try {
await applyMode.mutateAsync({ mode: "anthropic" });
toast.success("Role/global routing now on Anthropic — per-agent pins kept");
toast.success(
"Role/global routing now on Anthropic — per-agent pins kept",
);
} catch (e) {
toast.error("Switch failed: " + errMsg(e));
}
@@ -291,7 +319,9 @@ export function AIRoutingCard() {
mode: "self_hosted",
...(selfHostedModel ? { default_model: selfHostedModel } : {}),
});
toast.success("Role/global routing now on Self-Hosted LLM — per-agent pins kept");
toast.success(
"Role/global routing now on Self-Hosted LLM — per-agent pins kept",
);
} catch (e) {
toast.error("Switch failed: " + errMsg(e));
}
@@ -397,7 +427,10 @@ export function AIRoutingCard() {
}
disabled={clearGrokKey}
/>
<Button onClick={saveGrokKey} disabled={setGrokKeyMut.isPending}>
<Button
onClick={saveGrokKey}
disabled={setGrokKeyMut.isPending}
>
{setGrokKeyMut.isPending ? "Saving…" : "Save"}
</Button>
</div>
@@ -629,155 +662,171 @@ export function AIRoutingCard() {
Leave a row blank to inherit from the global mode. Saving overwrites
all per-agent overrides with what&apos;s picked here.
</p>
<div className="divide-y rounded-md border">
{AGENT_GROUPS.map((group) => (
<div key={group.title} className="p-4">
<h4 className="mb-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{group.title}
</h4>
<div className="grid grid-cols-1 gap-x-8 gap-y-3 sm:grid-cols-2">
{group.agents.map((a) => (
<div
key={a.slug}
className="grid grid-cols-[1fr_170px] items-center gap-4 rounded-md border px-3 py-2.5"
>
<div className="min-w-0">
<div className="truncate font-mono text-xs">
{a.slug}
</div>
<div className="truncate text-[11px] text-muted-foreground">
{a.label}
</div>
</div>
<Select
value={mixMap[a.slug] ?? ""}
onValueChange={(v: string) =>
setMixMap((prev) => ({
...prev,
[a.slug]: v === "__clear__" ? "" : v,
}))
}
>
<SelectTrigger size="sm" className="w-full text-xs">
<SelectValue placeholder="(inherit)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__clear__">
(inherit global)
</SelectItem>
{/* Anthropic models */}
{catalogAnthropicOnly.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="anthropic" />
Anthropic
</SelectLabel>
{catalogAnthropicOnly.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name}
</SelectItem>
),
)}
</SelectGroup>
)}
{/* Grok (xAI) models */}
{catalogGrokOnly.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="grok" />
Grok (xAI)
</SelectLabel>
{catalogGrokOnly.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name}
</SelectItem>
),
)}
</SelectGroup>
)}
{/* Ollama Cloud models */}
{catalogOllamaOnly.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="ollama" />
Ollama Cloud
</SelectLabel>
{catalogOllamaOnly.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name}
</SelectItem>
),
)}
</SelectGroup>
)}
{/* Self-Hosted models */}
{selfHostedModels.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="self-hosted" />
Self-Hosted
</SelectLabel>
{selfHostedModels.map((m: SelfHostedModel) => (
<SelectItem
key={m.model_name}
value={m.model_name}
>
{m.display_name}
</SelectItem>
))}
</SelectGroup>
)}
{/* Fallback: un-grouped catalog when no grouping is possible */}
{catalogAnthropicOnly.length === 0 &&
catalogOllamaOnly.length === 0 &&
selfHostedModels.length === 0 &&
catalogForMix.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name} {c.model_name}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
))}
{agentsLoading ? (
<div className="divide-y rounded-md border">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="p-4">
<Skeleton className="mb-2 h-3 w-24" />
<div className="grid grid-cols-1 gap-x-8 gap-y-3 sm:grid-cols-2">
<Skeleton className="h-14 w-full rounded-md" />
<Skeleton className="h-14 w-full rounded-md" />
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<div className="divide-y rounded-md border">
{agentGroups.map((group) => (
<div key={group.title} className="p-4">
<HelpTip label={group.titleHint}>
<h4 className="mb-2 w-fit text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{group.title}
</h4>
</HelpTip>
<div className="grid grid-cols-1 gap-x-8 gap-y-3 sm:grid-cols-2">
{group.agents.map((a) => (
<div
key={a.id}
className="grid grid-cols-[1fr_170px] items-center gap-4 rounded-md border px-3 py-2.5"
>
<div className="min-w-0">
<div className="truncate font-mono text-xs">
{a.id}
</div>
<div className="truncate text-[11px] text-muted-foreground">
{a.name}
</div>
</div>
<Select
value={mixMap[a.id] ?? ""}
onValueChange={(v: string) =>
setMixMap((prev) => ({
...prev,
[a.id]: v === "__clear__" ? "" : v,
}))
}
>
<SelectTrigger size="sm" className="w-full text-xs">
<SelectValue placeholder="(inherit)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__clear__">
(inherit global)
</SelectItem>
{/* Anthropic models */}
{catalogAnthropicOnly.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="anthropic" />
Anthropic
</SelectLabel>
{catalogAnthropicOnly.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name}
</SelectItem>
),
)}
</SelectGroup>
)}
{/* Grok (xAI) models */}
{catalogGrokOnly.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="grok" />
Grok (xAI)
</SelectLabel>
{catalogGrokOnly.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name}
</SelectItem>
),
)}
</SelectGroup>
)}
{/* Ollama Cloud models */}
{catalogOllamaOnly.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="ollama" />
Ollama Cloud
</SelectLabel>
{catalogOllamaOnly.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name}
</SelectItem>
),
)}
</SelectGroup>
)}
{/* Self-Hosted models */}
{selfHostedModels.length > 0 && (
<SelectGroup>
<SelectLabel>
<ProviderBadge variant="self-hosted" />
Self-Hosted
</SelectLabel>
{selfHostedModels.map((m: SelfHostedModel) => (
<SelectItem
key={m.model_name}
value={m.model_name}
>
{m.display_name}
</SelectItem>
))}
</SelectGroup>
)}
{/* Fallback: un-grouped catalog when no grouping is possible */}
{catalogAnthropicOnly.length === 0 &&
catalogOllamaOnly.length === 0 &&
selfHostedModels.length === 0 &&
catalogForMix.map(
(c: {
model_name: string;
display_name: string;
}) => (
<SelectItem
key={c.model_name}
value={c.model_name}
>
{c.display_name} {c.model_name}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
))}
</div>
</div>
))}
</div>
)}
{catalogOllamaOnly.length === 0 ? (
<p className="text-xs text-muted-foreground">
Ollama catalog empty check /api/providers/catalog.
+27
View File
@@ -125,6 +125,15 @@ const AGENT_ROSTER: Agent[] = [
cell: "backend",
status: "idle" as AgentState,
},
{
id: "23",
agent_id: "be-pr-reviewer",
name: "Backend PR Reviewer",
role: "pr_reviewer" as AgentRole,
team: "backend" as Team,
cell: "backend",
status: "idle" as AgentState,
},
// Frontend Cell
{
id: "10",
@@ -171,6 +180,15 @@ const AGENT_ROSTER: Agent[] = [
cell: "frontend",
status: "idle" as AgentState,
},
{
id: "24",
agent_id: "fe-pr-reviewer",
name: "Frontend PR Reviewer",
role: "pr_reviewer" as AgentRole,
team: "frontend" as Team,
cell: "frontend",
status: "idle" as AgentState,
},
// UX/UI Cell
{
id: "15",
@@ -217,6 +235,15 @@ const AGENT_ROSTER: Agent[] = [
cell: "ux_ui",
status: "idle" as AgentState,
},
{
id: "25",
agent_id: "ux-pr-reviewer",
name: "UX/UI PR Reviewer",
role: "pr_reviewer" as AgentRole,
team: "ux_ui" as Team,
cell: "ux_ui",
status: "idle" as AgentState,
},
];
// Query keys
+10 -6
View File
@@ -55,18 +55,21 @@ const AGENT_UUIDS: Record<string, string> = {
"00000000-0000-0000-0001-000000000003": "be-qa",
"00000000-0000-0000-0001-000000000004": "be-pm",
"00000000-0000-0000-0001-000000000005": "be-doc",
"00000000-0000-0000-0001-000000000006": "be-pr-reviewer",
// Frontend Cell
"00000000-0000-0000-0002-000000000001": "fe-dev-1",
"00000000-0000-0000-0002-000000000002": "fe-dev-2",
"00000000-0000-0000-0002-000000000003": "fe-qa",
"00000000-0000-0000-0002-000000000004": "fe-pm",
"00000000-0000-0000-0002-000000000005": "fe-doc",
"00000000-0000-0000-0002-000000000006": "fe-pr-reviewer",
// UX/UI Cell
"00000000-0000-0000-0003-000000000001": "ux-dev-1",
"00000000-0000-0000-0003-000000000002": "ux-dev-2",
"00000000-0000-0000-0003-000000000003": "ux-qa",
"00000000-0000-0000-0003-000000000004": "ux-pm",
"00000000-0000-0000-0003-000000000005": "ux-doc",
"00000000-0000-0000-0003-000000000006": "ux-pr-reviewer",
// Board / Management
"00000000-0000-0000-0004-000000000001": "main-pm",
"00000000-0000-0000-0004-000000000002": "product-owner",
@@ -92,18 +95,21 @@ const AGENT_NAMES: Record<string, string> = {
"be-dev-2": "Backend Dev 2",
"be-qa": "Backend QA",
"be-doc": "Backend Doc",
"be-pr-reviewer": "Backend PR Reviewer",
// Frontend Cell
"fe-pm": "Frontend PM",
"fe-dev-1": "Frontend Dev 1",
"fe-dev-2": "Frontend Dev 2",
"fe-qa": "Frontend QA",
"fe-doc": "Frontend Doc",
"fe-pr-reviewer": "Frontend PR Reviewer",
// UX/UI Cell
"ux-pm": "UX/UI PM",
"ux-dev-1": "UX/UI Dev 1",
"ux-dev-2": "UX/UI Dev 2",
"ux-qa": "UX/UI QA",
"ux-doc": "UX/UI Doc",
"ux-pr-reviewer": "UX/UI PR Reviewer",
// CEO (human)
ceo: "CEO",
CEO: "CEO",
@@ -181,18 +187,21 @@ const AGENT_CODES: Record<string, string> = {
"be-dev-2": "BD2",
"be-qa": "BQA",
"be-doc": "BDC",
"be-pr-reviewer": "BPR",
// Frontend Cell
"fe-pm": "FPM",
"fe-dev-1": "FD1",
"fe-dev-2": "FD2",
"fe-qa": "FQA",
"fe-doc": "FDC",
"fe-pr-reviewer": "FPR",
// UX/UI Cell
"ux-pm": "UPM",
"ux-dev-1": "UD1",
"ux-dev-2": "UD2",
"ux-qa": "UQA",
"ux-doc": "UDC",
"ux-pr-reviewer": "UPR",
// CEO
ceo: "CEO",
CEO: "CEO",
@@ -240,12 +249,7 @@ export function isKnownAgent(agentId: string | null | undefined): boolean {
// ---------------------------------------------------------------------------
export type AgentTeamColor =
| "backend"
| "frontend"
| "ux_ui"
| "board"
| "ceo"
| "system";
"backend" | "frontend" | "ux_ui" | "board" | "ceo" | "system";
/** Every value here is an existing Tailwind color family already used
* elsewhere in the codebase at the same `/15` bg + `/40` border weight
+54 -1
View File
@@ -25,24 +25,31 @@ export const AGENT_IDS = {
mainPm: "main-pm",
headMarketing: "head-marketing",
auditor: "auditor",
// Board-adjacent singletons (CEO-facing / read-only)
intake: "intake-1",
secretary: "secretary-1",
prReviewer: "pr-reviewer-1",
// Backend Cell
bePm: "be-pm",
beDev1: "be-dev-1",
beDev2: "be-dev-2",
beQa: "be-qa",
beDoc: "be-doc",
bePrReviewer: "be-pr-reviewer",
// Frontend Cell
fePm: "fe-pm",
feDev1: "fe-dev-1",
feDev2: "fe-dev-2",
feQa: "fe-qa",
feDoc: "fe-doc",
fePrReviewer: "fe-pr-reviewer",
// UX/UI Cell
uxPm: "ux-pm",
uxDev1: "ux-dev-1",
uxDev2: "ux-dev-2",
uxQa: "ux-qa",
uxDoc: "ux-doc",
uxPrReviewer: "ux-pr-reviewer",
};
export const TASK_IDS = {
@@ -123,6 +130,13 @@ export const mockAgents: MockAgent[] = [
role: AgentRole.DOCUMENTER,
team: Team.BACKEND,
},
{
id: AGENT_IDS.bePrReviewer,
name: "Backend PR Reviewer",
slug: "be-pr-reviewer",
role: AgentRole.PR_REVIEWER,
team: Team.BACKEND,
},
// Frontend Cell
{
id: AGENT_IDS.feDev1,
@@ -159,7 +173,14 @@ export const mockAgents: MockAgent[] = [
role: AgentRole.DOCUMENTER,
team: Team.FRONTEND,
},
// UX/UI Cell (only 1 dev per seed data)
{
id: AGENT_IDS.fePrReviewer,
name: "Frontend PR Reviewer",
slug: "fe-pr-reviewer",
role: AgentRole.PR_REVIEWER,
team: Team.FRONTEND,
},
// UX/UI Cell (2 devs per seed data)
{
id: AGENT_IDS.uxDev1,
name: "UX/UI Developer 1",
@@ -195,6 +216,13 @@ export const mockAgents: MockAgent[] = [
role: AgentRole.DOCUMENTER,
team: Team.UX_UI,
},
{
id: AGENT_IDS.uxPrReviewer,
name: "UX/UI PR Reviewer",
slug: "ux-pr-reviewer",
role: AgentRole.PR_REVIEWER,
team: Team.UX_UI,
},
// Board/Management (team=null per seed data)
{
id: AGENT_IDS.mainPm,
@@ -224,6 +252,31 @@ export const mockAgents: MockAgent[] = [
role: AgentRole.AUDITOR,
team: null,
},
// Board-adjacent singletons — CEO-facing on-demand roles, not Board
// reviewers. pr-reviewer-1 carries team=board (unlike its siblings here)
// because that's the only signal distinguishing it from the per-cell
// reviewers below, which carry their own cell's team instead.
{
id: AGENT_IDS.intake,
name: "Intake",
slug: "intake-1",
role: AgentRole.PROMPTER,
team: null,
},
{
id: AGENT_IDS.secretary,
name: "Secretary",
slug: "secretary-1",
role: AgentRole.SECRETARY,
team: null,
},
{
id: AGENT_IDS.prReviewer,
name: "PR Reviewer",
slug: "pr-reviewer-1",
role: AgentRole.PR_REVIEWER,
team: Team.BOARD,
},
// CEO (Human - Renzo)
{
id: AGENT_IDS.ceo,