[chore] panel: prettier reformat across the codebase

Apply `pnpm format` (prettier 3.8.5, 80-col / double-quote / semi /
trailing-comma-all) to the 223 pre-existing panel files that predated the
prettier infra added in cb5365a4. Pure formatting — no semantic changes:
multi-line arrays/objects collapsed where they fit, trailing newlines added
(.prettierrc.json), import grouping unchanged.

Verified: `pnpm format:check` clean, `pnpm lint` clean, `pnpm typecheck`
clean, `pnpm test` 113/113 pass (7 files).
This commit is contained in:
Renn F
2026-06-27 01:12:05 +02:00
parent 204e1525ee
commit 517bee7d28
223 changed files with 5721 additions and 2505 deletions
+228 -30
View File
@@ -1,6 +1,9 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { orchestratorApi, type SpawnAgentRequest } from "@/lib/api/orchestrator";
import {
orchestratorApi,
type SpawnAgentRequest,
} from "@/lib/api/orchestrator";
import { agentsApi, type AgentDefinition } from "@/lib/api/agents";
import { registerAgentRoster } from "@/lib/agent-utils";
import type { Agent, AgentRole, Team, AgentState } from "@/types";
@@ -12,32 +15,208 @@ export type { AgentDefinition } from "@/lib/api/agents";
// agents, but it is not the source of truth and may lag the backend.
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 },
{
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,
},
// 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 },
{
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,
},
// 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 },
{
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 },
{
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 },
{ 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 },
{ 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 },
{
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,
},
{
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,
},
{
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
@@ -78,7 +257,11 @@ export function useAgentRosterSync(): void {
}
// Map team → cell (cells carry a cell name; board/management agents have none).
const TEAM_CELLS: ReadonlyArray<Team> = ["backend", "frontend", "ux_ui"] as Team[];
const TEAM_CELLS: ReadonlyArray<Team> = [
"backend",
"frontend",
"ux_ui",
] as Team[];
function definitionToAgent(def: AgentDefinition): Agent {
return {
@@ -163,8 +346,13 @@ export function useSpawnAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ agentId, request }: { agentId: string; request?: SpawnAgentRequest }) =>
orchestratorApi.spawn(agentId, request),
mutationFn: ({
agentId,
request,
}: {
agentId: string;
request?: SpawnAgentRequest;
}) => orchestratorApi.spawn(agentId, request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
},
@@ -175,8 +363,13 @@ export function useStopAgent() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ agentId, graceful = true }: { agentId: string; graceful?: boolean }) =>
orchestratorApi.stop(agentId, graceful),
mutationFn: ({
agentId,
graceful = true,
}: {
agentId: string;
graceful?: boolean;
}) => orchestratorApi.stop(agentId, graceful),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
},
@@ -187,8 +380,13 @@ export function useResolveWait() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ agentId, resolution }: { agentId: string; resolution: string }) =>
orchestratorApi.resolveWait(agentId, resolution),
mutationFn: ({
agentId,
resolution,
}: {
agentId: string;
resolution: string;
}) => orchestratorApi.resolveWait(agentId, resolution),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: agentKeys.orchestrator() });
},
+5 -3
View File
@@ -22,9 +22,11 @@ function retryUnlessNotFound(failureCount: number, error: unknown): boolean {
export const channelKeys = {
all: ["channels"] as const,
list: (filters?: ChannelFilters) => [...channelKeys.all, "list", filters] as const,
list: (filters?: ChannelFilters) =>
[...channelKeys.all, "list", filters] as const,
detail: (id: string) => [...channelKeys.all, "detail", id] as const,
groups: (channelId: string) => [...channelKeys.all, "groups", channelId] as const,
groups: (channelId: string) =>
[...channelKeys.all, "groups", channelId] as const,
};
export const sessionKeys = {
@@ -105,7 +107,7 @@ export function useSession(sessionId: string | null) {
relationship_type: link.relationship_type,
};
}
})
}),
);
return {
+9 -7
View File
@@ -62,12 +62,13 @@ export function useMetrics() {
queryKey: dashboardKeys.metrics(),
queryFn: async (): Promise<MetricsSummary> => {
// Fetch all metrics in parallel, including real agent status
const [velocity, blockers, communication, agentStatus] = await Promise.all([
dashboardApi.getVelocityMetrics(),
dashboardApi.getBlockerMetrics(),
dashboardApi.getCommunicationMetrics(),
dashboardApi.getAgentStatus(),
]);
const [velocity, blockers, communication, agentStatus] =
await Promise.all([
dashboardApi.getVelocityMetrics(),
dashboardApi.getBlockerMetrics(),
dashboardApi.getCommunicationMetrics(),
dashboardApi.getAgentStatus(),
]);
return {
velocity,
blockers,
@@ -255,7 +256,8 @@ export function useCreateAuditorFlag() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateFlagRequest) => dashboardApi.createAuditorFlag(data),
mutationFn: (data: CreateFlagRequest) =>
dashboardApi.createAuditorFlag(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
queryClient.invalidateQueries({
+37 -14
View File
@@ -37,7 +37,8 @@ import type {
export const gitKeys = {
all: ["git"] as const,
status: (projectSlug: string) => [...gitKeys.all, "status", projectSlug] as const,
status: (projectSlug: string) =>
[...gitKeys.all, "status", projectSlug] as const,
log: (projectSlug: string, limit?: number, branch?: string) =>
[...gitKeys.all, "log", projectSlug, { limit, branch }] as const,
branches: (projectSlug: string, includeRemote?: boolean) =>
@@ -53,7 +54,11 @@ export const gitKeys = {
/**
* Get git status for a project
*/
export function useGitStatus(projectSlug: string, taskId?: string, enabled: boolean = true) {
export function useGitStatus(
projectSlug: string,
taskId?: string,
enabled: boolean = true,
) {
return useQuery<GitStatusResponse>({
queryKey: gitKeys.status(projectSlug),
queryFn: () => gitApi.getStatus(projectSlug, taskId),
@@ -70,7 +75,7 @@ export function useGitLog(
projectSlug: string,
limit: number = 10,
branch?: string,
enabled: boolean = true
enabled: boolean = true,
) {
return useQuery<GitLogResponse>({
queryKey: gitKeys.log(projectSlug, limit, branch),
@@ -86,7 +91,7 @@ export function useGitLog(
export function useGitBranches(
projectSlug: string,
includeRemote: boolean = false,
enabled: boolean = true
enabled: boolean = true,
) {
return useQuery<GitBranchListResponse>({
queryKey: gitKeys.branches(projectSlug, includeRemote),
@@ -103,7 +108,7 @@ export function useGitDiff(
projectSlug: string,
staged: boolean = false,
filePath?: string,
enabled: boolean = true
enabled: boolean = true,
) {
return useQuery<GitDiffResponse>({
queryKey: gitKeys.diff(projectSlug, staged, filePath),
@@ -127,7 +132,9 @@ export function useGitCommit() {
mutationFn: (request) => gitApi.commit(request),
onSuccess: (_, variables) => {
// Invalidate status and log after commit
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "log", variables.project_slug],
});
@@ -148,7 +155,9 @@ export function useGitPush() {
mutationFn: (request) => gitApi.push(request),
onSuccess: (_, variables) => {
// Invalidate status after push
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
},
});
}
@@ -166,7 +175,9 @@ export function useCreateBranch() {
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "branches", variables.project_slug],
});
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
},
});
}
@@ -181,7 +192,9 @@ export function useCheckout() {
mutationFn: (request) => gitApi.checkout(request),
onSuccess: (_, variables) => {
// Invalidate everything for this project after checkout
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "log", variables.project_slug],
});
@@ -205,7 +218,9 @@ export function useCreatePR() {
mutationFn: (request) => gitApi.createPR(request),
onSuccess: (_, variables) => {
// Invalidate status after PR creation
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
// Also invalidate tasks since PR creation updates task state
queryClient.invalidateQueries({ queryKey: ["tasks"] });
},
@@ -225,7 +240,9 @@ export function useMergePR() {
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "branches", variables.project_slug],
});
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
// Also invalidate tasks since merge updates task state
queryClient.invalidateQueries({ queryKey: ["tasks"] });
},
@@ -242,7 +259,9 @@ export function useGitPull() {
mutationFn: (request) => gitApi.pull(request),
onSuccess: (_, variables) => {
// Invalidate status and log after pull
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "log", variables.project_slug],
});
@@ -260,7 +279,9 @@ export function useGitFetch() {
mutationFn: (request) => gitApi.fetch(request),
onSuccess: (_, variables) => {
// Invalidate status after fetch
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
},
});
}
@@ -275,7 +296,9 @@ export function useGitRebase() {
mutationFn: (request) => gitApi.rebase(request),
onSuccess: (_, variables) => {
// Invalidate everything after rebase since history has changed
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
queryClient.invalidateQueries({
queryKey: gitKeys.status(variables.project_slug),
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "log", variables.project_slug],
});
+1 -1
View File
@@ -71,7 +71,7 @@ export function useAgentJournalEntries(
task_id?: string;
limit?: number;
offset?: number;
}
},
) {
return useQuery<JournalEntry[]>({
queryKey: [...journalKeys.entries(), "agent", agentIdOrSlug, params],
+21 -9
View File
@@ -54,13 +54,18 @@ import type {
export const kbKeys = {
all: ["knowledge-base"] as const,
stats: () => [...kbKeys.all, "stats"] as const,
indexStats: (indexType: KBIndexType) => [...kbKeys.all, "stats", indexType] as const,
indexStats: (indexType: KBIndexType) =>
[...kbKeys.all, "stats", indexType] as const,
health: () => [...kbKeys.all, "health"] as const,
staleness: () => [...kbKeys.all, "staleness"] as const,
search: (query: string, filters?: string) => [...kbKeys.all, "search", query, filters] as const,
documents: (indexType: KBIndexType, params?: string) => [...kbKeys.all, "documents", indexType, params] as const,
learnings: (query: string, filters?: string) => [...kbKeys.all, "learnings", query, filters] as const,
proactiveContext: (taskId: string) => [...kbKeys.all, "proactive-context", taskId] as const,
search: (query: string, filters?: string) =>
[...kbKeys.all, "search", query, filters] as const,
documents: (indexType: KBIndexType, params?: string) =>
[...kbKeys.all, "documents", indexType, params] as const,
learnings: (query: string, filters?: string) =>
[...kbKeys.all, "learnings", query, filters] as const,
proactiveContext: (taskId: string) =>
[...kbKeys.all, "proactive-context", taskId] as const,
};
// =============================================================================
@@ -116,7 +121,7 @@ export function useKBSearch(params: KBSearchRequest, enabled = true) {
export function useKBDocuments(
indexType: KBIndexType,
params?: { limit?: number; offset?: number },
enabled = true
enabled = true,
) {
return useQuery({
queryKey: kbKeys.documents(indexType, JSON.stringify(params)),
@@ -144,7 +149,8 @@ export function useRAGQuery() {
*/
export function useRAGContext() {
return useMutation({
mutationFn: (params: RAGQueryRequest) => knowledgeBaseApi.getContext(params),
mutationFn: (params: RAGQueryRequest) =>
knowledgeBaseApi.getContext(params),
});
}
@@ -338,9 +344,15 @@ export function useRecordLearning() {
/**
* Search learnings (as query for persistent results)
*/
export function useSearchLearnings(request: LearningSearchRequest, enabled = true) {
export function useSearchLearnings(
request: LearningSearchRequest,
enabled = true,
) {
return useQuery<KBSearchResponse>({
queryKey: kbKeys.learnings(request.query, JSON.stringify({ category: request.category, team: request.team })),
queryKey: kbKeys.learnings(
request.query,
JSON.stringify({ category: request.category, team: request.team }),
),
queryFn: () => knowledgeBaseApi.searchLearnings(request),
enabled: enabled && request.query.length >= 3,
staleTime: 1000 * 60 * 2, // 2 minutes
+10 -4
View File
@@ -1,11 +1,15 @@
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { notificationsApi, type NotificationFilters } from "@/lib/api/notifications";
import {
notificationsApi,
type NotificationFilters,
} from "@/lib/api/notifications";
export const notificationKeys = {
all: ["notifications"] as const,
list: (filters?: NotificationFilters) => [...notificationKeys.all, "list", filters] as const,
list: (filters?: NotificationFilters) =>
[...notificationKeys.all, "list", filters] as const,
detail: (id: string) => [...notificationKeys.all, "detail", id] as const,
};
@@ -29,7 +33,8 @@ export function useMarkNotificationRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (notificationId: string) => notificationsApi.markRead(notificationId),
mutationFn: (notificationId: string) =>
notificationsApi.markRead(notificationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
},
@@ -40,7 +45,8 @@ export function useAcknowledgeNotification() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (notificationId: string) => notificationsApi.acknowledge(notificationId),
mutationFn: (notificationId: string) =>
notificationsApi.acknowledge(notificationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
},
+12 -3
View File
@@ -10,11 +10,19 @@ export const productKeys = {
};
export function useProducts() {
return useQuery({ queryKey: productKeys.lists(), queryFn: () => productsApi.list(), staleTime: 60000 });
return useQuery({
queryKey: productKeys.lists(),
queryFn: () => productsApi.list(),
staleTime: 60000,
});
}
export function useProduct(id: string) {
return useQuery({ queryKey: productKeys.detail(id), queryFn: () => productsApi.get(id), enabled: !!id });
return useQuery({
queryKey: productKeys.detail(id),
queryFn: () => productsApi.get(id),
enabled: !!id,
});
}
export function useCreateProduct() {
@@ -28,7 +36,8 @@ export function useCreateProduct() {
export function useUpdateProduct() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, patch }: { id: string; patch: ProductUpdate }) => productsApi.update(id, patch),
mutationFn: ({ id, patch }: { id: string; patch: ProductUpdate }) =>
productsApi.update(id, patch),
onSuccess: (_d, v) => {
qc.invalidateQueries({ queryKey: productKeys.lists() });
qc.invalidateQueries({ queryKey: productKeys.detail(v.id) });
+16 -5
View File
@@ -6,7 +6,8 @@ import type { ProjectCreate, ProjectUpdate } from "@/types";
export const projectKeys = {
all: ["projects"] as const,
lists: () => [...projectKeys.all, "list"] as const,
list: (filters?: ProjectFilters) => [...projectKeys.lists(), filters] as const,
list: (filters?: ProjectFilters) =>
[...projectKeys.lists(), filters] as const,
details: () => [...projectKeys.all, "detail"] as const,
detail: (id: string) => [...projectKeys.details(), id] as const,
};
@@ -43,8 +44,13 @@ export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ projectId, updates }: { projectId: string; updates: ProjectUpdate }) =>
projectsApi.update(projectId, updates),
mutationFn: ({
projectId,
updates,
}: {
projectId: string;
updates: ProjectUpdate;
}) => projectsApi.update(projectId, updates),
onSuccess: (project) => {
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
queryClient.setQueryData(projectKeys.detail(project.id), project);
@@ -56,8 +62,13 @@ export function useSetWorkspace() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ projectId, localPath }: { projectId: string; localPath: string }) =>
projectsApi.setWorkspace(projectId, localPath),
mutationFn: ({
projectId,
localPath,
}: {
projectId: string;
localPath: string;
}) => projectsApi.setWorkspace(projectId, localPath),
onSuccess: (project) => {
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
queryClient.setQueryData(projectKeys.detail(project.id), project);
+9 -3
View File
@@ -18,11 +18,17 @@ export function useRateLimitSync() {
syncFromApi(response);
} catch (err) {
// Endpoint unavailable — treat as no-op per acceptance criteria
const status = (err as { response?: { status?: number } })?.response?.status;
const status = (err as { response?: { status?: number } })?.response
?.status;
if (status === 404) {
console.warn("[rate-limits] GET /api/system/rate-limits returned 404 — endpoint not available");
console.warn(
"[rate-limits] GET /api/system/rate-limits returned 404 — endpoint not available",
);
} else {
console.warn("[rate-limits] GET /api/system/rate-limits unavailable:", err);
console.warn(
"[rate-limits] GET /api/system/rate-limits unavailable:",
err,
);
}
}
}, [syncFromApi]);
+8 -3
View File
@@ -4,7 +4,10 @@ import { useEffect, useRef } from "react";
import { useWebSocket } from "./use-websocket";
import { useRateLimitStore } from "@/store/rate-limit-store";
import { useUsageStore } from "@/store/usage-store";
import type { RateLimitHitEvent, RateLimitLiftedEvent } from "@/types/rate-limits";
import type {
RateLimitHitEvent,
RateLimitLiftedEvent,
} from "@/types/rate-limits";
/**
* Unified shape for all messages arriving on the /ws/system endpoint.
@@ -43,7 +46,9 @@ interface UseRateLimitWebSocketOptions {
* Accepts an optional onReconnect callback that fires when the connection
* recovers from a reconnecting state.
*/
export function useRateLimitWebSocket(options: UseRateLimitWebSocketOptions = {}) {
export function useRateLimitWebSocket(
options: UseRateLimitWebSocketOptions = {},
) {
const { onReconnect } = options;
const prevStateRef = useRef<string | null>(null);
@@ -53,7 +58,7 @@ export function useRateLimitWebSocket(options: UseRateLimitWebSocketOptions = {}
const { state, lastMessage } = useWebSocket<SystemWsMessage>(
"/system",
undefined,
true
true,
);
// Sync WS connection state into useUsageStore for cross-component visibility.
+7 -2
View File
@@ -10,7 +10,9 @@ import { useEffect, useRef } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { useUIStore } from "@/lib/stores/ui-store";
export function useScrollRestoration(scrollContainerRef?: React.RefObject<HTMLElement>) {
export function useScrollRestoration(
scrollContainerRef?: React.RefObject<HTMLElement>,
) {
const pathname = usePathname();
const searchParams = useSearchParams();
const { setScrollPosition, getScrollPosition } = useUIStore();
@@ -27,7 +29,10 @@ export function useScrollRestoration(scrollContainerRef?: React.RefObject<HTMLEl
const handleScroll = () => {
const position = isWindow
? { x: window.scrollX, y: window.scrollY }
: { x: (container as HTMLElement).scrollLeft, y: (container as HTMLElement).scrollTop };
: {
x: (container as HTMLElement).scrollLeft,
y: (container as HTMLElement).scrollTop,
};
setScrollPosition(routeKey, position);
};
+7 -5
View File
@@ -70,10 +70,10 @@ export function useSecretary() {
esRef.current = source;
const listener = (e: MessageEvent) => handleEvent(e.data);
LIVE_EVENT_KINDS.forEach((kind) =>
source.addEventListener(kind, listener as EventListener)
source.addEventListener(kind, listener as EventListener),
);
},
[closeStream, handleEvent]
[closeStream, handleEvent],
);
const start = useCallback(
@@ -81,11 +81,13 @@ export function useSecretary() {
const { session_id } = await secretaryApi.startLive(initialMessage);
setSessionId(session_id);
bufRef.current = "";
setMessages(initialMessage ? [{ role: "user", text: initialMessage }] : []);
setMessages(
initialMessage ? [{ role: "user", text: initialMessage }] : [],
);
openStream(session_id);
return session_id;
},
[openStream]
[openStream],
);
const send = useCallback(
@@ -95,7 +97,7 @@ export function useSecretary() {
setMessages((prev) => [...prev, { role: "user", text }]);
await secretaryApi.sendMessage(sessionId, text);
},
[sessionId]
[sessionId],
);
const stop = useCallback(async (): Promise<void> => {
+61 -20
View File
@@ -22,7 +22,8 @@ export const taskKeys = {
list: (filters?: TaskFilters) => [...taskKeys.lists(), filters] as const,
details: () => [...taskKeys.all, "detail"] as const,
detail: (id: string) => [...taskKeys.details(), id] as const,
subtasks: (parentId: string) => [...taskKeys.all, "subtasks", parentId] as const,
subtasks: (parentId: string) =>
[...taskKeys.all, "subtasks", parentId] as const,
boardReview: (id: string) => [...taskKeys.all, "board-review", id] as const,
stats: () => [...taskKeys.all, "stats"] as const,
statsByTeam: () => [...taskKeys.all, "stats-by-team"] as const,
@@ -110,7 +111,7 @@ export function useTaskStatsByTeam() {
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (task: TaskCreate) => tasksApi.create(task),
onSuccess: () => {
@@ -123,8 +124,13 @@ export function useUpdateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ taskId, updates }: { taskId: string; updates: TaskUpdate }) =>
tasksApi.update(taskId, updates),
mutationFn: ({
taskId,
updates,
}: {
taskId: string;
updates: TaskUpdate;
}) => tasksApi.update(taskId, updates),
onSuccess: (task) => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
queryClient.setQueryData(taskKeys.detail(task.id), task);
@@ -134,7 +140,7 @@ export function useUpdateTask() {
export function useDeleteTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (taskId: string) => tasksApi.delete(taskId),
onSuccess: () => {
@@ -146,7 +152,7 @@ export function useDeleteTask() {
// Lifecycle action hooks
export function useTaskLifecycle() {
const queryClient = useQueryClient();
const invalidateTask = (task: Task) => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
queryClient.setQueryData(taskKeys.detail(task.id), task);
@@ -163,8 +169,13 @@ export function useTaskLifecycle() {
});
const block = useMutation({
mutationFn: ({ taskId, blockerId }: { taskId: string; blockerId?: string }) =>
tasksApi.block(taskId, blockerId),
mutationFn: ({
taskId,
blockerId,
}: {
taskId: string;
blockerId?: string;
}) => tasksApi.block(taskId, blockerId),
onSuccess: invalidateTask,
});
@@ -207,8 +218,13 @@ export function useTaskLifecycle() {
});
const complete = useMutation({
mutationFn: ({ taskId, justification }: { taskId: string; justification: string }) =>
tasksApi.complete(taskId, justification),
mutationFn: ({
taskId,
justification,
}: {
taskId: string;
justification: string;
}) => tasksApi.complete(taskId, justification),
onSuccess: invalidateTask,
});
@@ -242,33 +258,58 @@ export function useTaskLifecycle() {
// Progress tracking
const addProgress = useMutation({
mutationFn: ({ taskId, request }: { taskId: string; request: ProgressRequest }) =>
tasksApi.addProgress(taskId, request),
mutationFn: ({
taskId,
request,
}: {
taskId: string;
request: ProgressRequest;
}) => tasksApi.addProgress(taskId, request),
onSuccess: invalidateTask,
});
const addCheckpoint = useMutation({
mutationFn: ({ taskId, request }: { taskId: string; request: CheckpointRequest }) =>
tasksApi.addCheckpoint(taskId, request),
mutationFn: ({
taskId,
request,
}: {
taskId: string;
request: CheckpointRequest;
}) => tasksApi.addCheckpoint(taskId, request),
onSuccess: invalidateTask,
});
const addCommit = useMutation({
mutationFn: ({ taskId, request }: { taskId: string; request: CommitRequest }) =>
tasksApi.addCommit(taskId, request),
mutationFn: ({
taskId,
request,
}: {
taskId: string;
request: CommitRequest;
}) => tasksApi.addCommit(taskId, request),
onSuccess: invalidateTask,
});
// Soft block and escalation
const softBlock = useMutation({
mutationFn: ({ taskId, request }: { taskId: string; request: SoftBlockRequest }) =>
tasksApi.softBlock(taskId, request),
mutationFn: ({
taskId,
request,
}: {
taskId: string;
request: SoftBlockRequest;
}) => tasksApi.softBlock(taskId, request),
onSuccess: invalidateTask,
});
const escalate = useMutation({
mutationFn: ({ taskId, request }: { taskId: string; request: EscalateRequest }) =>
tasksApi.escalate(taskId, request),
mutationFn: ({
taskId,
request,
}: {
taskId: string;
request: EscalateRequest;
}) => tasksApi.escalate(taskId, request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
},
+12 -6
View File
@@ -20,13 +20,19 @@ import type {
export const usageKeys = {
all: ["usage"] as const,
summary: (period: UsagePeriod) => [...usageKeys.all, "summary", period] as const,
timeSeries: (period: UsagePeriod) => [...usageKeys.all, "time-series", period] as const,
agentUsage: (period: UsagePeriod) => [...usageKeys.all, "by-agent", period] as const,
teamUsage: (period: UsagePeriod) => [...usageKeys.all, "by-team", period] as const,
modelUsage: (period: UsagePeriod) => [...usageKeys.all, "by-model", period] as const,
summary: (period: UsagePeriod) =>
[...usageKeys.all, "summary", period] as const,
timeSeries: (period: UsagePeriod) =>
[...usageKeys.all, "time-series", period] as const,
agentUsage: (period: UsagePeriod) =>
[...usageKeys.all, "by-agent", period] as const,
teamUsage: (period: UsagePeriod) =>
[...usageKeys.all, "by-team", period] as const,
modelUsage: (period: UsagePeriod) =>
[...usageKeys.all, "by-model", period] as const,
projection: () => [...usageKeys.all, "projection"] as const,
cacheEfficiency: (period: UsagePeriod) => [...usageKeys.all, "cache-efficiency", period] as const,
cacheEfficiency: (period: UsagePeriod) =>
[...usageKeys.all, "cache-efficiency", period] as const,
sessions: (limit: number) => [...usageKeys.all, "sessions", limit] as const,
};
+54 -27
View File
@@ -1,9 +1,9 @@
"use client";
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import {
WebSocketConnection,
getWebSocketUrl
import {
WebSocketConnection,
getWebSocketUrl,
} from "@/lib/websocket/connection";
import { CEO_AGENT_ID, STREAM_MAX_MESSAGES } from "@/lib/constants";
@@ -51,7 +51,7 @@ export interface NotificationMessage {
export function useWebSocket<T>(
endpoint: string,
queryParams?: Record<string, string>,
enabled: boolean = true
enabled: boolean = true,
) {
const [state, setState] = useState<ConnectionState>("disconnected");
const [lastMessage, setLastMessage] = useState<T | null>(null);
@@ -59,7 +59,9 @@ export function useWebSocket<T>(
const connectionRef = useRef<WebSocketConnection | null>(null);
// Memoize queryParams string to prevent unnecessary reconnects
const queryString = queryParams ? new URLSearchParams(queryParams).toString() : "";
const queryString = queryParams
? new URLSearchParams(queryParams).toString()
: "";
useEffect(() => {
// Don't connect if disabled or no endpoint
@@ -77,7 +79,10 @@ export function useWebSocket<T>(
onMessage: (data) => {
const message = data as T;
setLastMessage(message);
setMessages((prev) => [...prev.slice(-(STREAM_MAX_MESSAGES - 1)), message]);
setMessages((prev) => [
...prev.slice(-(STREAM_MAX_MESSAGES - 1)),
message,
]);
},
onStateChange: setState,
});
@@ -122,12 +127,18 @@ export function useWebSocket<T>(
* Subscribe to an agent's output stream
*/
export function useAgentStream(agentId: string | null) {
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
useWebSocket<AgentStreamMessage>(
agentId ? "/agents/" + agentId : "",
{ viewer_id: CEO_AGENT_ID },
!!agentId
);
const {
state,
lastMessage,
messages,
clearMessages,
isConnected,
isConnecting,
} = useWebSocket<AgentStreamMessage>(
agentId ? "/agents/" + agentId : "",
{ viewer_id: CEO_AGENT_ID },
!!agentId,
);
// Extract stream chunks
const streamChunks = messages
@@ -153,12 +164,18 @@ export function useAgentStream(agentId: string | null) {
* Subscribe to a channel's message stream
*/
export function useChannelStream(channelId: string | null) {
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
useWebSocket<ChannelMessage>(
channelId ? "/channels/" + channelId : "",
{ agent_id: CEO_AGENT_ID },
!!channelId
);
const {
state,
lastMessage,
messages,
clearMessages,
isConnected,
isConnecting,
} = useWebSocket<ChannelMessage>(
channelId ? "/channels/" + channelId : "",
{ agent_id: CEO_AGENT_ID },
!!channelId,
);
// Filter to only actual messages
const channelMessages = messages.filter((m) => m.type === "message.new");
@@ -178,12 +195,18 @@ export function useChannelStream(channelId: string | null) {
* Subscribe to notifications for the CEO
*/
export function useNotificationStream() {
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
useWebSocket<NotificationMessage>(
"/notifications/" + CEO_AGENT_ID,
undefined,
true
);
const {
state,
lastMessage,
messages,
clearMessages,
isConnected,
isConnecting,
} = useWebSocket<NotificationMessage>(
"/notifications/" + CEO_AGENT_ID,
undefined,
true,
);
// Filter to notification events, de-duplicated by notification_id so a
// stream replay (e.g. after a websocket reconnect) does not surface — or
@@ -223,7 +246,9 @@ export function useNotificationStream() {
// =============================================================================
export function useConnectionStatus() {
const [connections, setConnections] = useState<Record<string, ConnectionState>>({});
const [connections, setConnections] = useState<
Record<string, ConnectionState>
>({});
const updateConnection = useCallback((id: string, state: ConnectionState) => {
setConnections((prev) => ({ ...prev, [id]: state }));
@@ -238,10 +263,12 @@ export function useConnectionStatus() {
}, []);
const hasActiveConnections = Object.values(connections).some(
(s) => s === "connected" || s === "connecting" || s === "reconnecting"
(s) => s === "connected" || s === "connecting" || s === "reconnecting",
);
const allConnected = Object.values(connections).every((s) => s === "connected");
const allConnected = Object.values(connections).every(
(s) => s === "connected",
);
return {
connections,
+46 -14
View File
@@ -1,15 +1,20 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { workSessionsApi, type WorkSessionFilters } from "@/lib/api/work-sessions";
import {
workSessionsApi,
type WorkSessionFilters,
} from "@/lib/api/work-sessions";
import type { WorkSession, WorkSessionCreate } from "@/types";
// Query keys
export const workSessionKeys = {
all: ["work-sessions"] as const,
lists: () => [...workSessionKeys.all, "list"] as const,
list: (filters?: WorkSessionFilters) => [...workSessionKeys.lists(), filters] as const,
list: (filters?: WorkSessionFilters) =>
[...workSessionKeys.lists(), filters] as const,
details: () => [...workSessionKeys.all, "detail"] as const,
detail: (id: string) => [...workSessionKeys.details(), id] as const,
forTask: (taskId: string) => [...workSessionKeys.all, "task", taskId] as const,
forTask: (taskId: string) =>
[...workSessionKeys.all, "task", taskId] as const,
};
// Hooks
@@ -56,19 +61,31 @@ export function useWorkSessionActions() {
queryClient.invalidateQueries({ queryKey: workSessionKeys.lists() });
queryClient.setQueryData(workSessionKeys.detail(session.id), session);
if (session.task_id) {
queryClient.invalidateQueries({ queryKey: workSessionKeys.forTask(session.task_id) });
queryClient.invalidateQueries({
queryKey: workSessionKeys.forTask(session.task_id),
});
}
};
const addCommit = useMutation({
mutationFn: ({ sessionId, commitSha }: { sessionId: string; commitSha: string }) =>
workSessionsApi.addCommit(sessionId, commitSha),
mutationFn: ({
sessionId,
commitSha,
}: {
sessionId: string;
commitSha: string;
}) => workSessionsApi.addCommit(sessionId, commitSha),
onSuccess: invalidateSession,
});
const addFiles = useMutation({
mutationFn: ({ sessionId, filePaths }: { sessionId: string; filePaths: string[] }) =>
workSessionsApi.addFiles(sessionId, filePaths),
mutationFn: ({
sessionId,
filePaths,
}: {
sessionId: string;
filePaths: string[];
}) => workSessionsApi.addFiles(sessionId, filePaths),
onSuccess: invalidateSession,
});
@@ -86,14 +103,24 @@ export function useWorkSessionActions() {
});
const updatePRStatus = useMutation({
mutationFn: ({ sessionId, prStatus }: { sessionId: string; prStatus: string }) =>
workSessionsApi.updatePRStatus(sessionId, prStatus),
mutationFn: ({
sessionId,
prStatus,
}: {
sessionId: string;
prStatus: string;
}) => workSessionsApi.updatePRStatus(sessionId, prStatus),
onSuccess: invalidateSession,
});
const mergePR = useMutation({
mutationFn: ({ sessionId, mergedBy }: { sessionId: string; mergedBy: string }) =>
workSessionsApi.mergePR(sessionId, mergedBy),
mutationFn: ({
sessionId,
mergedBy,
}: {
sessionId: string;
mergedBy: string;
}) => workSessionsApi.mergePR(sessionId, mergedBy),
onSuccess: invalidateSession,
});
@@ -103,8 +130,13 @@ export function useWorkSessionActions() {
});
const abandon = useMutation({
mutationFn: ({ sessionId, reason }: { sessionId: string; reason?: string }) =>
workSessionsApi.abandon(sessionId, reason),
mutationFn: ({
sessionId,
reason,
}: {
sessionId: string;
reason?: string;
}) => workSessionsApi.abandon(sessionId, reason),
onSuccess: invalidateSession,
});