2026-04-20 15:10:54 +02:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
2026-06-19 09:15:01 +02:00
|
|
|
import { useEffect } from "react";
|
2026-04-20 15:10:54 +02:00
|
|
|
import { orchestratorApi, type SpawnAgentRequest } from "@/lib/api/orchestrator";
|
2026-06-19 09:15:01 +02:00
|
|
|
import { agentsApi, type AgentDefinition } from "@/lib/api/agents";
|
|
|
|
|
import { registerAgentRoster } from "@/lib/agent-utils";
|
2026-04-20 15:10:54 +02:00
|
|
|
import type { Agent, AgentRole, Team, AgentState } from "@/types";
|
|
|
|
|
|
|
|
|
|
export type { AgentDefinition } from "@/lib/api/agents";
|
|
|
|
|
|
2026-06-19 09:15:01 +02:00
|
|
|
// Offline / first-paint fallback only — the live `/api/agents` roster
|
|
|
|
|
// (useAgentDefinitions) is authoritative. Keep this list in sync when adding
|
|
|
|
|
// agents, but it is not the source of truth and may lag the backend.
|
2026-04-20 15:10:54 +02:00
|
|
|
const AGENT_ROSTER: Agent[] = [
|
|
|
|
|
// Board / Management
|
|
|
|
|
{ id: "1", agent_id: "main-pm", name: "Main PM", role: "main_pm" as AgentRole, team: null, cell: null, status: "idle" as AgentState },
|
|
|
|
|
{ id: "2", agent_id: "product-owner", name: "Product Owner", role: "product_owner" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
|
|
|
|
{ id: "3", agent_id: "head-marketing", name: "Head of Marketing", role: "head_marketing" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
|
|
|
|
{ id: "4", agent_id: "auditor", name: "Auditor", role: "auditor" as AgentRole, team: null, cell: null, status: "idle" as AgentState },
|
2026-06-19 09:15:01 +02:00
|
|
|
// Board-adjacent singletons
|
|
|
|
|
{ id: "20", agent_id: "intake-1", name: "Intake", role: "prompter" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
|
|
|
|
{ id: "21", agent_id: "secretary-1", name: "Secretary", role: "secretary" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
|
|
|
|
{ id: "22", agent_id: "pr-reviewer-1", name: "PR Reviewer", role: "pr_reviewer" as AgentRole, team: "board" as Team, cell: null, status: "idle" as AgentState },
|
2026-04-20 15:10:54 +02:00
|
|
|
// Backend Cell
|
|
|
|
|
{ id: "5", agent_id: "be-dev-1", name: "Backend Dev 1", role: "developer" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "6", agent_id: "be-dev-2", name: "Backend Dev 2", role: "developer" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "7", agent_id: "be-qa", name: "Backend QA", role: "qa" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "8", agent_id: "be-pm", name: "Backend PM", role: "cell_pm" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "9", agent_id: "be-doc", name: "Backend Documenter", role: "documenter" as AgentRole, team: "backend" as Team, cell: "backend", status: "idle" as AgentState },
|
|
|
|
|
// Frontend Cell
|
|
|
|
|
{ id: "10", agent_id: "fe-dev-1", name: "Frontend Dev 1", role: "developer" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "11", agent_id: "fe-dev-2", name: "Frontend Dev 2", role: "developer" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "12", agent_id: "fe-qa", name: "Frontend QA", role: "qa" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "13", agent_id: "fe-pm", name: "Frontend PM", role: "cell_pm" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
|
|
|
|
{ id: "14", agent_id: "fe-doc", name: "Frontend Documenter", role: "documenter" as AgentRole, team: "frontend" as Team, cell: "frontend", status: "idle" as AgentState },
|
|
|
|
|
// UX/UI Cell
|
|
|
|
|
{ id: "15", agent_id: "ux-dev-1", name: "UX/UI Dev 1", role: "developer" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
|
|
|
|
{ id: "16", agent_id: "ux-dev-2", name: "UX/UI Dev 2", role: "developer" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
2026-06-16 06:57:00 +02:00
|
|
|
{ id: "19", agent_id: "ux-qa", name: "UX/UI QA", role: "qa" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
2026-04-20 15:10:54 +02:00
|
|
|
{ id: "17", agent_id: "ux-pm", name: "UX/UI PM", role: "cell_pm" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
|
|
|
|
{ id: "18", agent_id: "ux-doc", name: "UX/UI Documenter", role: "documenter" as AgentRole, team: "ux_ui" as Team, cell: "ux_ui", status: "idle" as AgentState },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Query keys
|
|
|
|
|
export const agentKeys = {
|
|
|
|
|
all: ["agents"] as const,
|
|
|
|
|
definitions: () => [...agentKeys.all, "definitions"] as const,
|
|
|
|
|
orchestrator: () => [...agentKeys.all, "orchestrator"] as const,
|
|
|
|
|
status: () => [...agentKeys.orchestrator(), "status"] as const,
|
|
|
|
|
waiting: () => [...agentKeys.orchestrator(), "waiting"] as const,
|
|
|
|
|
agent: (id: string) => [...agentKeys.orchestrator(), "agent", id] as const,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fetch agent definitions from API
|
|
|
|
|
export function useAgentDefinitions() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: agentKeys.definitions(),
|
|
|
|
|
queryFn: agentsApi.getAll,
|
|
|
|
|
staleTime: 5 * 60 * 1000, // 5 min - agents don't change often
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 09:15:01 +02:00
|
|
|
/**
|
|
|
|
|
* Register the live `/api/agents` roster into the display-name resolver
|
|
|
|
|
* (agent-utils). Mount once near the app root so every surface that resolves an
|
|
|
|
|
* assignee (task table, task detail, journals, communications, commits) shows
|
|
|
|
|
* the real agent name instead of a raw UUID, and never drifts as agents are
|
|
|
|
|
* added backend-side. Returns nothing — it's a side-effecting sync.
|
|
|
|
|
*/
|
|
|
|
|
export function useAgentRosterSync(): void {
|
|
|
|
|
const { data: definitions } = useAgentDefinitions();
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (definitions && definitions.length > 0) {
|
|
|
|
|
registerAgentRoster(
|
|
|
|
|
definitions.map((d) => ({ uuid: d.uuid, slug: d.id, name: d.name })),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}, [definitions]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map team → cell (cells carry a cell name; board/management agents have none).
|
|
|
|
|
const TEAM_CELLS: ReadonlyArray<Team> = ["backend", "frontend", "ux_ui"] as Team[];
|
|
|
|
|
|
|
|
|
|
function definitionToAgent(def: AgentDefinition): Agent {
|
|
|
|
|
return {
|
|
|
|
|
id: def.uuid,
|
|
|
|
|
agent_id: def.id,
|
|
|
|
|
name: def.name,
|
|
|
|
|
role: (def.role ?? "developer") as AgentRole,
|
|
|
|
|
team: def.team,
|
|
|
|
|
cell: def.team && TEAM_CELLS.includes(def.team) ? def.team : null,
|
|
|
|
|
status: "idle" as AgentState,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 15:10:54 +02:00
|
|
|
// Hooks
|
|
|
|
|
|
2026-06-19 09:15:01 +02:00
|
|
|
// Returns the agent roster (live definitions when loaded, static fallback
|
|
|
|
|
// otherwise), enriched with live orchestrator status.
|
2026-04-20 15:10:54 +02:00
|
|
|
export function useAgents() {
|
2026-06-19 09:15:01 +02:00
|
|
|
const { data: definitions } = useAgentDefinitions();
|
2026-04-20 15:10:54 +02:00
|
|
|
const { data: orchestratorStatus } = useOrchestratorStatus();
|
|
|
|
|
|
2026-06-19 09:15:01 +02:00
|
|
|
// A stable signature so the derived roster refetches when the live set
|
|
|
|
|
// changes (react-query keys on this, not on the closure).
|
|
|
|
|
const rosterKey = definitions?.map((d) => d.id).join(",") ?? "static";
|
|
|
|
|
|
2026-04-20 15:10:54 +02:00
|
|
|
return useQuery({
|
2026-06-19 09:15:01 +02:00
|
|
|
queryKey: [...agentKeys.all, "roster", rosterKey],
|
2026-04-20 15:10:54 +02:00
|
|
|
queryFn: async (): Promise<Agent[]> => {
|
2026-06-19 09:15:01 +02:00
|
|
|
// Build a map of agent statuses from the orchestrator status array
|
2026-04-20 15:10:54 +02:00
|
|
|
const statusMap = new Map<string, string>();
|
|
|
|
|
if (orchestratorStatus?.agents) {
|
|
|
|
|
for (const agentStatus of orchestratorStatus.agents) {
|
|
|
|
|
statusMap.set(agentStatus.agent_id, agentStatus.state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 09:15:01 +02:00
|
|
|
const base: Agent[] =
|
|
|
|
|
definitions && definitions.length > 0
|
|
|
|
|
? definitions.map(definitionToAgent)
|
|
|
|
|
: AGENT_ROSTER;
|
|
|
|
|
|
|
|
|
|
// Enrich with live status from orchestrator
|
|
|
|
|
return base.map((agent) => {
|
2026-04-20 15:10:54 +02:00
|
|
|
const liveState = statusMap.get(agent.agent_id);
|
|
|
|
|
return {
|
|
|
|
|
...agent,
|
|
|
|
|
status: (liveState as AgentState) ?? agent.status,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
},
|
2026-06-16 06:57:00 +02:00
|
|
|
staleTime: 5 * 60 * 1000, // 5 min — allows roster to refresh when orchestrator status changes
|
2026-04-20 15:10:54 +02:00
|
|
|
enabled: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useOrchestratorStatus() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: agentKeys.status(),
|
|
|
|
|
queryFn: () => orchestratorApi.getStatus(),
|
|
|
|
|
refetchInterval: 10000, // Refetch every 10 seconds
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useAgentStatus(agentId: string) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: agentKeys.agent(agentId),
|
|
|
|
|
queryFn: () => orchestratorApi.getAgentStatus(agentId),
|
|
|
|
|
enabled: !!agentId,
|
|
|
|
|
refetchInterval: 5000, // Refetch every 5 seconds
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useWaitingAgents() {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: agentKeys.waiting(),
|
|
|
|
|
queryFn: () => orchestratorApi.getWaitingAgents(),
|
|
|
|
|
refetchInterval: 10000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useSpawnAgent() {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ agentId, request }: { agentId: string; request?: SpawnAgentRequest }) =>
|
|
|
|
|
orchestratorApi.spawn(agentId, request),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useStopAgent() {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ agentId, graceful = true }: { agentId: string; graceful?: boolean }) =>
|
|
|
|
|
orchestratorApi.stop(agentId, graceful),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useResolveWait() {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
return useMutation({
|
|
|
|
|
mutationFn: ({ agentId, resolution }: { agentId: string; resolution: string }) =>
|
|
|
|
|
orchestratorApi.resolveWait(agentId, resolution),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch a single agent definition by ID or slug
|
|
|
|
|
export function useAgentDefinition(agentId: string) {
|
|
|
|
|
return useQuery({
|
|
|
|
|
queryKey: [...agentKeys.definitions(), agentId],
|
|
|
|
|
queryFn: () => agentsApi.getOne(agentId),
|
|
|
|
|
enabled: !!agentId,
|
|
|
|
|
staleTime: 5 * 60 * 1000, // 5 min - agent definitions don't change often
|
|
|
|
|
});
|
|
|
|
|
}
|