mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[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:
+228
-30
@@ -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() });
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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],
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useAgentJournalEntries(
|
||||
task_id?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
},
|
||||
) {
|
||||
return useQuery<JournalEntry[]>({
|
||||
queryKey: [...journalKeys.entries(), "agent", agentIdOrSlug, params],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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() });
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user