I mean, it's at a good place rn...

This commit is contained in:
Renn F
2026-04-20 15:10:54 +02:00
parent 0023c25d60
commit 8e201901c0
264 changed files with 36484 additions and 748 deletions
+35
View File
@@ -0,0 +1,35 @@
export * from "./use-tasks";
export * from "./use-agents";
export * from "./use-channels";
export * from "./use-notifications";
// Re-export dashboard hooks excluding duplicates from use-agents
export {
dashboardKeys,
useCeoOverview,
useMetrics,
useVelocityMetrics,
useBlockerMetrics,
useKanbanDev,
useKanbanQa,
useKanbanPm,
useCeoTeamDetails,
useCeoBlockerDetails,
useCeoVelocity,
useRecentActivity,
useAgentStatus as useDashboardAgentStatus, // Renamed to avoid conflict
useKanbanDocumenter,
useKanbanCellPm,
useKanbanBoard,
useKanbanStats,
useAuditorDashboard,
useAuditorFlags,
useAuditorReports,
useCreateAuditorFlag,
useResolveAuditorFlag,
useCreateAuditorReport,
useSendAuditorReport,
} from "./use-dashboard";
export * from "./use-websocket";
export * from "./use-journals";
export * from "./use-projects";
export * from "./use-work-sessions";
+154
View File
@@ -0,0 +1,154 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { orchestratorApi, type SpawnAgentRequest } from "@/lib/api/orchestrator";
import { agentsApi } from "@/lib/api/agents";
import type { Agent, AgentRole, Team, AgentState } from "@/types";
export type { AgentDefinition } from "@/lib/api/agents";
// Static agent roster for RoboCo (18 agents)
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 },
// 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 },
{ id: "16", 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
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
});
}
// Hooks
// Returns the static agent roster (optionally enriched with live status)
export function useAgents() {
const { data: orchestratorStatus } = useOrchestratorStatus();
return useQuery({
queryKey: [...agentKeys.all, "roster"],
queryFn: async (): Promise<Agent[]> => {
// Build a map of agent statuses from the agents array
const statusMap = new Map<string, string>();
if (orchestratorStatus?.agents) {
for (const agentStatus of orchestratorStatus.agents) {
statusMap.set(agentStatus.agent_id, agentStatus.state);
}
}
// Enrich static roster with live status from orchestrator
return AGENT_ROSTER.map((agent) => {
const liveState = statusMap.get(agent.agent_id);
return {
...agent,
status: (liveState as AgentState) ?? agent.status,
};
});
},
staleTime: Infinity, // Static data, only updates when orchestrator updates
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
});
}
+119
View File
@@ -0,0 +1,119 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { channelsApi, type ChannelFilters } from "@/lib/api/channels";
import { sessionsApi } from "@/lib/api/sessions";
import { messagesApi } from "@/lib/api/messages";
import { tasksApi } from "@/lib/api/tasks";
export const channelKeys = {
all: ["channels"] 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,
};
export const sessionKeys = {
all: ["sessions"] as const,
list: (groupId: string) => [...sessionKeys.all, "list", groupId] as const,
detail: (id: string) => [...sessionKeys.all, "detail", id] as const,
};
export const messageKeys = {
all: ["messages"] as const,
list: (sessionId: string) => [...messageKeys.all, "list", sessionId] as const,
};
// Fetch channel list once - manual refresh available
export function useChannels(filters?: ChannelFilters) {
return useQuery({
queryKey: channelKeys.list(filters),
queryFn: () => channelsApi.list(filters),
staleTime: Infinity,
});
}
export function useChannel(channelId: string | null) {
return useQuery({
queryKey: channelKeys.detail(channelId || ""),
queryFn: () => channelsApi.get(channelId!),
enabled: !!channelId,
staleTime: Infinity,
});
}
// Fetch groups for a channel
export function useChannelGroups(channelId: string | null) {
return useQuery({
queryKey: channelKeys.groups(channelId || ""),
queryFn: () => channelsApi.getGroups(channelId!),
enabled: !!channelId,
staleTime: Infinity,
});
}
// Fetch sessions for a group
export function useGroupSessions(groupId: string | null) {
return useQuery({
queryKey: sessionKeys.list(groupId || ""),
queryFn: () => sessionsApi.listByGroup(groupId!),
enabled: !!groupId,
staleTime: Infinity,
});
}
// Fetch a single session by ID (with task links and task titles)
export function useSession(sessionId: string | null) {
return useQuery({
queryKey: sessionKeys.detail(sessionId || ""),
queryFn: async () => {
const session = await sessionsApi.get(sessionId!);
// Fetch task links separately since the endpoint doesn't include them
try {
const taskLinks = await sessionsApi.getTasksForSession(sessionId!);
// Fetch task details to get titles
const taskLinksWithTitles = await Promise.all(
taskLinks.map(async (link) => {
try {
const task = await tasksApi.get(link.task_id);
return {
task_id: link.task_id,
task_title: task.title,
is_primary: link.is_primary,
relationship_type: link.relationship_type,
};
} catch {
return {
task_id: link.task_id,
task_title: null,
is_primary: link.is_primary,
relationship_type: link.relationship_type,
};
}
})
);
return {
...session,
task_links: taskLinksWithTitles,
};
} catch {
// If fetching task links fails, return session without them
return session;
}
},
enabled: !!sessionId,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// Fetch messages for a session - WebSocket handles new messages
export function useSessionMessages(sessionId: string | null) {
return useQuery({
queryKey: messageKeys.list(sessionId || ""),
queryFn: () => messagesApi.listBySession(sessionId!),
enabled: !!sessionId,
staleTime: Infinity,
});
}
+312
View File
@@ -0,0 +1,312 @@
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
dashboardApi,
MetricsSummary,
CreateFlagRequest,
CreateReportRequest,
} from "@/lib/api/dashboard";
import type {
Team,
AuditorDashboard,
AuditorFlag,
AuditorReport,
FlagSeverity,
} from "@/types";
export const dashboardKeys = {
all: ["dashboard"] as const,
ceoOverview: () => [...dashboardKeys.all, "ceo-overview"] as const,
ceoTeams: () => [...dashboardKeys.all, "ceo-teams"] as const,
ceoBlockers: () => [...dashboardKeys.all, "ceo-blockers"] as const,
ceoVelocity: (days: number) =>
[...dashboardKeys.all, "ceo-velocity", days] as const,
metrics: () => [...dashboardKeys.all, "metrics"] as const,
velocity: () => [...dashboardKeys.all, "velocity"] as const,
blockers: () => [...dashboardKeys.all, "blockers"] as const,
activity: (hours: number) =>
[...dashboardKeys.all, "activity", hours] as const,
agentStatus: () => [...dashboardKeys.all, "agent-status"] as const,
// Kanban keys
kanbanDev: (team: Team) =>
[...dashboardKeys.all, "kanban", "dev", team] as const,
kanbanQa: (team: Team) =>
[...dashboardKeys.all, "kanban", "qa", team] as const,
kanbanDocumenter: (team: Team) =>
[...dashboardKeys.all, "kanban", "documenter", team] as const,
kanbanCellPm: (team: Team) =>
[...dashboardKeys.all, "kanban", "cell-pm", team] as const,
kanbanPm: () => [...dashboardKeys.all, "kanban", "pm"] as const,
kanbanBoard: () => [...dashboardKeys.all, "kanban", "board"] as const,
kanbanStats: (team?: Team) =>
[...dashboardKeys.all, "kanban", "stats", team] as const,
// Auditor keys
auditor: () => [...dashboardKeys.all, "auditor"] as const,
auditorFlags: (params?: { severity?: FlagSeverity; resolved?: boolean }) =>
[...dashboardKeys.all, "auditor", "flags", params] as const,
auditorReports: (params?: { report_type?: string; limit?: number }) =>
[...dashboardKeys.all, "auditor", "reports", params] as const,
};
export function useCeoOverview() {
return useQuery({
queryKey: dashboardKeys.ceoOverview(),
queryFn: () => dashboardApi.getCeoOverview(),
refetchInterval: 60000, // Refetch every minute
});
}
export function useMetrics() {
return useQuery({
queryKey: dashboardKeys.metrics(),
queryFn: async (): Promise<MetricsSummary> => {
// Fetch all metrics in parallel
const [velocity, blockers, communication] = await Promise.all([
dashboardApi.getVelocityMetrics(),
dashboardApi.getBlockerMetrics(),
dashboardApi.getCommunicationMetrics(),
]);
return {
velocity,
blockers,
communication,
agents: { total_agents: 0, running: 0, idle: 0, waiting: 0, errors: 0 },
};
},
refetchInterval: 60000,
});
}
export function useVelocityMetrics() {
return useQuery({
queryKey: dashboardKeys.velocity(),
queryFn: () => dashboardApi.getVelocityMetrics(),
refetchInterval: 60000,
});
}
export function useBlockerMetrics() {
return useQuery({
queryKey: dashboardKeys.blockers(),
queryFn: () => dashboardApi.getBlockerMetrics(),
refetchInterval: 60000,
});
}
export function useKanbanDev(team: Team) {
return useQuery({
queryKey: dashboardKeys.kanbanDev(team),
queryFn: () => dashboardApi.getKanbanDev(team),
refetchInterval: 30000,
});
}
export function useKanbanQa(team: Team) {
return useQuery({
queryKey: dashboardKeys.kanbanQa(team),
queryFn: () => dashboardApi.getKanbanQa(team),
refetchInterval: 30000,
});
}
export function useKanbanPm() {
return useQuery({
queryKey: dashboardKeys.kanbanPm(),
queryFn: () => dashboardApi.getKanbanPm(),
refetchInterval: 30000,
});
}
// =============================================================================
// ADDITIONAL CEO HOOKS
// =============================================================================
export function useCeoTeamDetails() {
return useQuery({
queryKey: dashboardKeys.ceoTeams(),
queryFn: () => dashboardApi.getCeoTeamDetails(),
refetchInterval: 60000,
});
}
export function useCeoBlockerDetails() {
return useQuery({
queryKey: dashboardKeys.ceoBlockers(),
queryFn: () => dashboardApi.getCeoBlockerDetails(),
refetchInterval: 60000,
});
}
export function useCeoVelocity(days: number = 7) {
return useQuery({
queryKey: dashboardKeys.ceoVelocity(days),
queryFn: () => dashboardApi.getCeoVelocity(days),
refetchInterval: 60000,
});
}
export function useRecentActivity(hours: number = 24) {
return useQuery({
queryKey: dashboardKeys.activity(hours),
queryFn: () => dashboardApi.getRecentActivity(hours),
refetchInterval: 30000,
});
}
export function useAgentStatus() {
return useQuery({
queryKey: dashboardKeys.agentStatus(),
queryFn: () => dashboardApi.getAgentStatus(),
refetchInterval: 10000,
});
}
// =============================================================================
// ADDITIONAL KANBAN HOOKS
// =============================================================================
export function useKanbanDocumenter(team: Team) {
return useQuery({
queryKey: dashboardKeys.kanbanDocumenter(team),
queryFn: () => dashboardApi.getKanbanDocumenter(team),
refetchInterval: 30000,
});
}
export function useKanbanCellPm(team: Team) {
return useQuery({
queryKey: dashboardKeys.kanbanCellPm(team),
queryFn: () => dashboardApi.getKanbanCellPm(team),
refetchInterval: 30000,
});
}
export function useKanbanBoard() {
return useQuery({
queryKey: dashboardKeys.kanbanBoard(),
queryFn: () => dashboardApi.getKanbanBoard(),
refetchInterval: 30000,
});
}
export function useKanbanStats(team?: Team) {
return useQuery({
queryKey: dashboardKeys.kanbanStats(team),
queryFn: () => dashboardApi.getKanbanStats(team),
refetchInterval: 60000,
});
}
// =============================================================================
// AUDITOR DASHBOARD HOOKS
// =============================================================================
/**
* Get the complete auditor dashboard
*/
export function useAuditorDashboard() {
return useQuery<AuditorDashboard>({
queryKey: dashboardKeys.auditor(),
queryFn: () => dashboardApi.getAuditorDashboard(),
refetchInterval: 30000,
});
}
/**
* Get auditor flags with optional filters
*/
export function useAuditorFlags(params?: {
severity?: FlagSeverity;
resolved?: boolean;
}) {
return useQuery<AuditorFlag[]>({
queryKey: dashboardKeys.auditorFlags(params),
queryFn: () => dashboardApi.getAuditorFlags(params),
refetchInterval: 30000,
});
}
/**
* Get auditor reports
*/
export function useAuditorReports(params?: {
report_type?: string;
limit?: number;
}) {
return useQuery<AuditorReport[]>({
queryKey: dashboardKeys.auditorReports(params),
queryFn: () => dashboardApi.getAuditorReports(params),
refetchInterval: 60000,
});
}
/**
* Create a new auditor flag
*/
export function useCreateAuditorFlag() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateFlagRequest) => dashboardApi.createAuditorFlag(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
queryClient.invalidateQueries({
queryKey: dashboardKeys.auditorFlags(),
});
},
});
}
/**
* Resolve an auditor flag
*/
export function useResolveAuditorFlag() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ flagId, notes }: { flagId: string; notes?: string }) =>
dashboardApi.resolveAuditorFlag(flagId, notes),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
queryClient.invalidateQueries({
queryKey: dashboardKeys.auditorFlags(),
});
},
});
}
/**
* Create a new auditor report
*/
export function useCreateAuditorReport() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateReportRequest) =>
dashboardApi.createAuditorReport(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
queryClient.invalidateQueries({
queryKey: dashboardKeys.auditorReports(),
});
},
});
}
/**
* Send an auditor report to CEO
*/
export function useSendAuditorReport() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (reportId: string) => dashboardApi.sendAuditorReport(reportId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.auditor() });
queryClient.invalidateQueries({
queryKey: dashboardKeys.auditorReports(),
});
},
});
}
+252
View File
@@ -0,0 +1,252 @@
/**
* Git React Query Hooks
*
* React Query hooks for Git operations.
*/
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { gitApi } from "@/lib/api/git";
import type {
GitStatusResponse,
GitLogResponse,
GitBranchListResponse,
GitDiffResponse,
GitCommitRequest,
GitCommitResponse,
GitPushRequest,
GitPushResponse,
GitCreateBranchRequest,
GitCreateBranchResponse,
GitCheckoutRequest,
GitCheckoutResponse,
GitCreatePRRequest,
GitCreatePRResponse,
GitMergePRRequest,
GitMergePRResponse,
} from "@/types/git";
// =============================================================================
// Query Keys
// =============================================================================
export const gitKeys = {
all: ["git"] 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) =>
[...gitKeys.all, "branches", projectSlug, { includeRemote }] as const,
diff: (projectSlug: string, staged?: boolean, filePath?: string) =>
[...gitKeys.all, "diff", projectSlug, { staged, filePath }] as const,
};
// =============================================================================
// Query Hooks
// =============================================================================
/**
* Get git status for a project
*/
export function useGitStatus(projectSlug: string, taskId?: string, enabled: boolean = true) {
return useQuery<GitStatusResponse>({
queryKey: gitKeys.status(projectSlug),
queryFn: () => gitApi.getStatus(projectSlug, taskId),
enabled: enabled && !!projectSlug,
staleTime: 10000, // 10 seconds - status changes frequently
refetchInterval: 30000, // Auto-refresh every 30s
});
}
/**
* Get git log for a project
*/
export function useGitLog(
projectSlug: string,
limit: number = 10,
branch?: string,
enabled: boolean = true
) {
return useQuery<GitLogResponse>({
queryKey: gitKeys.log(projectSlug, limit, branch),
queryFn: () => gitApi.getLog(projectSlug, limit, branch),
enabled: enabled && !!projectSlug,
staleTime: 30000, // 30 seconds
});
}
/**
* Get git branches for a project
*/
export function useGitBranches(
projectSlug: string,
includeRemote: boolean = false,
enabled: boolean = true
) {
return useQuery<GitBranchListResponse>({
queryKey: gitKeys.branches(projectSlug, includeRemote),
queryFn: () => gitApi.getBranches(projectSlug, includeRemote),
enabled: enabled && !!projectSlug,
staleTime: 60000, // 1 minute
});
}
/**
* Get git diff for a project
*/
export function useGitDiff(
projectSlug: string,
staged: boolean = false,
filePath?: string,
enabled: boolean = true
) {
return useQuery<GitDiffResponse>({
queryKey: gitKeys.diff(projectSlug, staged, filePath),
queryFn: () => gitApi.getDiff(projectSlug, staged, filePath),
enabled: enabled && !!projectSlug,
staleTime: 10000, // 10 seconds - diff can change frequently
});
}
// =============================================================================
// Mutation Hooks
// =============================================================================
/**
* Create a git commit
*/
export function useGitCommit() {
const queryClient = useQueryClient();
return useMutation<GitCommitResponse, Error, GitCommitRequest>({
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.all, "log", variables.project_slug],
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "diff", variables.project_slug],
});
},
});
}
/**
* Push commits to remote
*/
export function useGitPush() {
const queryClient = useQueryClient();
return useMutation<GitPushResponse, Error, GitPushRequest>({
mutationFn: (request) => gitApi.push(request),
onSuccess: (_, variables) => {
// Invalidate status after push
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
},
});
}
/**
* Create a task branch
*/
export function useCreateBranch() {
const queryClient = useQueryClient();
return useMutation<GitCreateBranchResponse, Error, GitCreateBranchRequest>({
mutationFn: (request) => gitApi.createBranch(request),
onSuccess: (_, variables) => {
// Invalidate branches after creating a new one
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "branches", variables.project_slug],
});
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
},
});
}
/**
* Checkout a branch
*/
export function useCheckout() {
const queryClient = useQueryClient();
return useMutation<GitCheckoutResponse, Error, GitCheckoutRequest>({
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.all, "log", variables.project_slug],
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "diff", variables.project_slug],
});
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "branches", variables.project_slug],
});
},
});
}
/**
* Create a pull request
*/
export function useCreatePR() {
const queryClient = useQueryClient();
return useMutation<GitCreatePRResponse, Error, GitCreatePRRequest>({
mutationFn: (request) => gitApi.createPR(request),
onSuccess: (_, variables) => {
// Invalidate status after PR creation
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
// Also invalidate tasks since PR creation updates task state
queryClient.invalidateQueries({ queryKey: ["tasks"] });
},
});
}
/**
* Merge a pull request
*/
export function useMergePR() {
const queryClient = useQueryClient();
return useMutation<GitMergePRResponse, Error, GitMergePRRequest>({
mutationFn: (request) => gitApi.mergePR(request),
onSuccess: (_, variables) => {
// Invalidate branches after merge
queryClient.invalidateQueries({
queryKey: [...gitKeys.all, "branches", variables.project_slug],
});
queryClient.invalidateQueries({ queryKey: gitKeys.status(variables.project_slug) });
// Also invalidate tasks since merge updates task state
queryClient.invalidateQueries({ queryKey: ["tasks"] });
},
});
}
// =============================================================================
// Bundled Hook for Git Operations
// =============================================================================
/**
* Bundled hook for all git write operations
*/
export function useGitOperations() {
const commit = useGitCommit();
const push = useGitPush();
const createBranch = useCreateBranch();
const checkout = useCheckout();
const createPR = useCreatePR();
const mergePR = useMergePR();
return {
commit,
push,
createBranch,
checkout,
createPR,
mergePR,
};
}
+273
View File
@@ -0,0 +1,273 @@
/**
* Journals Hooks
*
* React Query hooks for journal operations.
*/
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { journalsApi } from "@/lib/api/journals";
import type {
Journal,
JournalEntry,
JournalEntryCreate,
JournalEntryType,
JournalStats,
GrowthMetrics,
} from "@/types";
// =============================================================================
// QUERY KEYS
// =============================================================================
export const journalKeys = {
all: ["journals"] as const,
myJournal: () => [...journalKeys.all, "my"] as const,
journal: (agentId: string) => [...journalKeys.all, agentId] as const,
entries: () => [...journalKeys.all, "entries"] as const,
myEntries: (params?: { entry_type?: JournalEntryType; task_id?: string }) =>
[...journalKeys.entries(), "my", params] as const,
entry: (entryId: string) => [...journalKeys.entries(), entryId] as const,
stats: () => [...journalKeys.all, "stats"] as const,
myStats: () => [...journalKeys.stats(), "my"] as const,
growth: () => [...journalKeys.all, "growth"] as const,
myGrowth: () => [...journalKeys.growth(), "my"] as const,
search: (query: string) => [...journalKeys.all, "search", query] as const,
};
// =============================================================================
// JOURNAL QUERIES
// =============================================================================
/**
* Get the current agent's journal
*/
export function useMyJournal() {
return useQuery<Journal>({
queryKey: journalKeys.myJournal(),
queryFn: journalsApi.getMyJournal,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
/**
* Get a journal by agent ID or slug (e.g., "be-dev-1")
*/
export function useJournalByAgent(agentIdOrSlug: string, enabled = true) {
return useQuery<Journal>({
queryKey: journalKeys.journal(agentIdOrSlug),
queryFn: () => journalsApi.getJournalByAgent(agentIdOrSlug),
enabled: enabled && !!agentIdOrSlug,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
/**
* List entries for a specific agent by ID or slug
*/
export function useAgentJournalEntries(
agentIdOrSlug: string,
params?: {
entry_type?: JournalEntryType;
task_id?: string;
limit?: number;
offset?: number;
}
) {
return useQuery<JournalEntry[]>({
queryKey: [...journalKeys.entries(), "agent", agentIdOrSlug, params],
queryFn: () => journalsApi.listAgentEntries(agentIdOrSlug, params),
enabled: !!agentIdOrSlug,
staleTime: 1000 * 60 * 2, // 2 minutes
});
}
// =============================================================================
// ENTRY QUERIES
// =============================================================================
/**
* List current agent's journal entries
*/
export function useMyJournalEntries(params?: {
entry_type?: JournalEntryType;
task_id?: string;
limit?: number;
offset?: number;
}) {
return useQuery<JournalEntry[]>({
queryKey: journalKeys.myEntries(params),
queryFn: () => journalsApi.listMyEntries(params),
staleTime: 1000 * 60 * 2, // 2 minutes
});
}
/**
* Get a specific journal entry
*/
export function useJournalEntry(entryId: string, enabled = true) {
return useQuery<JournalEntry>({
queryKey: journalKeys.entry(entryId),
queryFn: () => journalsApi.getEntry(entryId),
enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// =============================================================================
// ANALYTICS QUERIES
// =============================================================================
/**
* Get journal statistics
*/
export function useMyJournalStats() {
return useQuery<JournalStats>({
queryKey: journalKeys.myStats(),
queryFn: journalsApi.getMyStats,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
/**
* Get growth metrics
*/
export function useMyGrowthMetrics() {
return useQuery<GrowthMetrics>({
queryKey: journalKeys.myGrowth(),
queryFn: journalsApi.getMyGrowthMetrics,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// =============================================================================
// MUTATIONS
// =============================================================================
/**
* Create a new journal entry
*/
export function useCreateJournalEntry() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: JournalEntryCreate) => journalsApi.createEntry(data),
onSuccess: () => {
// Invalidate entries and stats
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
queryClient.invalidateQueries({ queryKey: journalKeys.myJournal() });
},
});
}
/**
* Delete a journal entry
*/
export function useDeleteJournalEntry() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (entryId: string) => journalsApi.deleteEntry(entryId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
queryClient.invalidateQueries({ queryKey: journalKeys.myJournal() });
},
});
}
/**
* Add a task reflection
*/
export function useAddTaskReflection() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof journalsApi.addTaskReflection>[0]) =>
journalsApi.addTaskReflection(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
},
});
}
/**
* Add a decision log
*/
export function useAddDecisionLog() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof journalsApi.addDecisionLog>[0]) =>
journalsApi.addDecisionLog(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
},
});
}
/**
* Add a learning entry
*/
export function useAddLearning() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof journalsApi.addLearning>[0]) =>
journalsApi.addLearning(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
},
});
}
/**
* Add a struggle entry
*/
export function useAddStruggle() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof journalsApi.addStruggle>[0]) =>
journalsApi.addStruggle(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
queryClient.invalidateQueries({ queryKey: journalKeys.growth() });
},
});
}
/**
* Add a general note
*/
export function useAddNote() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: Parameters<typeof journalsApi.addNote>[0]) =>
journalsApi.addNote(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: journalKeys.entries() });
queryClient.invalidateQueries({ queryKey: journalKeys.stats() });
},
});
}
/**
* Search journal entries
*/
export function useSearchJournalEntries(query: string, topK = 10) {
return useQuery<JournalEntry[]>({
queryKey: journalKeys.search(query),
queryFn: () => journalsApi.searchEntries(query, topK),
enabled: query.length > 2, // Only search if query is meaningful
staleTime: 1000 * 60 * 2, // 2 minutes
});
}
+377
View File
@@ -0,0 +1,377 @@
"use client";
/**
* Knowledge Base Hooks
*
* React Query hooks for KB search and RAG operations.
*/
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { knowledgeBaseApi } from "@/lib/api/knowledge-base";
import type {
KBSearchRequest,
KBSearchResponse,
RAGQueryRequest,
RAGQueryResponse,
KBStats,
KBIndexType,
KBIndexStats,
RAGHealthResponse,
MentorAskRequest,
MentorAskResponse,
ErrorSearchRequest,
ErrorSearchResponse,
ErrorRecordRequest,
ErrorRecordResponse,
DecisionCheckRequest,
DecisionCheckResponse,
DecisionRecordRequest,
DecisionRecordResponse,
StandardsGetRequest,
StandardsGetResponse,
ValidateActionRequest,
ValidateActionResponse,
CodeReviewRequest,
CodeReviewResponse,
LearningRecordRequest,
LearningRecordResponse,
LearningSearchRequest,
ProactiveContextResponse,
TokenEstimateRequest,
TokenEstimateResponse,
RefreshIndexRequest,
RefreshIndexResponse,
ClearIndexResponse,
ReindexResponse,
ReindexRequest,
IndexStalenessResponse,
} from "@/types";
// =============================================================================
// QUERY KEYS
// =============================================================================
export const kbKeys = {
all: ["knowledge-base"] as const,
stats: () => [...kbKeys.all, "stats"] 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,
};
// =============================================================================
// STATS QUERIES
// =============================================================================
/**
* Get KB index statistics
*/
export function useKBStats() {
return useQuery<KBStats>({
queryKey: kbKeys.stats(),
queryFn: () => knowledgeBaseApi.getStats(),
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
});
}
/**
* Get stats for a specific index type
*/
export function useKBIndexStats(indexType: KBIndexType) {
return useQuery<KBIndexStats>({
queryKey: kbKeys.indexStats(indexType),
queryFn: () => knowledgeBaseApi.getIndexStats(indexType),
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// =============================================================================
// SEARCH QUERIES
// =============================================================================
/**
* Semantic search (enabled only when query has content)
*/
export function useKBSearch(params: KBSearchRequest, enabled = true) {
return useQuery<KBSearchResponse>({
queryKey: kbKeys.search(params.query, JSON.stringify(params.index_types)),
queryFn: () => knowledgeBaseApi.search(params),
enabled: enabled && params.query.length >= 3,
staleTime: 1000 * 60 * 2, // 2 minutes
});
}
// =============================================================================
// BROWSE QUERIES
// =============================================================================
/**
* List documents in a specific index
*/
export function useKBDocuments(
indexType: KBIndexType,
params?: { limit?: number; offset?: number },
enabled = true
) {
return useQuery({
queryKey: kbKeys.documents(indexType, JSON.stringify(params)),
queryFn: () => knowledgeBaseApi.listDocuments(indexType, params),
enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// =============================================================================
// MUTATIONS
// =============================================================================
/**
* RAG Query - mutation because it triggers AI generation
*/
export function useRAGQuery() {
return useMutation<RAGQueryResponse, Error, RAGQueryRequest>({
mutationFn: (params) => knowledgeBaseApi.ragQuery(params),
});
}
/**
* Get context without generating answer
*/
export function useRAGContext() {
return useMutation({
mutationFn: (params: RAGQueryRequest) => knowledgeBaseApi.getContext(params),
});
}
// =============================================================================
// HEALTH QUERY
// =============================================================================
/**
* Get RAG system health status
*/
export function useRAGHealth() {
return useQuery<RAGHealthResponse>({
queryKey: kbKeys.health(),
queryFn: () => knowledgeBaseApi.getHealth(),
staleTime: 1000 * 60, // 1 minute
refetchInterval: 1000 * 60 * 5, // Check every 5 minutes
});
}
// =============================================================================
// INDEX MANAGEMENT MUTATIONS
// =============================================================================
/**
* Delete/clear an index
*/
export function useDeleteIndex() {
const queryClient = useQueryClient();
return useMutation<ClearIndexResponse, Error, KBIndexType>({
mutationFn: (indexType) => knowledgeBaseApi.deleteIndex(indexType),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: kbKeys.stats() });
},
});
}
/**
* Refresh an index
*/
export function useRefreshIndex() {
const queryClient = useQueryClient();
return useMutation<RefreshIndexResponse, Error, RefreshIndexRequest>({
mutationFn: (request) => knowledgeBaseApi.refreshIndex(request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: kbKeys.stats() });
},
});
}
/**
* Reindex all with detailed reporting
*
* Returns detailed IndexingReport for both code and documentation,
* including success/failure counts and failed file paths.
*/
export function useReindexAll() {
const queryClient = useQueryClient();
return useMutation<ReindexResponse, Error, ReindexRequest | undefined>({
mutationFn: (request) => knowledgeBaseApi.reindexAll(request),
onSuccess: () => {
// Invalidate all relevant queries after reindexing
queryClient.invalidateQueries({ queryKey: kbKeys.stats() });
queryClient.invalidateQueries({ queryKey: kbKeys.staleness() });
queryClient.invalidateQueries({ queryKey: kbKeys.health() });
},
});
}
/**
* Check if indexes are stale (source files modified after indexing)
*
* Use this to show a "Reindex recommended" warning in the UI.
*/
export function useIndexStaleness() {
return useQuery<IndexStalenessResponse>({
queryKey: kbKeys.staleness(),
queryFn: () => knowledgeBaseApi.checkStaleness(),
staleTime: 5 * 60 * 1000, // 5 minutes
refetchInterval: 10 * 60 * 1000, // Check every 10 minutes
});
}
// =============================================================================
// MENTOR MUTATIONS
// =============================================================================
/**
* Ask the mentor for help
*/
export function useAskMentor() {
return useMutation<MentorAskResponse, Error, MentorAskRequest>({
mutationFn: (request) => knowledgeBaseApi.askMentor(request),
});
}
// =============================================================================
// ERROR MUTATIONS
// =============================================================================
/**
* Search for known error solutions
*/
export function useSearchErrors() {
return useMutation<ErrorSearchResponse, Error, ErrorSearchRequest>({
mutationFn: (request) => knowledgeBaseApi.searchErrors(request),
});
}
/**
* Record an error solution
*/
export function useRecordError() {
return useMutation<ErrorRecordResponse, Error, ErrorRecordRequest>({
mutationFn: (request) => knowledgeBaseApi.recordError(request),
});
}
// =============================================================================
// DECISION MUTATIONS
// =============================================================================
/**
* Check if a similar decision was made before
*/
export function useCheckDecision() {
return useMutation<DecisionCheckResponse, Error, DecisionCheckRequest>({
mutationFn: (request) => knowledgeBaseApi.checkDecision(request),
});
}
/**
* Record a decision for future reference
*/
export function useRecordDecision() {
return useMutation<DecisionRecordResponse, Error, DecisionRecordRequest>({
mutationFn: (request) => knowledgeBaseApi.recordDecision(request),
});
}
// =============================================================================
// STANDARDS MUTATIONS
// =============================================================================
/**
* Get standards for a domain
*/
export function useGetStandards() {
return useMutation<StandardsGetResponse, Error, StandardsGetRequest>({
mutationFn: (request) => knowledgeBaseApi.getStandards(request),
});
}
/**
* Validate an action against standards
*/
export function useValidateAction() {
return useMutation<ValidateActionResponse, Error, ValidateActionRequest>({
mutationFn: (request) => knowledgeBaseApi.validateAction(request),
});
}
// =============================================================================
// CODE REVIEW MUTATIONS
// =============================================================================
/**
* Review code against standards
*/
export function useReviewCode() {
return useMutation<CodeReviewResponse, Error, CodeReviewRequest>({
mutationFn: (request) => knowledgeBaseApi.reviewCode(request),
});
}
// =============================================================================
// LEARNING HOOKS
// =============================================================================
/**
* Record a learning
*/
export function useRecordLearning() {
return useMutation<LearningRecordResponse, Error, LearningRecordRequest>({
mutationFn: (request) => knowledgeBaseApi.recordLearning(request),
});
}
/**
* Search learnings (as query for persistent results)
*/
export function useSearchLearnings(request: LearningSearchRequest, enabled = true) {
return useQuery<KBSearchResponse>({
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
});
}
// =============================================================================
// PROACTIVE CONTEXT HOOKS
// =============================================================================
/**
* Get proactive context for a task
*/
export function useProactiveContext(taskId: string, enabled = true) {
return useQuery<ProactiveContextResponse>({
queryKey: kbKeys.proactiveContext(taskId),
queryFn: () => knowledgeBaseApi.getProactiveContext({ task_id: taskId }),
enabled: enabled && !!taskId,
staleTime: 1000 * 60 * 5, // 5 minutes
});
}
// =============================================================================
// TOKEN ESTIMATION
// =============================================================================
/**
* Estimate token count
*/
export function useEstimateTokens() {
return useMutation<TokenEstimateResponse, Error, TokenEstimateRequest>({
mutationFn: (request) => knowledgeBaseApi.estimateTokens(request),
});
}
+59
View File
@@ -0,0 +1,59 @@
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { notificationsApi, type NotificationFilters } from "@/lib/api/notifications";
export const notificationKeys = {
all: ["notifications"] as const,
list: (filters?: NotificationFilters) => [...notificationKeys.all, "list", filters] as const,
detail: (id: string) => [...notificationKeys.all, "detail", id] as const,
};
export function useNotifications(filters?: NotificationFilters) {
return useQuery({
queryKey: notificationKeys.list(filters),
queryFn: () => notificationsApi.list(filters),
refetchInterval: 30000, // Refetch every 30 seconds
});
}
export function useNotification(notificationId: string | null) {
return useQuery({
queryKey: notificationKeys.detail(notificationId || ""),
queryFn: () => notificationsApi.get(notificationId!),
enabled: !!notificationId,
});
}
export function useMarkNotificationRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (notificationId: string) => notificationsApi.markRead(notificationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
},
});
}
export function useAcknowledgeNotification() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (notificationId: string) => notificationsApi.acknowledge(notificationId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
},
});
}
export function useMarkAllNotificationsRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => notificationsApi.markAllRead(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: notificationKeys.all });
},
});
}
+78
View File
@@ -0,0 +1,78 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { projectsApi, type ProjectFilters } from "@/lib/api/projects";
import type { ProjectCreate, ProjectUpdate } from "@/types";
// Query keys
export const projectKeys = {
all: ["projects"] as const,
lists: () => [...projectKeys.all, "list"] as const,
list: (filters?: ProjectFilters) => [...projectKeys.lists(), filters] as const,
details: () => [...projectKeys.all, "detail"] as const,
detail: (id: string) => [...projectKeys.details(), id] as const,
};
// Hooks
export function useProjects(filters?: ProjectFilters) {
return useQuery({
queryKey: projectKeys.list(filters),
queryFn: () => projectsApi.list(filters),
staleTime: 60000, // 1 minute
});
}
export function useProject(projectId: string) {
return useQuery({
queryKey: projectKeys.detail(projectId),
queryFn: () => projectsApi.get(projectId),
enabled: !!projectId,
});
}
export function useCreateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (project: ProjectCreate) => projectsApi.create(project),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
},
});
}
export function useUpdateProject() {
const queryClient = useQueryClient();
return useMutation({
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);
},
});
}
export function useSetWorkspace() {
const queryClient = useQueryClient();
return useMutation({
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);
},
});
}
export function useDeactivateProject() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (projectId: string) => projectsApi.deactivate(projectId),
onSuccess: (project) => {
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
queryClient.setQueryData(projectKeys.detail(project.id), project);
},
});
}
+76
View File
@@ -0,0 +1,76 @@
/**
* Scroll Restoration Hook
*
* Saves and restores scroll position when navigating between pages.
*/
"use client";
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>) {
const pathname = usePathname();
const searchParams = useSearchParams();
const { setScrollPosition, getScrollPosition } = useUIStore();
// Create a unique key for current route including search params
const routeKey = `${pathname}?${searchParams.toString()}`;
const hasRestored = useRef(false);
// Save scroll position on scroll
useEffect(() => {
const container = scrollContainerRef?.current ?? window;
const isWindow = container === window;
const handleScroll = () => {
const position = isWindow
? { x: window.scrollX, y: window.scrollY }
: { x: (container as HTMLElement).scrollLeft, y: (container as HTMLElement).scrollTop };
setScrollPosition(routeKey, position);
};
// Debounce scroll handler
let timeout: NodeJS.Timeout;
const debouncedScroll = () => {
clearTimeout(timeout);
timeout = setTimeout(handleScroll, 100);
};
container.addEventListener("scroll", debouncedScroll, { passive: true });
return () => {
clearTimeout(timeout);
container.removeEventListener("scroll", debouncedScroll);
};
}, [routeKey, scrollContainerRef, setScrollPosition]);
// Restore scroll position on mount
useEffect(() => {
if (hasRestored.current) return;
const savedPosition = getScrollPosition(routeKey);
if (savedPosition) {
const container = scrollContainerRef?.current ?? window;
const isWindow = container === window;
// Delay restoration to ensure content is rendered
requestAnimationFrame(() => {
if (isWindow) {
window.scrollTo(savedPosition.x, savedPosition.y);
} else {
(container as HTMLElement).scrollLeft = savedPosition.x;
(container as HTMLElement).scrollTop = savedPosition.y;
}
hasRestored.current = true;
});
}
}, [routeKey, scrollContainerRef, getScrollPosition]);
// Reset restoration flag when route changes
useEffect(() => {
hasRestored.current = false;
}, [routeKey]);
}
+305
View File
@@ -0,0 +1,305 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { tasksApi, type TaskFilters } from "@/lib/api/tasks";
import type {
Task,
TaskCreate,
ProgressRequest,
CheckpointRequest,
CommitRequest,
SoftBlockRequest,
EscalateRequest,
} from "@/types";
// Type for task updates - allows any Task field to be updated
export type TaskUpdate = Partial<Task>;
// Query keys
export const taskKeys = {
all: ["tasks"] as const,
lists: () => [...taskKeys.all, "list"] as const,
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,
stats: () => [...taskKeys.all, "stats"] as const,
statsByTeam: () => [...taskKeys.all, "stats-by-team"] as const,
};
// Hooks
export function useTasks(filters?: TaskFilters) {
return useQuery({
queryKey: taskKeys.list(filters),
queryFn: () => tasksApi.list(filters),
staleTime: 30000, // 30 seconds
});
}
export function useTask(taskId: string) {
return useQuery({
queryKey: taskKeys.detail(taskId),
queryFn: () => tasksApi.get(taskId),
enabled: !!taskId,
});
}
export function useSubtasks(parentTaskId: string) {
const { data: allTasks = [] } = useTasks();
return useQuery({
queryKey: taskKeys.subtasks(parentTaskId),
queryFn: async (): Promise<Task[]> => {
// Filter tasks where parent_task_id matches
return allTasks.filter((task) => task.parent_task_id === parentTaskId);
},
enabled: !!parentTaskId && allTasks.length > 0,
});
}
export function useTaskStats() {
return useQuery({
queryKey: taskKeys.stats(),
queryFn: () => tasksApi.getStats(),
staleTime: 60000, // 1 minute
});
}
export function useTaskStatsByTeam() {
return useQuery({
queryKey: taskKeys.statsByTeam(),
queryFn: () => tasksApi.getStatsByTeam(),
staleTime: 60000, // 1 minute
});
}
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (task: TaskCreate) => tasksApi.create(task),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
},
});
}
export function useUpdateTask() {
const queryClient = useQueryClient();
return useMutation({
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);
},
});
}
export function useDeleteTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (taskId: string) => tasksApi.delete(taskId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
},
});
}
// 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);
};
const claim = useMutation({
mutationFn: (taskId: string) => tasksApi.claim(taskId),
onSuccess: invalidateTask,
});
const start = useMutation({
mutationFn: (taskId: string) => tasksApi.start(taskId),
onSuccess: invalidateTask,
});
const block = useMutation({
mutationFn: ({ taskId, blockerId }: { taskId: string; blockerId?: string }) =>
tasksApi.block(taskId, blockerId),
onSuccess: invalidateTask,
});
const unblock = useMutation({
mutationFn: (taskId: string) => tasksApi.unblock(taskId),
onSuccess: invalidateTask,
});
const pause = useMutation({
mutationFn: (taskId: string) => tasksApi.pause(taskId),
onSuccess: invalidateTask,
});
const resume = useMutation({
mutationFn: (taskId: string) => tasksApi.resume(taskId),
onSuccess: invalidateTask,
});
const verify = useMutation({
mutationFn: (taskId: string) => tasksApi.verify(taskId),
onSuccess: invalidateTask,
});
const submitQa = useMutation({
mutationFn: ({ taskId, devNotes }: { taskId: string; devNotes?: string }) =>
tasksApi.submitQa(taskId, devNotes),
onSuccess: invalidateTask,
});
const passQa = useMutation({
mutationFn: ({ taskId, qaNotes }: { taskId: string; qaNotes?: string }) =>
tasksApi.passQa(taskId, qaNotes),
onSuccess: invalidateTask,
});
const failQa = useMutation({
mutationFn: ({ taskId, qaNotes }: { taskId: string; qaNotes?: string }) =>
tasksApi.failQa(taskId, qaNotes),
onSuccess: invalidateTask,
});
const complete = useMutation({
mutationFn: (taskId: string) => tasksApi.complete(taskId),
onSuccess: invalidateTask,
});
const cancel = useMutation({
mutationFn: (taskId: string) => tasksApi.cancel(taskId),
onSuccess: invalidateTask,
});
const reopen = useMutation({
mutationFn: (taskId: string) => tasksApi.reopen(taskId),
onSuccess: invalidateTask,
});
const activate = useMutation({
mutationFn: (taskId: string) => tasksApi.activate(taskId),
onSuccess: invalidateTask,
});
const docsComplete = useMutation({
mutationFn: (taskId: string) => tasksApi.docsComplete(taskId),
onSuccess: invalidateTask,
});
const submitPmReview = useMutation({
mutationFn: (taskId: string) => tasksApi.submitPmReview(taskId),
onSuccess: invalidateTask,
});
// Progress tracking
const addProgress = useMutation({
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),
onSuccess: invalidateTask,
});
const addCommit = useMutation({
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),
onSuccess: invalidateTask,
});
const escalate = useMutation({
mutationFn: ({ taskId, request }: { taskId: string; request: EscalateRequest }) =>
tasksApi.escalate(taskId, request),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
},
});
// CEO Approval workflow
const ceoApprove = useMutation({
mutationFn: ({ taskId, notes }: { taskId: string; notes?: string }) =>
tasksApi.ceoApprove(taskId, notes),
onSuccess: invalidateTask,
});
const ceoReject = useMutation({
mutationFn: ({ taskId, notes }: { taskId: string; notes: string }) =>
tasksApi.ceoReject(taskId, notes),
onSuccess: invalidateTask,
});
const escalateToCeo = useMutation({
mutationFn: ({ taskId, reason }: { taskId: string; reason: string }) =>
tasksApi.escalateToCeo(taskId, reason),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
},
});
return {
// Lifecycle
claim,
start,
block,
unblock,
pause,
resume,
verify,
submitQa,
passQa,
failQa,
complete,
cancel,
reopen,
activate,
docsComplete,
submitPmReview,
// Progress tracking
addProgress,
addCheckpoint,
addCommit,
// Soft block and escalation
softBlock,
escalate,
// CEO Approval
ceoApprove,
ceoReject,
escalateToCeo,
};
}
// Query hook for tasks awaiting CEO approval
export function useTasksAwaitingCeoApproval() {
return useQuery({
queryKey: [...taskKeys.all, "awaiting-ceo"] as const,
queryFn: () => tasksApi.getAwaitingCeoApproval(),
staleTime: 30000,
});
}
// Query hook for tasks awaiting PM review
export function useTasksAwaitingPmReview() {
return useQuery({
queryKey: [...taskKeys.all, "awaiting-pm-review"] as const,
queryFn: () => tasksApi.getAwaitingPmReview(),
staleTime: 30000,
});
}
+234
View File
@@ -0,0 +1,234 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import {
WebSocketConnection,
getWebSocketUrl
} from "@/lib/websocket/connection";
import { CEO_AGENT_ID, STREAM_MAX_MESSAGES } from "@/lib/constants";
// Re-export ConnectionState type
export type { ConnectionState } from "@/lib/websocket/connection";
import type { ConnectionState } from "@/lib/websocket/connection";
// =============================================================================
// Types
// =============================================================================
export interface AgentStreamMessage {
type: "connected" | "agent.stream";
agent_id: string;
chunk?: string;
watcher_count?: number;
timestamp?: string;
}
export interface ChannelMessage {
type: "connected" | "message.new" | "session.closed";
channel_id?: string;
message_id?: string;
agent_id?: string;
content?: string;
message_type?: string;
subscriber_count?: number;
timestamp?: string;
}
export interface NotificationMessage {
type: "connected" | "notification";
agent_id?: string;
notification_id?: string;
notification_type?: string;
subject?: string;
priority?: string;
timestamp?: string;
}
// =============================================================================
// Generic WebSocket Hook
// =============================================================================
export function useWebSocket<T>(
endpoint: string,
queryParams?: Record<string, string>,
enabled: boolean = true
) {
const [state, setState] = useState<ConnectionState>("disconnected");
const [lastMessage, setLastMessage] = useState<T | null>(null);
const [messages, setMessages] = useState<T[]>([]);
const connectionRef = useRef<WebSocketConnection | null>(null);
// Memoize queryParams string to prevent unnecessary reconnects
const queryString = queryParams ? new URLSearchParams(queryParams).toString() : "";
useEffect(() => {
// Don't connect if disabled or no endpoint
if (!enabled || !endpoint) {
return;
}
// Build URL
const baseUrl = getWebSocketUrl();
const url = baseUrl + endpoint + (queryString ? "?" + queryString : "");
// Create connection
const connection = new WebSocketConnection({
url,
onMessage: (data) => {
const message = data as T;
setLastMessage(message);
setMessages((prev) => [...prev.slice(-(STREAM_MAX_MESSAGES - 1)), message]);
},
onStateChange: setState,
});
connectionRef.current = connection;
connection.connect();
// Cleanup on unmount or when dependencies change
return () => {
connection.disconnect();
connectionRef.current = null;
};
}, [enabled, endpoint, queryString]); // Stable dependencies
const disconnect = useCallback(() => {
connectionRef.current?.disconnect();
connectionRef.current = null;
setState("disconnected");
}, []);
const clearMessages = useCallback(() => {
setMessages([]);
setLastMessage(null);
}, []);
return {
state,
lastMessage,
messages,
disconnect,
clearMessages,
isConnected: state === "connected",
isConnecting: state === "connecting" || state === "reconnecting",
};
}
// =============================================================================
// Specialized Hooks
// =============================================================================
/**
* 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
);
// Extract stream chunks
const streamChunks = messages
.filter((m) => m.type === "agent.stream" && m.chunk)
.map((m) => m.chunk as string);
// Combine chunks into full output
const streamOutput = streamChunks.join("");
return {
state,
lastMessage,
messages,
streamChunks,
streamOutput,
clearMessages,
isConnected,
isConnecting,
};
}
/**
* 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
);
// Filter to only actual messages
const channelMessages = messages.filter((m) => m.type === "message.new");
return {
state,
lastMessage,
channelMessages,
allMessages: messages,
clearMessages,
isConnected,
isConnecting,
};
}
/**
* Subscribe to notifications for the CEO
*/
export function useNotificationStream() {
const { state, lastMessage, messages, clearMessages, isConnected, isConnecting } =
useWebSocket<NotificationMessage>(
"/notifications/" + CEO_AGENT_ID,
undefined,
true
);
// Filter to only notification events
const notifications = messages.filter((m) => m.type === "notification");
return {
state,
lastMessage,
notifications,
allMessages: messages,
clearMessages,
isConnected,
isConnecting,
};
}
// =============================================================================
// Connection Status Hook (for UI indicators)
// =============================================================================
export function useConnectionStatus() {
const [connections, setConnections] = useState<Record<string, ConnectionState>>({});
const updateConnection = useCallback((id: string, state: ConnectionState) => {
setConnections((prev) => ({ ...prev, [id]: state }));
}, []);
const removeConnection = useCallback((id: string) => {
setConnections((prev) => {
const next = { ...prev };
delete next[id];
return next;
});
}, []);
const hasActiveConnections = Object.values(connections).some(
(s) => s === "connected" || s === "connecting" || s === "reconnecting"
);
const allConnected = Object.values(connections).every((s) => s === "connected");
return {
connections,
updateConnection,
removeConnection,
hasActiveConnections,
allConnected,
};
}
+120
View File
@@ -0,0 +1,120 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
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,
details: () => [...workSessionKeys.all, "detail"] as const,
detail: (id: string) => [...workSessionKeys.details(), id] as const,
forTask: (taskId: string) => [...workSessionKeys.all, "task", taskId] as const,
};
// Hooks
export function useWorkSessions(filters?: WorkSessionFilters) {
return useQuery({
queryKey: workSessionKeys.list(filters),
queryFn: () => workSessionsApi.list(filters),
staleTime: 30000, // 30 seconds
});
}
export function useWorkSession(sessionId: string) {
return useQuery({
queryKey: workSessionKeys.detail(sessionId),
queryFn: () => workSessionsApi.get(sessionId),
enabled: !!sessionId,
});
}
export function useWorkSessionForTask(taskId: string) {
return useQuery({
queryKey: workSessionKeys.forTask(taskId),
queryFn: () => workSessionsApi.getForTask(taskId),
enabled: !!taskId,
staleTime: 10000, // 10 seconds
});
}
export function useCreateWorkSession() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (session: WorkSessionCreate) => workSessionsApi.create(session),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: workSessionKeys.lists() });
},
});
}
export function useWorkSessionActions() {
const queryClient = useQueryClient();
const invalidateSession = (session: WorkSession) => {
queryClient.invalidateQueries({ queryKey: workSessionKeys.lists() });
queryClient.setQueryData(workSessionKeys.detail(session.id), session);
if (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),
onSuccess: invalidateSession,
});
const addFiles = useMutation({
mutationFn: ({ sessionId, filePaths }: { sessionId: string; filePaths: string[] }) =>
workSessionsApi.addFiles(sessionId, filePaths),
onSuccess: invalidateSession,
});
const createPR = useMutation({
mutationFn: ({
sessionId,
prNumber,
prUrl,
}: {
sessionId: string;
prNumber: number;
prUrl: string;
}) => workSessionsApi.createPR(sessionId, prNumber, prUrl),
onSuccess: invalidateSession,
});
const updatePRStatus = useMutation({
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),
onSuccess: invalidateSession,
});
const complete = useMutation({
mutationFn: (sessionId: string) => workSessionsApi.complete(sessionId),
onSuccess: invalidateSession,
});
const abandon = useMutation({
mutationFn: ({ sessionId, reason }: { sessionId: string; reason?: string }) =>
workSessionsApi.abandon(sessionId, reason),
onSuccess: invalidateSession,
});
return {
addCommit,
addFiles,
createPR,
updatePRStatus,
mergePR,
complete,
abandon,
};
}