fix(.gitignore): anchor Python build artifacts; recover panel/src/lib (28 files)

The 'lib/' rule (intended for Python virtualenv at repo root) was matching panel/src/lib/, hiding the entire panel API client + utility tree from git. Anchored Python build-artifact rules to the repo root with a leading slash so they only match at the top level. Adds 28 panel/src/lib files that should have been tracked from day one.
This commit is contained in:
Renn F
2026-05-02 03:18:47 +02:00
parent 62bda0c497
commit a82a4f9fd4
44 changed files with 5875 additions and 51 deletions
+186
View File
@@ -0,0 +1,186 @@
/**
* A2A (Agent-to-Agent) Protocol API Client
*
* API functions for agent-to-agent communication protocol.
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
// =============================================================================
// Types
// =============================================================================
export interface A2AMessage {
id: string;
from_agent_id: string;
to_agent_id: string;
content: string;
message_type: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
export interface A2AMessageSendRequest {
to_agent_id: string;
content: string;
message_type?: string;
task_id?: string;
metadata?: Record<string, unknown>;
}
export interface A2AMessageResponse {
message_id: string;
status: string;
delivered_at?: string;
}
export interface A2ATask {
id: string;
title: string;
status: string;
assigned_to: string | null;
created_at: string;
}
export interface A2AAgentCard {
agent_id: string;
name: string;
role: string;
capabilities: string[];
status: string;
current_task_id: string | null;
}
export interface A2AStreamChunk {
chunk_id: string;
content: string;
is_final: boolean;
}
// =============================================================================
// API Client
// =============================================================================
export const a2aApi = {
// ===========================================================================
// MESSAGE ENDPOINTS
// ===========================================================================
/**
* Send a message to another agent
*/
sendMessage: async (request: A2AMessageSendRequest): Promise<A2AMessageResponse> => {
if (isMockMode()) {
return {
message_id: `msg-${Date.now()}`,
status: "delivered",
delivered_at: new Date().toISOString(),
};
}
const { data } = await api.post<A2AMessageResponse>("/a2a/message/send", request);
return data;
},
/**
* Stream a message to another agent (for long content)
*/
streamMessage: async (request: A2AMessageSendRequest): Promise<A2AMessageResponse> => {
if (isMockMode()) {
return {
message_id: `msg-${Date.now()}`,
status: "streaming",
};
}
const { data } = await api.post<A2AMessageResponse>("/a2a/message/stream", request);
return data;
},
// ===========================================================================
// TASK ENDPOINTS
// ===========================================================================
/**
* List tasks visible via A2A protocol
*/
listTasks: async (): Promise<A2ATask[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<A2ATask[]>("/a2a/tasks");
return data;
},
/**
* Get a specific task via A2A protocol
*/
getTask: async (taskId: string): Promise<A2ATask> => {
if (isMockMode()) {
return {
id: taskId,
title: "Mock Task",
status: "in_progress",
assigned_to: null,
created_at: new Date().toISOString(),
};
}
const { data } = await api.get<A2ATask>(`/a2a/tasks/${taskId}`);
return data;
},
/**
* Subscribe to task updates (returns SSE stream URL)
* Note: This endpoint returns Server-Sent Events, handle appropriately
*/
subscribeToTask: (taskId: string): string => {
// Returns the URL for SSE subscription
return `/a2a/tasks/${taskId}/subscribe`;
},
/**
* Cancel a task via A2A protocol
*/
cancelTask: async (taskId: string): Promise<{ status: string; task_id: string }> => {
if (isMockMode()) {
return {
status: "cancelled",
task_id: taskId,
};
}
const { data } = await api.post<{ status: string; task_id: string }>(`/a2a/tasks/${taskId}/cancel`);
return data;
},
// ===========================================================================
// AGENT ENDPOINTS
// ===========================================================================
/**
* List all agents available via A2A protocol
*/
listAgents: async (): Promise<A2AAgentCard[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<A2AAgentCard[]>("/a2a/agents");
return data;
},
/**
* Get agent card (profile/capabilities)
*/
getAgentCard: async (agentId: string): Promise<A2AAgentCard> => {
if (isMockMode()) {
return {
agent_id: agentId,
name: "Mock Agent",
role: "developer",
capabilities: ["coding", "testing"],
status: "idle",
current_task_id: null,
};
}
const { data } = await api.get<A2AAgentCard>(`/a2a/agents/${agentId}/card`);
return data;
},
};
+70
View File
@@ -0,0 +1,70 @@
/**
* Agents API Client
*
* Fetches agent data from the backend API.
*/
import api from "./client";
import type { AgentRole, Team } from "@/types";
import { isMockMode, mockAgents } from "@/lib/mock-data";
export interface AgentDefinition {
id: string; // slug from backend (e.g., "be-dev-1")
name: string;
role: AgentRole | null;
team: Team | null;
}
interface AgentApiResponse {
id: string; // UUID (not used for definitions)
name: string;
slug: string;
role: string;
team: string | null;
}
export const agentsApi = {
/**
* Get all agents from the API
*/
getAll: async (): Promise<AgentDefinition[]> => {
if (isMockMode()) {
return mockAgents.map((a) => ({
id: a.slug || a.id,
name: a.name,
role: a.role,
team: a.team,
}));
}
const response = await api.get<AgentApiResponse[]>("/agents");
const data = response.data;
// Handle unexpected response formats
if (!data || !Array.isArray(data)) {
console.warn("Unexpected agents API response:", data);
return [];
}
return data.map((a) => ({
id: a.slug || a.id, // Use slug as ID, fallback to UUID
name: a.name || "Unknown",
role: (a.role as AgentRole) || null,
team: (a.team as Team) || null,
}));
},
/**
* Get a single agent by ID or slug
*/
getOne: async (idOrSlug: string): Promise<AgentDefinition> => {
const response = await api.get<AgentApiResponse>(`/agents/${idOrSlug}`);
const a = response.data;
return {
id: a.slug,
name: a.name,
role: a.role as AgentRole,
team: a.team as Team | null,
};
},
};
+135
View File
@@ -0,0 +1,135 @@
import api from "./client";
import type { Channel, PaginatedResponse, Group } from "@/types";
import { ChannelType } from "@/types";
import { isMockMode, mockChannels, mockGroups } from "@/lib/mock-data";
export interface ChannelFilters {
type?: string;
is_private?: boolean;
}
export interface ChannelCreate {
name: string;
slug: string;
type?: string;
description?: string;
topic?: string;
is_private?: boolean;
}
export interface ChannelUpdate {
name?: string;
description?: string;
topic?: string;
is_archived?: boolean;
}
export const channelsApi = {
// List all channels
list: async (filters?: ChannelFilters): Promise<Channel[]> => {
if (isMockMode()) {
let channels = [...mockChannels] as Channel[];
if (filters?.type) {
channels = channels.filter((c) => c.type === filters.type);
}
if (filters?.is_private !== undefined) {
channels = channels.filter((c) => c.is_private === filters.is_private);
}
return channels;
}
const { data } = await api.get<PaginatedResponse<Channel>>("/channels", { params: filters });
return data.items;
},
// Get channel by ID
get: async (channelId: string): Promise<Channel> => {
if (isMockMode()) {
const channel = mockChannels.find((c) => c.id === channelId);
if (channel) return channel as Channel;
throw new Error("Channel not found");
}
const { data } = await api.get<Channel>("/channels/" + channelId);
return data;
},
// Get channel by slug
getBySlug: async (slug: string): Promise<Channel> => {
if (isMockMode()) {
const channel = mockChannels.find((c) => c.slug === slug);
if (channel) return channel as Channel;
throw new Error("Channel not found");
}
// Backend uses query param filter, not path segment
const { data } = await api.get<PaginatedResponse<Channel>>("/channels", {
params: { slug },
});
if (!data.items.length) {
throw new Error("Channel not found");
}
return data.items[0];
},
// Get groups for a channel
getGroups: async (channelId: string): Promise<Group[]> => {
if (isMockMode()) {
return mockGroups as Group[];
}
const { data } = await api.get<Group[]>("/channels/" + channelId + "/groups");
return data;
},
// Create a new channel (PM/CEO only)
create: async (channel: ChannelCreate): Promise<Channel> => {
if (isMockMode()) {
const newChannel: Channel = {
id: `channel-${Date.now()}`,
name: channel.name,
slug: channel.slug,
type: (channel.type as ChannelType) || ChannelType.CELL,
description: channel.description || null,
topic: channel.topic || null,
is_private: channel.is_private || false,
is_archived: false,
member_count: 0,
message_count: 0,
group_count: 0,
can_write: true,
};
(mockChannels as Channel[]).push(newChannel);
return newChannel;
}
const { data } = await api.post<Channel>("/channels", channel);
return data;
},
// Update a channel (PM/CEO only)
update: async (channelId: string, updates: ChannelUpdate): Promise<Channel> => {
if (isMockMode()) {
const idx = mockChannels.findIndex((c) => c.id === channelId);
if (idx === -1) throw new Error("Channel not found");
const updated = { ...mockChannels[idx], ...updates } as Channel;
(mockChannels as Channel[])[idx] = updated;
return updated;
}
const { data } = await api.patch<Channel>("/channels/" + channelId, updates);
return data;
},
// Add a member to a channel (PM/CEO only)
addMember: async (channelId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.post("/channels/" + channelId + "/add-member", { agent_id: agentId });
},
// Remove a member from a channel (PM/CEO only)
removeMember: async (channelId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.delete("/channels/" + channelId + "/remove-member", {
data: { agent_id: agentId },
});
},
};
+112
View File
@@ -0,0 +1,112 @@
import axios, { AxiosInstance, AxiosError } from "axios";
import { API_URL, CEO_AGENT_ID, CEO_ROLE } from "@/lib/constants";
// Create axios instance with default config
const api: AxiosInstance = axios.create({
baseURL: API_URL,
headers: {
"Content-Type": "application/json",
},
timeout: 60000, // Increased to 60s for long operations like reindexing
});
// Request interceptor to add auth headers and logging
api.interceptors.request.use(
(config) => {
// Add agent context headers for API authorization
config.headers["X-Agent-ID"] = CEO_AGENT_ID;
config.headers["X-Agent-Role"] = CEO_ROLE;
// Log request in development
if (process.env.NODE_ENV === "development") {
console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`);
}
return config;
},
(error) => {
console.error("[API] Request setup error:", error);
return Promise.reject(error);
}
);
// Response interceptor for comprehensive error handling
api.interceptors.response.use(
(response) => {
// Log successful responses in development
if (process.env.NODE_ENV === "development") {
console.log(`[API] ✓ ${response.config.method?.toUpperCase()} ${response.config.url}`);
}
return response;
},
(error: AxiosError) => {
// Extract error details
const status = error.response?.status;
const url = error.config?.url;
const method = error.config?.method?.toUpperCase();
const errorData = error.response?.data as Record<string, unknown> | undefined;
const errorDetail = errorData?.detail || error.message;
// Log comprehensive error info
console.error(`[API] ✗ ${method} ${url}`, {
status,
detail: errorDetail,
error: error.message,
});
// Specific error handling with helpful messages
if (error.code === "ECONNABORTED") {
console.error("[API] Request timed out - backend may be overloaded or unavailable");
} else if (error.code === "ERR_NETWORK") {
console.error("[API] Network error - check if backend is running at", API_URL);
} else if (status === 401) {
console.error("[API] Unauthorized - check API authentication headers");
} else if (status === 403) {
console.error("[API] Forbidden - insufficient permissions for this action");
} else if (status === 404) {
console.error("[API] Not found - endpoint may not exist:", url);
} else if (status === 422) {
console.error("[API] Validation error - request data is invalid:", errorDetail);
} else if (status && status >= 500) {
console.error("[API] Server error - backend encountered an internal error");
}
return Promise.reject(error);
}
);
/**
* Helper to extract user-friendly error message from API error
*/
export function getErrorMessage(error: unknown): string {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<{ detail?: string }>;
// Check for specific error codes
if (error.code === "ECONNABORTED") {
return "Request timed out. The server may be busy.";
}
if (error.code === "ERR_NETWORK") {
return "Cannot connect to server. Check if the backend is running.";
}
// Check for API error response
if (axiosError.response?.data?.detail) {
return String(axiosError.response.data.detail);
}
// Check for HTTP status
const status = axiosError.response?.status;
if (status === 401) return "Authentication required. Please refresh the page.";
if (status === 403) return "Permission denied for this action.";
if (status === 404) return "The requested resource was not found.";
if (status === 422) return "Invalid request data.";
if (status && status >= 500) return "Server error. Please try again later.";
}
// Generic error
return error instanceof Error ? error.message : "An unexpected error occurred";
}
export { api, API_URL };
export default api;
+568
View File
@@ -0,0 +1,568 @@
import api from "./client";
import { Team, TaskStatus } from "@/types";
import type {
KanbanBoard,
AuditorDashboard,
AuditorFlag,
AuditorReport,
FlagSeverity,
CEOOverview as CEOOverviewType,
Task,
} from "@/types";
import {
isMockMode,
mockDashboardStats,
mockTeamHealth,
getMockRecentActivity,
mockAuditorDashboard,
mockAuditorFlags,
mockAuditorReports,
mockTasks,
mockOrchestratorStatus,
} from "@/lib/mock-data";
// Re-export types from @/types for backward compatibility
export type { CEOOverviewType as CEOOverview };
export interface TeamHealth {
team: Team;
health_score: number;
active_tasks: number;
blocked_tasks: number;
completed_today: number;
}
export interface MetricsSummary {
velocity: VelocityMetric;
blockers: BlockerMetric;
communication: CommunicationMetric;
agents: AgentMetric;
}
export interface VelocityMetric {
tasks_completed_today: number;
tasks_completed_week: number;
average_completion_time_hours: number;
}
export interface BlockerMetric {
total_blocked: number;
blocked_by_team: Record<string, number>;
longest_blocked_hours: number;
}
export interface CommunicationMetric {
messages_today: number;
active_channels: number;
notifications_pending: number;
}
export interface AgentMetric {
total_agents: number;
running: number;
idle: number;
waiting: number;
errors: number;
}
// =============================================================================
// AUDITOR API TYPES
// =============================================================================
export interface CreateFlagRequest {
severity: FlagSeverity;
category: string;
title: string;
description: string;
related_task_id?: string;
related_agent_id?: string;
}
export interface CreateReportRequest {
report_type: string;
title: string;
summary: string;
sections: Array<Record<string, unknown>>;
}
// Helper to create mock kanban board from tasks
function createMockKanbanBoard(tasks: Task[], team?: Team): KanbanBoard {
const filteredTasks = team ? tasks.filter((t) => t.team === team) : tasks;
const pendingTasks = filteredTasks.filter((t) => t.status === TaskStatus.PENDING);
const inProgressTasks = filteredTasks.filter((t) => t.status === TaskStatus.IN_PROGRESS);
const blockedTasks = filteredTasks.filter((t) => t.status === TaskStatus.BLOCKED);
const awaitingQaTasks = filteredTasks.filter((t) => t.status === TaskStatus.AWAITING_QA);
const completedTasks = filteredTasks.filter((t) => t.status === TaskStatus.COMPLETED);
return {
columns: [
{
id: "pending",
title: "Pending",
status: TaskStatus.PENDING,
tasks: pendingTasks,
count: pendingTasks.length,
},
{
id: "in_progress",
title: "In Progress",
status: TaskStatus.IN_PROGRESS,
tasks: inProgressTasks,
count: inProgressTasks.length,
},
{
id: "blocked",
title: "Blocked",
status: TaskStatus.BLOCKED,
tasks: blockedTasks,
count: blockedTasks.length,
},
{
id: "awaiting_qa",
title: "Awaiting QA",
status: TaskStatus.AWAITING_QA,
tasks: awaitingQaTasks,
count: awaitingQaTasks.length,
},
{
id: "completed",
title: "Completed",
status: TaskStatus.COMPLETED,
tasks: completedTasks,
count: completedTasks.length,
},
],
total_tasks: filteredTasks.length,
};
}
export const dashboardApi = {
// Get CEO overview
getCeoOverview: async (): Promise<CEOOverviewType> => {
if (isMockMode()) {
return {
health_status: mockTeamHealth,
key_metrics: {
total_tasks: mockDashboardStats.total_tasks,
tasks_in_progress: mockDashboardStats.tasks_in_progress,
tasks_blocked: mockDashboardStats.tasks_blocked,
tasks_completed_today: mockDashboardStats.tasks_completed_today,
active_agents: mockDashboardStats.active_agents,
},
auditor_alerts: {},
roadmap_progress: {
phase_1: 100,
phase_2: 75,
phase_3: 25,
},
};
}
const { data } = await api.get<CEOOverviewType>("/dashboard/ceo");
return data;
},
// Get metrics - individual endpoints
getVelocityMetrics: async (): Promise<VelocityMetric> => {
if (isMockMode()) {
return {
tasks_completed_today: mockDashboardStats.tasks_completed_today,
tasks_completed_week: 15,
average_completion_time_hours: 4.5,
};
}
const { data } = await api.get<VelocityMetric>("/dashboard/metrics/velocity");
return data;
},
getBlockerMetrics: async (): Promise<BlockerMetric> => {
if (isMockMode()) {
const blockedTasks = mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
return {
total_blocked: blockedTasks.length,
blocked_by_team: {
backend: blockedTasks.filter((t) => t.team === Team.BACKEND).length,
frontend: blockedTasks.filter((t) => t.team === Team.FRONTEND).length,
ux_ui: blockedTasks.filter((t) => t.team === Team.UX_UI).length,
marketing: blockedTasks.filter((t) => t.team === Team.MARKETING).length,
},
longest_blocked_hours: 24,
};
}
const { data } = await api.get<BlockerMetric>("/dashboard/metrics/blockers");
return data;
},
getCommunicationMetrics: async (): Promise<CommunicationMetric> => {
if (isMockMode()) {
return {
messages_today: 45,
active_channels: 5,
notifications_pending: 3,
};
}
const { data } = await api.get<CommunicationMetric>("/dashboard/metrics/communication");
return data;
},
getHealthMetrics: async () => {
if (isMockMode()) {
return mockTeamHealth;
}
const { data } = await api.get("/dashboard/metrics/health");
return data;
},
// Get kanban board for a team (dev view)
getKanbanDev: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>("/kanban/dev/" + team);
return data;
},
// Get QA kanban board
getKanbanQa: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>("/kanban/qa/" + team);
return data;
},
// Get PM kanban board (cross-team view)
getKanbanPm: async (): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks);
}
const { data } = await api.get<KanbanBoard>("/kanban/main-pm");
return data;
},
// Get agent status
getAgentStatus: async () => {
if (isMockMode()) {
return mockOrchestratorStatus;
}
const { data } = await api.get("/dashboard/agents/status");
return data;
},
// Get recent activity
getRecentActivity: async (hours: number = 24, limit: number = 50) => {
if (isMockMode()) {
return getMockRecentActivity().slice(0, limit);
}
const { data } = await api.get<{ period_hours: number; activity: unknown[] }>("/dashboard/activity/recent", {
params: { hours, limit },
});
// Backend returns { period_hours, activity }, extract the activity array
return data.activity ?? [];
},
// =============================================================================
// AUDITOR DASHBOARD ENDPOINTS
// =============================================================================
// Get complete auditor dashboard
getAuditorDashboard: async (): Promise<AuditorDashboard> => {
if (isMockMode()) {
return mockAuditorDashboard as AuditorDashboard;
}
const { data } = await api.get<AuditorDashboard>("/dashboard/auditor");
return data;
},
// Get auditor flags
getAuditorFlags: async (params?: {
severity?: FlagSeverity;
resolved?: boolean;
}): Promise<AuditorFlag[]> => {
if (isMockMode()) {
let flags = [...mockAuditorFlags] as AuditorFlag[];
if (params?.severity) {
flags = flags.filter((f) => f.severity === params.severity);
}
if (params?.resolved !== undefined) {
flags = flags.filter((f) => (params.resolved ? f.resolved_at : !f.resolved_at));
}
return flags;
}
const { data } = await api.get<AuditorFlag[]>("/dashboard/auditor/flags", {
params,
});
return data;
},
// Create an auditor flag
createAuditorFlag: async (
request: CreateFlagRequest
): Promise<AuditorFlag> => {
if (isMockMode()) {
const newFlag: AuditorFlag = {
id: `flag-${Date.now()}`,
severity: request.severity,
category: request.category,
title: request.title,
description: request.description,
related_task_id: request.related_task_id ?? null,
related_agent_id: request.related_agent_id ?? null,
created_at: new Date().toISOString(),
resolved_at: null,
notes: null,
};
(mockAuditorFlags as AuditorFlag[]).push(newFlag);
return newFlag;
}
const { data } = await api.post<AuditorFlag>(
"/dashboard/auditor/flags",
request
);
return data;
},
// Resolve an auditor flag
resolveAuditorFlag: async (
flagId: string,
notes?: string
): Promise<{ status: string; flag_id: string }> => {
if (isMockMode()) {
const flags = mockAuditorFlags as AuditorFlag[];
const idx = flags.findIndex((f) => f.id === flagId);
if (idx !== -1) {
flags[idx] = {
...flags[idx],
resolved_at: new Date().toISOString(),
notes: notes ?? null,
};
return { status: "resolved", flag_id: flagId };
}
throw new Error("Flag not found");
}
const { data } = await api.put(`/dashboard/auditor/flags/${flagId}/resolve`, null, {
params: { notes },
});
return data;
},
// Get auditor reports
getAuditorReports: async (params?: {
report_type?: string;
limit?: number;
}): Promise<AuditorReport[]> => {
if (isMockMode()) {
let reports = [...mockAuditorReports] as AuditorReport[];
if (params?.report_type) {
reports = reports.filter((r) => r.report_type === params.report_type);
}
if (params?.limit) {
reports = reports.slice(0, params.limit);
}
return reports;
}
const { data } = await api.get<AuditorReport[]>(
"/dashboard/auditor/reports",
{ params }
);
return data;
},
// Create an auditor report
createAuditorReport: async (
request: CreateReportRequest
): Promise<AuditorReport> => {
if (isMockMode()) {
const newReport: AuditorReport = {
id: `report-${Date.now()}`,
report_type: request.report_type,
title: request.title,
summary: request.summary,
sections: request.sections,
created_at: new Date().toISOString(),
sent_at: null,
};
(mockAuditorReports as AuditorReport[]).push(newReport);
return newReport;
}
const { data } = await api.post<AuditorReport>(
"/dashboard/auditor/reports",
request
);
return data;
},
// Send a report to CEO
sendAuditorReport: async (
reportId: string
): Promise<{ status: string; report_id: string }> => {
if (isMockMode()) {
const reports = mockAuditorReports as AuditorReport[];
const idx = reports.findIndex((r) => r.id === reportId);
if (idx !== -1) {
reports[idx] = {
...reports[idx],
sent_at: new Date().toISOString(),
};
return { status: "sent", report_id: reportId };
}
throw new Error("Report not found");
}
const { data } = await api.post(
`/dashboard/auditor/reports/${reportId}/send`
);
return data;
},
// =============================================================================
// CEO DETAIL ENDPOINTS
// =============================================================================
// Get detailed team metrics
getCeoTeamDetails: async () => {
if (isMockMode()) {
return mockTeamHealth.map((th) => ({
...th,
velocity_7d: 10,
avg_completion_time: 4.5,
agent_count: 5,
}));
}
const { data } = await api.get("/dashboard/ceo/teams");
return data;
},
// Get blocker details for CEO
getCeoBlockerDetails: async () => {
if (isMockMode()) {
const blockedTasks = mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
return {
total_blocked: blockedTasks.length,
blockers: blockedTasks.map((t) => ({
task_id: t.id,
title: t.title,
team: t.team,
blocked_hours: 24,
blocker_reason: "Dependency not resolved",
})),
};
}
const { data } = await api.get("/dashboard/ceo/blockers");
return data;
},
// Get velocity metrics for CEO
getCeoVelocity: async (days: number = 7) => {
if (isMockMode()) {
return {
period_days: days,
total_completed: 15,
by_team: {
backend: 6,
frontend: 5,
ux_ui: 4,
marketing: 0,
},
daily_breakdown: Array.from({ length: days }, (_, i) => ({
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
completed: Math.floor(Math.random() * 5) + 1,
})),
};
}
const { data } = await api.get("/dashboard/ceo/velocity", {
params: { days },
});
return data;
},
// =============================================================================
// KANBAN ENDPOINTS (Role-specific views)
// =============================================================================
// Get documenter kanban board
getKanbanDocumenter: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>(`/kanban/documenter/${team}`);
return data;
},
// Get cell PM kanban board
getKanbanCellPm: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks, team);
}
const { data } = await api.get<KanbanBoard>(`/kanban/pm/${team}`);
return data;
},
// Get board-level roadmap kanban
getKanbanBoard: async (): Promise<KanbanBoard> => {
if (isMockMode()) {
return createMockKanbanBoard(mockTasks);
}
const { data } = await api.get<KanbanBoard>("/kanban/board");
return data;
},
// Get kanban statistics
getKanbanStats: async (team?: Team) => {
if (isMockMode()) {
const tasks = team ? mockTasks.filter((t) => t.team === team) : mockTasks;
return {
total: tasks.length,
by_status: {
pending: tasks.filter((t) => t.status === TaskStatus.PENDING).length,
in_progress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
blocked: tasks.filter((t) => t.status === TaskStatus.BLOCKED).length,
awaiting_qa: tasks.filter((t) => t.status === TaskStatus.AWAITING_QA).length,
completed: tasks.filter((t) => t.status === TaskStatus.COMPLETED).length,
},
};
}
const { data } = await api.get("/kanban/stats", { params: { team } });
return data;
},
// Get metrics for a specific agent
getAgentMetrics: async (agentId: string) => {
if (isMockMode()) {
return {
agent_id: agentId,
tasks_completed: 10,
tasks_in_progress: 2,
avg_completion_time_hours: 8.5,
quality_score: 0.92,
};
}
const { data } = await api.get(`/dashboard/metrics/agent/${agentId}`);
return data;
},
// Get metrics for a specific team
getTeamMetrics: async (team: Team) => {
if (isMockMode()) {
return {
team,
tasks_total: 25,
tasks_completed: 15,
tasks_in_progress: 5,
tasks_blocked: 2,
velocity: 8.5,
quality_score: 0.88,
};
}
const { data } = await api.get(`/dashboard/metrics/team/${team}`);
return data;
},
// Get kanban board for any team (generic endpoint)
getKanbanForTeam: async (team: Team): Promise<KanbanBoard> => {
if (isMockMode()) {
const teamTasks = mockTasks.filter((t) => t.team === team);
return createMockKanbanBoard(teamTasks);
}
const { data } = await api.get<KanbanBoard>(`/dashboard/kanban/${team}`);
return data;
},
};
+242
View File
@@ -0,0 +1,242 @@
/**
* Git API Client
*
* API functions for Git operations (branches, commits, PRs).
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
import type {
GitStatusResponse,
GitLogResponse,
GitBranchListResponse,
GitDiffResponse,
GitCommitRequest,
GitCommitResponse,
GitPushRequest,
GitPushResponse,
GitCreateBranchRequest,
GitCreateBranchResponse,
GitCheckoutRequest,
GitCheckoutResponse,
GitCreatePRRequest,
GitCreatePRResponse,
GitMergePRRequest,
GitMergePRResponse,
} from "@/types/git";
// =============================================================================
// Mock Data
// =============================================================================
const mockGitStatus: GitStatusResponse = {
project_slug: "roboco",
current_branch: "feature/backend/abc12345",
has_changes: true,
staged_files: ["src/api/routes/tasks.py"],
unstaged_files: ["src/services/task_service.py"],
untracked_files: [],
ahead: 2,
behind: 0,
};
const mockGitLog: GitLogResponse = {
project_slug: "roboco",
branch: "feature/backend/abc12345",
commits: [
{
hash: "abc123456789abcdef",
short_hash: "abc1234",
message: "[abc12345] Add task status endpoint",
author: "backend-dev",
date: new Date().toISOString(),
},
],
};
const mockBranches: GitBranchListResponse = {
project_slug: "roboco",
current_branch: "feature/backend/abc12345",
branches: [
{ name: "main", is_current: false, is_remote: false, last_commit: "def456" },
{ name: "feature/backend/abc12345", is_current: true, is_remote: false, last_commit: "abc123" },
],
};
// =============================================================================
// API Client
// =============================================================================
export const gitApi = {
// ===========================================================================
// READ-ONLY OPERATIONS
// ===========================================================================
/**
* Get git status for a project
*/
getStatus: async (projectSlug: string, taskId?: string): Promise<GitStatusResponse> => {
if (isMockMode()) {
return { ...mockGitStatus, project_slug: projectSlug };
}
const { data } = await api.get<GitStatusResponse>("/git/status", {
params: { project_slug: projectSlug, _task_id: taskId },
});
return data;
},
/**
* Get git log for a project
*/
getLog: async (
projectSlug: string,
limit: number = 10,
branch?: string
): Promise<GitLogResponse> => {
if (isMockMode()) {
return { ...mockGitLog, project_slug: projectSlug };
}
const { data } = await api.get<GitLogResponse>("/git/log", {
params: { project_slug: projectSlug, limit, branch },
});
return data;
},
/**
* Get git branches for a project
*/
getBranches: async (
projectSlug: string,
includeRemote: boolean = false
): Promise<GitBranchListResponse> => {
if (isMockMode()) {
return { ...mockBranches, project_slug: projectSlug };
}
const { data } = await api.get<GitBranchListResponse>("/git/branches", {
params: { project_slug: projectSlug, include_remote: includeRemote },
});
return data;
},
/**
* Get git diff for a project
*/
getDiff: async (
projectSlug: string,
staged: boolean = false,
filePath?: string
): Promise<GitDiffResponse> => {
if (isMockMode()) {
return {
project_slug: projectSlug,
staged,
file_path: filePath || null,
diff: "- old line\n+ new line",
files_changed: 1,
};
}
const { data } = await api.get<GitDiffResponse>("/git/diff", {
params: { project_slug: projectSlug, staged, file_path: filePath },
});
return data;
},
// ===========================================================================
// WRITE OPERATIONS
// ===========================================================================
/**
* Create a git commit
*/
commit: async (request: GitCommitRequest): Promise<GitCommitResponse> => {
if (isMockMode()) {
return {
commit_hash: "abc123456789abcdef",
message: `[${request.task_id.slice(0, 8)}] ${request.message}`,
files_changed: request.files?.length || 1,
insertions: 10,
deletions: 5,
};
}
const { data } = await api.post<GitCommitResponse>("/git/commit", request);
return data;
},
/**
* Push commits to remote
*/
push: async (request: GitPushRequest): Promise<GitPushResponse> => {
if (isMockMode()) {
return {
branch: "feature/backend/abc12345",
commits_pushed: 2,
remote: "origin",
ready_for_pr: true,
};
}
const { data } = await api.post<GitPushResponse>("/git/push", request);
return data;
},
/**
* Create a task branch (PM only)
*/
createBranch: async (request: GitCreateBranchRequest): Promise<GitCreateBranchResponse> => {
if (isMockMode()) {
return {
branch_name: `${request.branch_type}/backend/${request.task_id.slice(0, 8)}`,
created_from: request.parent_branch || "main",
project_slug: request.project_slug,
};
}
const { data } = await api.post<GitCreateBranchResponse>("/git/branch/create", request);
return data;
},
/**
* Checkout a branch
*/
checkout: async (request: GitCheckoutRequest): Promise<GitCheckoutResponse> => {
if (isMockMode()) {
return {
branch: request.branch,
project_slug: request.project_slug,
};
}
const { data } = await api.post<GitCheckoutResponse>("/git/checkout", request);
return data;
},
/**
* Create a pull request
*/
createPR: async (request: GitCreatePRRequest): Promise<GitCreatePRResponse> => {
if (isMockMode()) {
return {
pr_number: 42,
pr_url: "https://github.com/org/repo/pull/42",
title: request.title,
source_branch: "feature/backend/abc12345",
target_branch: "main",
};
}
const { data } = await api.post<GitCreatePRResponse>("/git/pr/create", request);
return data;
},
/**
* Merge a pull request (PM only)
*/
mergePR: async (request: GitMergePRRequest): Promise<GitMergePRResponse> => {
if (isMockMode()) {
return {
pr_number: request.pr_number,
merged: true,
merge_commit: "def456789abcdef",
target_branch: "main",
};
}
const { data } = await api.post<GitMergePRResponse>("/git/pr/merge", request);
return data;
},
};
+61
View File
@@ -0,0 +1,61 @@
/**
* Groups API Client
*
* API functions for group management within channels.
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
import type { Group } from "@/types";
// =============================================================================
// Types
// =============================================================================
export interface GroupCreate {
channel_id: string;
name: string;
hierarchy_level?: number;
}
// =============================================================================
// API Client
// =============================================================================
export const groupsApi = {
/**
* Create a new group within a channel
*/
create: async (group: GroupCreate): Promise<Group> => {
if (isMockMode()) {
return {
id: `group-${Date.now()}`,
name: group.name,
hierarchy_level: group.hierarchy_level ?? 0,
is_active: true,
total_messages: 0,
active_session_id: null,
};
}
const { data } = await api.post<Group>("/groups", group);
return data;
},
/**
* Get a group by ID
*/
get: async (groupId: string): Promise<Group> => {
if (isMockMode()) {
return {
id: groupId,
name: "Mock Group",
hierarchy_level: 0,
is_active: true,
total_messages: 0,
active_session_id: null,
};
}
const { data } = await api.get<Group>(`/groups/${groupId}`);
return data;
},
};
+13
View File
@@ -0,0 +1,13 @@
export { api, API_URL } from "./client";
export { tasksApi } from "./tasks";
export { orchestratorApi } from "./orchestrator";
export { channelsApi } from "./channels";
export { notificationsApi } from "./notifications";
export { dashboardApi } from "./dashboard";
export { knowledgeBaseApi } from "./knowledge-base";
export { projectsApi } from "./projects";
export { workSessionsApi } from "./work-sessions";
export { gitApi } from "./git";
export { a2aApi } from "./a2a";
export { streamApi } from "./stream";
export { groupsApi } from "./groups";
+426
View File
@@ -0,0 +1,426 @@
/**
* Journals API Client
*
* Agent personal journals for reflection, growth tracking, and debugging.
* Matches backend: roboco/api/routes/journals.py
*/
import api from "./client";
import {
Journal,
JournalEntry,
JournalEntryCreate,
JournalEntryType,
JournalStats,
GrowthMetrics,
} from "@/types";
import {
isMockMode,
mockJournals,
mockJournalEntries,
} from "@/lib/mock-data";
// =============================================================================
// JOURNAL ENDPOINTS
// =============================================================================
/**
* Get the current agent's journal (CEO's journal in this case)
*/
async function getMyJournal(): Promise<Journal> {
if (isMockMode()) {
return mockJournals[0] as Journal;
}
const response = await api.get<Journal>("/journals/me");
return response.data;
}
/**
* Get a journal by agent ID or slug (e.g., "be-dev-1")
*/
async function getJournalByAgent(agentIdOrSlug: string): Promise<Journal> {
if (isMockMode()) {
const journal = mockJournals.find((j) => j.agent_id === agentIdOrSlug);
if (journal) return journal as Journal;
// Return first journal as fallback
return mockJournals[0] as Journal;
}
const response = await api.get<Journal>(`/journals/${agentIdOrSlug}`);
return response.data;
}
/**
* List entries for a specific agent by ID or slug
*/
async function listAgentEntries(
agentIdOrSlug: string,
params?: {
entry_type?: JournalEntryType;
task_id?: string;
limit?: number;
offset?: number;
}
): Promise<JournalEntry[]> {
if (isMockMode()) {
let entries = [...mockJournalEntries] as JournalEntry[];
if (params?.entry_type) {
entries = entries.filter((e) => e.type === params.entry_type);
}
if (params?.task_id) {
entries = entries.filter((e) => e.task_id === params.task_id);
}
const offset = params?.offset ?? 0;
const limit = params?.limit ?? 50;
return entries.slice(offset, offset + limit);
}
const response = await api.get<JournalEntry[]>(
`/journals/${agentIdOrSlug}/entries`,
{ params }
);
return response.data;
}
// =============================================================================
// ENTRY CRUD ENDPOINTS
// =============================================================================
/**
* Create a new journal entry
*/
async function createEntry(data: JournalEntryCreate): Promise<JournalEntry> {
if (isMockMode()) {
const newEntry: JournalEntry = {
id: `entry-${Date.now()}`,
journal_id: mockJournals[0].id,
type: data.type,
title: data.title,
content: data.content,
task_id: data.task_id ?? null,
session_id: data.session_id ?? null,
timestamp: new Date().toISOString(),
tags: data.tags ?? [],
sentiment: data.sentiment ?? null,
is_private: data.is_private ?? false,
created_at: new Date().toISOString(),
updated_at: null,
};
(mockJournalEntries as JournalEntry[]).push(newEntry);
return newEntry;
}
const response = await api.post<JournalEntry>("/journals/me/entries", data);
return response.data;
}
/**
* List current agent's journal entries
*/
async function listMyEntries(params?: {
entry_type?: JournalEntryType;
task_id?: string;
limit?: number;
offset?: number;
}): Promise<JournalEntry[]> {
if (isMockMode()) {
let entries = [...mockJournalEntries] as JournalEntry[];
if (params?.entry_type) {
entries = entries.filter((e) => e.type === params.entry_type);
}
if (params?.task_id) {
entries = entries.filter((e) => e.task_id === params.task_id);
}
const offset = params?.offset ?? 0;
const limit = params?.limit ?? 50;
return entries.slice(offset, offset + limit);
}
const response = await api.get<JournalEntry[]>("/journals/me/entries", {
params,
});
return response.data;
}
/**
* Get a specific journal entry by ID
*/
async function getEntry(entryId: string): Promise<JournalEntry> {
if (isMockMode()) {
const entry = mockJournalEntries.find((e) => e.id === entryId);
if (entry) return entry as JournalEntry;
throw new Error("Entry not found");
}
const response = await api.get<JournalEntry>(`/journals/entries/${entryId}`);
return response.data;
}
/**
* Delete a journal entry
*/
async function deleteEntry(entryId: string): Promise<void> {
if (isMockMode()) {
const idx = mockJournalEntries.findIndex((e) => e.id === entryId);
if (idx !== -1) mockJournalEntries.splice(idx, 1);
return;
}
await api.delete(`/journals/entries/${entryId}`);
}
// =============================================================================
// CONVENIENCE ENTRY ENDPOINTS
// =============================================================================
/**
* Add a task reflection entry
*/
async function addTaskReflection(data: {
task_id: string;
title: string;
what_done: string;
what_learned: string;
what_struggled?: string;
next_steps?: string[];
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.TASK_REFLECTION,
title: data.title,
content: JSON.stringify({
what_done: data.what_done,
what_learned: data.what_learned,
what_struggled: data.what_struggled,
next_steps: data.next_steps,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>(
"/journals/me/reflections",
data
);
return response.data;
}
/**
* Add a decision log entry
*/
async function addDecisionLog(data: {
title: string;
context: string;
options: string[];
chosen: string;
rationale: string;
consequences?: string;
task_id?: string;
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.DECISION_LOG,
title: data.title,
content: JSON.stringify({
context: data.context,
options: data.options,
chosen: data.chosen,
rationale: data.rationale,
consequences: data.consequences,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>("/journals/me/decisions", data);
return response.data;
}
/**
* Add a learning entry
*/
async function addLearning(data: {
title: string;
what_learned: string;
how_applied?: string;
source?: string;
task_id?: string;
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.LEARNING,
title: data.title,
content: JSON.stringify({
what_learned: data.what_learned,
how_applied: data.how_applied,
source: data.source,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>("/journals/me/learnings", data);
return response.data;
}
/**
* Add a struggle entry
*/
async function addStruggle(data: {
title: string;
what_struggled: string;
attempted_solutions: string[];
resolution?: string;
help_needed?: string;
task_id?: string;
tags?: string[];
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.STRUGGLE,
title: data.title,
content: JSON.stringify({
what_struggled: data.what_struggled,
attempted_solutions: data.attempted_solutions,
resolution: data.resolution,
help_needed: data.help_needed,
}),
task_id: data.task_id,
tags: data.tags,
});
}
const response = await api.post<JournalEntry>("/journals/me/struggles", data);
return response.data;
}
/**
* Add a general journal entry
*/
async function addNote(data: {
title: string;
content: string;
task_id?: string;
session_id?: string;
tags?: string[];
is_private?: boolean;
}): Promise<JournalEntry> {
if (isMockMode()) {
return createEntry({
type: JournalEntryType.GENERAL,
title: data.title,
content: data.content,
task_id: data.task_id,
session_id: data.session_id,
tags: data.tags,
is_private: data.is_private,
});
}
const response = await api.post<JournalEntry>("/journals/me/notes", data);
return response.data;
}
// =============================================================================
// ANALYTICS ENDPOINTS
// =============================================================================
/**
* Get statistics for the current agent's journal
*/
async function getMyStats(): Promise<JournalStats> {
if (isMockMode()) {
const entries = mockJournalEntries as JournalEntry[];
return {
total_entries: entries.length,
entries_by_type: {
task_reflection: entries.filter((e) => e.type === JournalEntryType.TASK_REFLECTION).length,
decision_log: entries.filter((e) => e.type === JournalEntryType.DECISION_LOG).length,
learning: entries.filter((e) => e.type === JournalEntryType.LEARNING).length,
struggle: entries.filter((e) => e.type === JournalEntryType.STRUGGLE).length,
general: entries.filter((e) => e.type === JournalEntryType.GENERAL).length,
},
last_entry_at: entries[0]?.created_at ?? null,
has_summary: false,
};
}
const response = await api.get<JournalStats>("/journals/me/stats");
return response.data;
}
/**
* Get growth metrics for the current agent
*/
async function getMyGrowthMetrics(): Promise<GrowthMetrics> {
if (isMockMode()) {
const entries = mockJournalEntries as JournalEntry[];
return {
total_reflections: entries.filter((e) => e.type === JournalEntryType.TASK_REFLECTION).length,
total_learnings: entries.filter((e) => e.type === JournalEntryType.LEARNING).length,
total_struggles: entries.filter((e) => e.type === JournalEntryType.STRUGGLE).length,
total_decisions: entries.filter((e) => e.type === JournalEntryType.DECISION_LOG).length,
struggle_resolution_rate: 0.7,
learning_frequency: 3.5,
sentiment_trend: "improving",
};
}
const response = await api.get<GrowthMetrics>("/journals/me/growth");
return response.data;
}
// =============================================================================
// SEARCH ENDPOINTS
// =============================================================================
/**
* Semantic search through journal entries
*/
async function searchEntries(
query: string,
topK: number = 10
): Promise<JournalEntry[]> {
if (isMockMode()) {
// Simple text search for mock mode
const queryLower = query.toLowerCase();
return (mockJournalEntries as JournalEntry[])
.filter(
(e) =>
e.title.toLowerCase().includes(queryLower) ||
e.content.toLowerCase().includes(queryLower)
)
.slice(0, topK);
}
const response = await api.post<JournalEntry[]>("/journals/me/search", {
query,
top_k: topK,
});
return response.data;
}
// =============================================================================
// EXPORT API
// =============================================================================
export const journalsApi = {
// Journal
getMyJournal,
getJournalByAgent,
// Entry CRUD
createEntry,
listMyEntries,
listAgentEntries,
getEntry,
deleteEntry,
// Convenience entry creators
addTaskReflection,
addDecisionLog,
addLearning,
addStruggle,
addNote,
// Analytics
getMyStats,
getMyGrowthMetrics,
// Search
searchEntries,
};
export default journalsApi;
+653
View File
@@ -0,0 +1,653 @@
/**
* Knowledge Base API Client
*
* Semantic search and RAG queries against indexed knowledge.
* Matches backend: roboco/api/routes/optimal.py
*/
import api from "./client";
import {
KBIndexType,
type KBSearchRequest,
type KBSearchResponse,
type KBSearchResult,
type RAGQueryRequest,
type RAGQueryResponse,
type KBStats,
type KBIndexStats,
type RAGHealthResponse,
type MentorAskRequest,
type MentorAskResponse,
type ErrorSearchRequest,
type ErrorSearchResponse,
type ErrorRecordRequest,
type ErrorRecordResponse,
type DecisionCheckRequest,
type DecisionCheckResponse,
type DecisionRecordRequest,
type DecisionRecordResponse,
type StandardsGetRequest,
type StandardsGetResponse,
type ValidateActionRequest,
type ValidateActionResponse,
type CodeReviewRequest,
type CodeReviewResponse,
type LearningRecordRequest,
type LearningRecordResponse,
type LearningSearchRequest,
type ProactiveContextRequest,
type ProactiveContextResponse,
type TokenEstimateRequest,
type TokenEstimateResponse,
type RefreshIndexRequest,
type RefreshIndexResponse,
type ClearIndexResponse,
type ReindexResponse,
type ReindexRequest,
type IndexStalenessResponse,
} from "@/types";
import { isMockMode } from "@/lib/mock-data";
// =============================================================================
// MOCK DATA
// =============================================================================
const mockSearchResults: KBSearchResult[] = [
{
content: "The TaskService handles all task lifecycle operations including creation, assignment, status transitions, and completion tracking...",
source: "roboco/services/tasks.py",
score: 0.92,
index_type: KBIndexType.CODE,
metadata: { language: "python", lines: "45-120" },
},
{
content: "## Task Lifecycle\n\nTasks follow a strict state machine from PENDING through IN_PROGRESS to COMPLETED. Each transition requires specific conditions...",
source: "docs/architecture/task-lifecycle.md",
score: 0.88,
index_type: KBIndexType.DOCUMENTATION,
metadata: { section: "Architecture" },
},
{
content: "BE-DEV-1: I've completed the task validation logic. The acceptance criteria now require at least one item before a task can be created.",
source: "channel:backend/session:abc123",
score: 0.75,
index_type: KBIndexType.CONVERSATIONS,
metadata: { agent: "be-dev-1", timestamp: "2024-01-15T10:30:00Z" },
},
{
content: "Learned that proper error boundaries in the task form prevent cascading failures. Applied this pattern to all form components.",
source: "journal:be-dev-1/entry:xyz789",
score: 0.71,
index_type: KBIndexType.JOURNALS,
metadata: { agent: "be-dev-1", type: "learning" },
},
];
const mockStats: KBStats = {
indexes: [
{ index_type: KBIndexType.CODE, document_count: 1250, chunk_count: 8500, last_updated: "2024-01-15T12:00:00Z" },
{ index_type: KBIndexType.DOCUMENTATION, document_count: 45, chunk_count: 320, last_updated: "2024-01-15T11:30:00Z" },
{ index_type: KBIndexType.CONVERSATIONS, document_count: 890, chunk_count: 4200, last_updated: "2024-01-15T12:15:00Z" },
{ index_type: KBIndexType.JOURNALS, document_count: 156, chunk_count: 780, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.ERRORS, document_count: 45, chunk_count: 180, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.STANDARDS, document_count: 12, chunk_count: 60, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.DECISIONS, document_count: 78, chunk_count: 390, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.REVIEWS, document_count: 234, chunk_count: 1170, last_updated: "2024-01-15T10:00:00Z" },
{ index_type: KBIndexType.LEARNINGS, document_count: 89, chunk_count: 445, last_updated: "2024-01-15T10:00:00Z" },
],
total_documents: 2799,
total_chunks: 15245,
};
// Backend returns stats as dict, we need to transform to array
interface BackendIndexStats {
initialized: boolean;
indexes: Record<string, { document_count: number; chunk_count: number; last_updated: string | null }>;
}
function transformStatsResponse(backendStats: BackendIndexStats): KBStats {
const indexes: KBIndexStats[] = Object.entries(backendStats.indexes || {}).map(([indexType, stats]) => ({
index_type: indexType as KBIndexType,
document_count: stats.document_count ?? 0,
chunk_count: stats.chunk_count ?? 0,
last_updated: stats.last_updated ?? null,
}));
const total_documents = indexes.reduce((sum, idx) => sum + idx.document_count, 0);
const total_chunks = indexes.reduce((sum, idx) => sum + idx.chunk_count, 0);
return { indexes, total_documents, total_chunks };
}
// =============================================================================
// SEARCH ENDPOINTS
// =============================================================================
/**
* Semantic search across indexed content
*/
async function search(params: KBSearchRequest): Promise<KBSearchResponse> {
if (isMockMode()) {
// Filter by index types if specified
let results = [...mockSearchResults];
if (params.index_types && params.index_types.length > 0) {
results = results.filter((r) => params.index_types!.includes(r.index_type));
}
// Filter by min score
if (params.min_score) {
results = results.filter((r) => r.score >= params.min_score!);
}
// Limit results
const topK = params.top_k ?? 10;
results = results.slice(0, topK);
return {
results,
total: results.length,
query: params.query,
};
}
const response = await api.post<KBSearchResponse>("/optimal/kb/search", params);
return response.data;
}
// =============================================================================
// RAG ENDPOINTS
// =============================================================================
/**
* RAG query - ask a question, get AI answer with citations
*/
async function ragQuery(params: RAGQueryRequest): Promise<RAGQueryResponse> {
if (isMockMode()) {
// Simulate RAG response with mock data
return {
answer: `Based on the indexed knowledge, here's what I found about "${params.question}":\n\nThe system uses a structured approach where tasks follow a defined lifecycle. Each task starts in PENDING state and can transition through various states like IN_PROGRESS, BLOCKED, and eventually COMPLETED or CANCELLED.\n\nKey points:\n- Tasks require acceptance criteria before creation\n- State transitions are validated by the TaskService\n- Agents can claim and work on tasks based on their role and team`,
citations: mockSearchResults.slice(0, 3).map((r) => ({
content: r.content,
source: r.source,
score: r.score,
index_type: r.index_type,
metadata: r.metadata,
})),
query: params.question,
context_used: 3,
};
}
// Transform frontend params to backend format
const backendParams = {
query: params.question,
index_types: params.index_types,
top_k: params.max_context_chunks ?? 5,
};
const response = await api.post<RAGQueryResponse>("/optimal/rag/query", backendParams);
return response.data;
}
/**
* Get context for a query without generating an answer
*/
async function getContext(params: RAGQueryRequest): Promise<KBSearchResult[]> {
if (isMockMode()) {
return mockSearchResults.slice(0, params.max_context_chunks ?? 5);
}
// Transform frontend params to backend format
const backendParams = {
query: params.question,
index_types: params.index_types,
top_k: params.max_context_chunks ?? 5,
};
const response = await api.post<{ context: KBSearchResult[] }>("/optimal/rag/context", backendParams);
return response.data.context;
}
// =============================================================================
// STATS ENDPOINTS
// =============================================================================
/**
* Get index statistics
*/
async function getStats(): Promise<KBStats> {
if (isMockMode()) {
return mockStats;
}
const response = await api.get<BackendIndexStats>("/optimal/stats");
return transformStatsResponse(response.data);
}
/**
* Get stats for a specific index type
*/
async function getIndexStats(indexType: KBIndexType): Promise<KBIndexStats> {
if (isMockMode()) {
const stats = mockStats.indexes.find((i) => i.index_type === indexType);
if (stats) return stats;
return {
index_type: indexType,
document_count: 0,
chunk_count: 0,
last_updated: null,
};
}
const response = await api.get<KBIndexStats>(`/optimal/stats/${indexType}`);
return response.data;
}
// =============================================================================
// BROWSE ENDPOINTS
// =============================================================================
/**
* List documents in a specific index (for browsing)
*/
async function listDocuments(
indexType: KBIndexType,
params?: { limit?: number; offset?: number }
): Promise<{ documents: Array<{ id: string; source: string; indexed_at: string; metadata?: Record<string, unknown> }>; total: number }> {
if (isMockMode()) {
// Generate mock document list
const docs = mockSearchResults
.filter((r) => r.index_type === indexType)
.map((r, idx) => ({
id: `doc-${indexType}-${idx}`,
source: r.source,
indexed_at: "2024-01-15T12:00:00Z",
}));
return { documents: docs, total: docs.length };
}
const response = await api.get<{ documents: Array<{ id: string; source: string; indexed_at: string; metadata?: Record<string, unknown> }>; total: number; index_type: string }>(
`/optimal/kb/${indexType}/documents`,
{ params }
);
return { documents: response.data.documents, total: response.data.total };
}
// =============================================================================
// HEALTH ENDPOINT
// =============================================================================
/**
* Get RAG system health status
*/
async function getHealth(): Promise<RAGHealthResponse> {
if (isMockMode()) {
return {
healthy: true,
embedding_status: "healthy",
llm_status: "healthy",
vector_store_status: "healthy",
details: {},
};
}
const response = await api.get<RAGHealthResponse>("/optimal/health");
return response.data;
}
// =============================================================================
// INDEX MANAGEMENT
// =============================================================================
/**
* Delete/clear an index
*/
async function deleteIndex(indexType: KBIndexType): Promise<ClearIndexResponse> {
if (isMockMode()) {
return { status: "cleared", index_type: indexType };
}
const response = await api.delete<ClearIndexResponse>(`/optimal/kb/${indexType}`);
return response.data;
}
/**
* Refresh an index with updated sources
*/
async function refreshIndex(request: RefreshIndexRequest): Promise<RefreshIndexResponse> {
if (isMockMode()) {
return { status: "refreshed", index_type: request.index_type, sources: request.sources };
}
const response = await api.post<RefreshIndexResponse>("/optimal/kb/refresh", request);
return response.data;
}
/**
* Trigger full reindex with detailed reporting
*
* @param request - Optional parameters for reindexing
* @param request.force - Force reindex even if indexes have content
* @param request.timeout_seconds - Max time to wait (default: 300s)
* @returns Detailed report of what was indexed, failed, and skipped
*/
async function reindexAll(request?: ReindexRequest): Promise<ReindexResponse> {
if (isMockMode()) {
return {
status: "reindexed",
code: {
index_type: "code",
total_attempted: 100,
successful: 98,
failed: 2,
skipped: 0,
success_rate: 98.0,
has_failures: true,
failed_sources: [
["/path/to/file1.py", "UTF-8 decode error"],
["/path/to/file2.py", "File too large"],
],
duration_seconds: 45.2,
},
documentation: {
index_type: "documentation",
total_attempted: 50,
successful: 50,
failed: 0,
skipped: 0,
success_rate: 100.0,
has_failures: false,
failed_sources: [],
duration_seconds: 12.5,
},
overall_success: true,
warnings: ["Code indexing had 2 failures"],
// Legacy fields
code_count: 98,
docs_count: 50,
};
}
const response = await api.post<ReindexResponse>("/optimal/kb/reindex", null, {
params: {
force: request?.force ?? false,
timeout_seconds: request?.timeout_seconds ?? 300,
},
});
return response.data;
}
/**
* Check if indexes are stale (source files modified after last indexing)
*
* @returns Staleness info for each index type
*/
async function checkStaleness(): Promise<IndexStalenessResponse> {
if (isMockMode()) {
return {
needs_reindex: true,
stale_indexes: ["code"],
details: {
code: {
status: "stale",
last_indexed: new Date(Date.now() - 86400000).toISOString(),
stale_file_count: 5,
stale_files_sample: [
"/roboco/services/task.py",
"/roboco/services/optimal.py",
],
recommendation: "Run /kb/reindex?force=true to update",
},
documentation: {
status: "current",
last_indexed: new Date().toISOString(),
indexed_sources_count: 50,
},
},
};
}
const response = await api.get<IndexStalenessResponse>(
"/optimal/stats/staleness"
);
return response.data;
}
// =============================================================================
// MENTOR ENDPOINTS
// =============================================================================
/**
* Ask the mentor for help
*/
async function askMentor(request: MentorAskRequest): Promise<MentorAskResponse> {
if (isMockMode()) {
return {
answer: `Here's what I found about "${request.question}":\n\nBased on our organizational knowledge, I recommend following the established patterns and consulting the relevant documentation.`,
sources: mockSearchResults.slice(0, 2),
conversation_id: "conv-" + Date.now(),
suggested_followups: ["Can you elaborate?", "What are the alternatives?"],
};
}
const response = await api.post<MentorAskResponse>("/optimal/mentor/ask", request);
return response.data;
}
// =============================================================================
// ERROR ENDPOINTS
// =============================================================================
/**
* Search for known error solutions
*/
async function searchErrors(request: ErrorSearchRequest): Promise<ErrorSearchResponse> {
if (isMockMode()) {
return { results: [], total: 0 };
}
const response = await api.post<ErrorSearchResponse>("/optimal/errors/search", request);
return response.data;
}
/**
* Record an error solution
*/
async function recordError(request: ErrorRecordRequest): Promise<ErrorRecordResponse> {
if (isMockMode()) {
return { error_id: "err-" + Date.now(), status: "recorded" };
}
const response = await api.post<ErrorRecordResponse>("/optimal/errors/record", request);
return response.data;
}
// =============================================================================
// DECISION ENDPOINTS
// =============================================================================
/**
* Check if a similar decision was made before
*/
async function checkDecision(request: DecisionCheckRequest): Promise<DecisionCheckResponse> {
if (isMockMode()) {
return { has_precedent: false, decisions: [], recommendation: "No similar decisions found" };
}
const response = await api.post<DecisionCheckResponse>("/optimal/decisions/check", request);
return response.data;
}
/**
* Record a decision for future reference
*/
async function recordDecision(request: DecisionRecordRequest): Promise<DecisionRecordResponse> {
if (isMockMode()) {
return { decision_id: "dec-" + Date.now(), status: "recorded" };
}
const response = await api.post<DecisionRecordResponse>("/optimal/decisions/record", request);
return response.data;
}
// =============================================================================
// STANDARDS ENDPOINTS
// =============================================================================
/**
* Get standards for a domain
*/
async function getStandards(request: StandardsGetRequest): Promise<StandardsGetResponse> {
if (isMockMode()) {
return { standards: [], total: 0 };
}
const response = await api.post<StandardsGetResponse>("/optimal/standards/get", request);
return response.data;
}
/**
* Validate an action against standards
*/
async function validateAction(request: ValidateActionRequest): Promise<ValidateActionResponse> {
if (isMockMode()) {
return { allowed: true, violations: [], warnings: [], relevant_standards: [] };
}
const response = await api.post<ValidateActionResponse>("/optimal/standards/validate", request);
return response.data;
}
// =============================================================================
// CODE REVIEW ENDPOINTS
// =============================================================================
/**
* Review code against standards
*/
async function reviewCode(request: CodeReviewRequest): Promise<CodeReviewResponse> {
if (isMockMode()) {
return {
file_path: request.file_path,
approved: true,
score: 85,
comments: [],
standards_checked: ["coding-standards"],
similar_reviews: [],
};
}
const response = await api.post<CodeReviewResponse>("/optimal/review/code", request);
return response.data;
}
// =============================================================================
// LEARNING ENDPOINTS
// =============================================================================
/**
* Record a learning for knowledge sharing
*/
async function recordLearning(request: LearningRecordRequest): Promise<LearningRecordResponse> {
if (isMockMode()) {
return { learning_id: "learn-" + Date.now(), status: "recorded" };
}
const response = await api.post<LearningRecordResponse>("/optimal/learnings/record", request);
return response.data;
}
/**
* Search for relevant learnings
*/
async function searchLearnings(request: LearningSearchRequest): Promise<KBSearchResponse> {
if (isMockMode()) {
return { results: [], total: 0, query: request.query };
}
const response = await api.post<KBSearchResponse>("/optimal/learnings/search", request);
return response.data;
}
// =============================================================================
// PROACTIVE CONTEXT ENDPOINTS
// =============================================================================
/**
* Get proactive context for a task
*/
async function getProactiveContext(request: ProactiveContextRequest): Promise<ProactiveContextResponse> {
if (isMockMode()) {
return {
task_id: request.task_id,
similar_tasks: [],
relevant_learnings: [],
code_patterns: [],
applicable_standards: [],
recent_decisions: [],
known_issues: [],
summary: "No relevant context found for this task.",
};
}
const response = await api.post<ProactiveContextResponse>("/optimal/context/proactive", request);
return response.data;
}
// =============================================================================
// TOKEN ESTIMATION
// =============================================================================
/**
* Estimate token count for content
*/
async function estimateTokens(request: TokenEstimateRequest): Promise<TokenEstimateResponse> {
if (isMockMode()) {
return {
token_count: Math.ceil(request.content.length / 4),
model: request.model || "claude-sonnet-4-20250514",
content_length: request.content.length,
};
}
const response = await api.post<TokenEstimateResponse>("/optimal/tokens/estimate", request);
return response.data;
}
// =============================================================================
// EXPORT API
// =============================================================================
export const knowledgeBaseApi = {
// Search
search,
// RAG
ragQuery,
getContext,
// Stats & Health
getStats,
getIndexStats,
getHealth,
// Browse
listDocuments,
// Index Management
deleteIndex,
refreshIndex,
reindexAll,
checkStaleness,
// Mentor
askMentor,
// Errors
searchErrors,
recordError,
// Decisions
checkDecision,
recordDecision,
// Standards
getStandards,
validateAction,
// Code Review
reviewCode,
// Learnings
recordLearning,
searchLearnings,
// Proactive Context
getProactiveContext,
// Token Estimation
estimateTokens,
};
export default knowledgeBaseApi;
+123
View File
@@ -0,0 +1,123 @@
import api from "./client";
import type { Message, MessageType } from "@/types";
import { isMockMode, getMockMessages, AGENT_IDS, CHANNEL_IDS } from "@/lib/mock-data";
// Store for mock messages (persists during session)
let mockMessagesStore: Message[] | null = null;
const getMessages = (): Message[] => {
if (!mockMessagesStore) {
mockMessagesStore = getMockMessages() as Message[];
}
return mockMessagesStore;
};
export const messagesApi = {
// List messages for a session
listBySession: async (
sessionId: string,
limit: number = 50,
before?: string,
after?: string
): Promise<{ items: Message[]; has_more: boolean }> => {
if (isMockMode()) {
let messages = getMessages().filter(
(m) => m.session_id === sessionId
);
if (before) {
messages = messages.filter(
(m) => new Date(m.timestamp) < new Date(before)
);
}
if (after) {
messages = messages.filter(
(m) => new Date(m.timestamp) > new Date(after)
);
}
return {
items: messages.slice(0, limit),
has_more: messages.length > limit,
};
}
const { data } = await api.get<{ items: Message[]; has_more: boolean }>("/messages", {
params: { session_id: sessionId, limit, before, after },
});
return data;
},
// Get message by ID
get: async (messageId: string): Promise<Message> => {
if (isMockMode()) {
const message = getMessages().find((m) => m.id === messageId);
if (message) return message;
throw new Error("Message not found");
}
const { data } = await api.get<Message>("/messages/" + messageId);
return data;
},
// Send a message
send: async (sessionId: string, content: string, type: string = "dialogue"): Promise<Message> => {
if (isMockMode()) {
const newMessage: Message = {
id: `msg-${Date.now()}`,
agent_id: AGENT_IDS.ceo,
channel_id: CHANNEL_IDS.backendCell,
group_id: `msg-${Date.now()}`,
session_id: sessionId,
type: type as MessageType,
content,
content_length: content.length,
is_reply: false,
reply_to: null,
mentions: [],
task_id: null,
commit_ref: null,
timestamp: new Date().toISOString(),
edited_at: null,
was_edited: false,
};
getMessages().push(newMessage);
return newMessage;
}
const { data } = await api.post<Message>("/messages", {
session_id: sessionId,
content,
type,
});
return data;
},
// Edit a message
edit: async (messageId: string, content: string): Promise<Message> => {
if (isMockMode()) {
const messages = getMessages();
const idx = messages.findIndex((m) => m.id === messageId);
if (idx !== -1) {
const message = messages[idx];
const editedMessage: Message = {
...message,
content,
content_length: content.length,
edited_at: new Date().toISOString(),
was_edited: true,
};
messages[idx] = editedMessage;
return editedMessage;
}
throw new Error("Message not found");
}
const { data } = await api.patch<Message>("/messages/" + messageId, { content });
return data;
},
// Delete a message
delete: async (messageId: string): Promise<void> => {
if (isMockMode()) {
const messages = getMessages();
const idx = messages.findIndex((m) => m.id === messageId);
if (idx !== -1) messages.splice(idx, 1);
return;
}
await api.delete("/messages/" + messageId);
},
};
+114
View File
@@ -0,0 +1,114 @@
import api from "./client";
import type { Notification, NotificationListResponse, NotificationType, NotificationPriority } from "@/types";
import { isMockMode, mockNotifications } from "@/lib/mock-data";
export interface NotificationFilters {
type?: NotificationType;
priority?: NotificationPriority;
unread_only?: boolean;
pending_ack_only?: boolean;
}
export const notificationsApi = {
// List notifications for CEO
list: async (filters?: NotificationFilters): Promise<NotificationListResponse> => {
if (isMockMode()) {
let notifications = [...mockNotifications] as Notification[];
if (filters?.type) {
notifications = notifications.filter((n) => n.type === filters.type);
}
if (filters?.priority) {
notifications = notifications.filter((n) => n.priority === filters.priority);
}
if (filters?.unread_only) {
notifications = notifications.filter((n) => !n.is_read);
}
if (filters?.pending_ack_only) {
notifications = notifications.filter((n) => n.requires_ack && !n.is_acknowledged);
}
return {
items: notifications,
total: notifications.length,
unread_count: notifications.filter((n) => !n.is_read).length,
pending_ack_count: notifications.filter((n) => n.requires_ack && !n.is_acknowledged).length,
};
}
// Backend uses type_filter, priority_filter parameter names
const params: Record<string, unknown> = {};
if (filters?.type) params.type_filter = filters.type;
if (filters?.priority) params.priority_filter = filters.priority;
if (filters?.unread_only) params.unread_only = filters.unread_only;
if (filters?.pending_ack_only) params.pending_ack_only = filters.pending_ack_only;
const { data } = await api.get<NotificationListResponse>("/notifications", { params });
return data;
},
// Get notification by ID
get: async (notificationId: string): Promise<Notification> => {
if (isMockMode()) {
const notification = mockNotifications.find((n) => n.id === notificationId);
if (notification) return notification as Notification;
throw new Error("Notification not found");
}
const { data } = await api.get<Notification>("/notifications/" + notificationId);
return data;
},
// Mark notification as read (backend returns 204 No Content)
markRead: async (notificationId: string): Promise<void> => {
if (isMockMode()) {
const idx = mockNotifications.findIndex((n) => n.id === notificationId);
if (idx !== -1) {
const notification = mockNotifications[idx];
mockNotifications[idx] = { ...notification, is_read: true };
}
return;
}
await api.post("/notifications/" + notificationId + "/read");
},
// Acknowledge notification (backend uses /ack)
acknowledge: async (notificationId: string): Promise<Notification> => {
if (isMockMode()) {
const idx = mockNotifications.findIndex((n) => n.id === notificationId);
if (idx !== -1) {
const notification = mockNotifications[idx];
const ackedNotification = {
...notification,
is_acknowledged: true,
is_fully_acknowledged: true,
};
mockNotifications[idx] = ackedNotification;
return ackedNotification as Notification;
}
throw new Error("Notification not found");
}
const { data } = await api.post<Notification>(
"/notifications/" + notificationId + "/ack"
);
return data;
},
// Mark all as read
markAllRead: async (): Promise<void> => {
if (isMockMode()) {
for (let i = 0; i < mockNotifications.length; i++) {
mockNotifications[i] = { ...mockNotifications[i], is_read: true };
}
return;
}
// Get all unread notifications
const { data } = await api.get<{ items: Notification[] }>("/notifications", {
params: { unread_only: true },
});
// Mark each as read
await Promise.all(
data.items.map((n) =>
api.post("/notifications/" + n.id + "/read").catch(() => {
// Ignore individual failures
})
)
);
},
};
+104
View File
@@ -0,0 +1,104 @@
import api from "./client";
import type { OrchestratorStatus, AgentStatusResponse, WaitingAgent } from "@/types";
import { isMockMode, mockOrchestratorStatus, mockWaitingAgents } from "@/lib/mock-data";
export interface SpawnAgentRequest {
task_id?: string;
initial_prompt?: string;
}
export const orchestratorApi = {
// Get orchestrator status
getStatus: async (): Promise<OrchestratorStatus> => {
if (isMockMode()) {
return mockOrchestratorStatus as OrchestratorStatus;
}
const { data } = await api.get<OrchestratorStatus>("/orchestrator/status");
return data;
},
// Get specific agent status
getAgentStatus: async (agentId: string): Promise<AgentStatusResponse> => {
if (isMockMode()) {
const found = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (found) return found as AgentStatusResponse;
throw new Error("Agent not found");
}
const { data } = await api.get<AgentStatusResponse>("/orchestrator/agents/" + agentId);
return data;
},
// Get waiting agents
getWaitingAgents: async (): Promise<WaitingAgent[]> => {
if (isMockMode()) {
return mockWaitingAgents as WaitingAgent[];
}
const { data } = await api.get<WaitingAgent[]>("/orchestrator/waiting");
return data;
},
// Spawn an agent
spawn: async (agentId: string, request?: SpawnAgentRequest): Promise<AgentStatusResponse> => {
if (isMockMode()) {
// Find or create agent in mock status
const existing = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (existing) {
existing.state = "running";
// Mock data uses string, so use empty string for no task
(existing as { task_id: string }).task_id = request?.task_id ?? "";
return existing as AgentStatusResponse;
}
const newAgent = {
agent_id: agentId,
state: "running",
task_id: request?.task_id ?? "",
error_count: 0,
started_at: new Date().toISOString(),
waiting_for: null as string | null,
};
mockOrchestratorStatus.agents.push(newAgent);
return newAgent as AgentStatusResponse;
}
const { data } = await api.post<AgentStatusResponse>(
"/orchestrator/agents/" + agentId + "/spawn",
{ agent_id: agentId, ...request }
);
return data;
},
// Stop an agent
stop: async (agentId: string, graceful: boolean = true): Promise<void> => {
if (isMockMode()) {
const agent = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (agent) {
agent.state = "idle";
(agent as { task_id: string }).task_id = "";
}
return;
}
await api.post("/orchestrator/agents/" + agentId + "/stop", null, {
params: { graceful },
});
},
// Resolve a waiting agent
resolveWait: async (agentId: string, resolution: string): Promise<AgentStatusResponse> => {
if (isMockMode()) {
// Remove from waiting agents
const idx = mockWaitingAgents.findIndex(a => a.agent_id === agentId);
if (idx !== -1) mockWaitingAgents.splice(idx, 1);
// Update agent status
const agent = mockOrchestratorStatus.agents.find(a => a.agent_id === agentId);
if (agent) {
agent.state = "running";
return agent as AgentStatusResponse;
}
throw new Error("Agent not found");
}
const { data } = await api.post<AgentStatusResponse>(
"/orchestrator/agents/" + agentId + "/resolve-wait",
{ resolution }
);
return data;
},
};
+204
View File
@@ -0,0 +1,204 @@
import api from "./client";
import type {
Project,
ProjectCreate,
ProjectUpdate,
ProjectSummary,
Team,
} from "@/types";
import { isMockMode } from "@/lib/mock-data";
// Mock data for offline mode
const mockProjects: Project[] = [];
export interface ProjectFilters {
assigned_cell?: Team;
active_only?: boolean;
limit?: number;
offset?: number;
}
export const projectsApi = {
// List projects with optional filters
list: async (filters?: ProjectFilters): Promise<ProjectSummary[]> => {
if (isMockMode()) {
let projects = [...mockProjects];
if (filters?.assigned_cell) {
projects = projects.filter((p) => p.assigned_cell === filters.assigned_cell);
}
if (filters?.active_only) {
projects = projects.filter((p) => p.is_active);
}
return projects.map((p) => ({
id: p.id,
name: p.name,
slug: p.slug,
git_url: p.git_url,
assigned_cell: p.assigned_cell,
is_active: p.is_active,
has_workspace: !!p.workspace_path,
has_git_token: false, // Mock mode has no tokens
}));
}
const params = new URLSearchParams();
if (filters?.assigned_cell) params.append("assigned_cell", filters.assigned_cell);
if (filters?.active_only !== undefined) params.append("active_only", String(filters.active_only));
if (filters?.limit) params.append("limit", String(filters.limit));
if (filters?.offset) params.append("offset", String(filters.offset));
const url = "/projects?" + params.toString();
const { data } = await api.get<ProjectSummary[]>(url);
return data;
},
// Get single project
get: async (projectId: string): Promise<Project> => {
if (isMockMode()) {
const project = mockProjects.find((p) => p.id === projectId);
if (!project) throw new Error("Project not found");
return project;
}
const { data } = await api.get<Project>("/projects/" + projectId);
return data;
},
// Create project (PM only)
create: async (project: ProjectCreate): Promise<Project> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newProject: Project = {
id: `project-${Date.now()}`,
name: project.name,
slug: project.slug,
git_url: project.git_url,
default_branch: project.default_branch ?? "main",
protected_branches: project.protected_branches ?? ["main", "master"],
assigned_cell: project.assigned_cell,
has_git_token: !!project.git_token, // Mock token status
is_active: true,
test_command: project.test_command ?? null,
lint_command: project.lint_command ?? null,
format_command: project.format_command ?? null,
typecheck_command: project.typecheck_command ?? null,
build_command: project.build_command ?? null,
workspace_path: null,
last_synced_at: null,
head_commit: null,
created_by: "mock-user",
created_at: now,
updated_at: null,
};
mockProjects.push(newProject);
return newProject;
}
const { data } = await api.post<Project>("/projects", project);
return data;
},
// Update project (PM only)
update: async (projectId: string, updates: ProjectUpdate): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = { ...mockProjects[idx], ...updates, updated_at: now };
return mockProjects[idx];
}
const { data } = await api.patch<Project>("/projects/" + projectId, updates);
return data;
},
// Set workspace path for local development
setWorkspace: async (projectId: string, workspacePath: string): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = { ...mockProjects[idx], workspace_path: workspacePath, updated_at: now };
return mockProjects[idx];
}
const { data } = await api.post<Project>("/projects/" + projectId + "/workspace", { workspace_path: workspacePath });
return data;
},
// Update sync state (for tracking git status)
updateSyncState: async (
projectId: string,
headCommit: string
): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = {
...mockProjects[idx],
head_commit: headCommit,
last_synced_at: now,
updated_at: now,
};
return mockProjects[idx];
}
const { data } = await api.post<Project>("/projects/" + projectId + "/sync-state", {
head_commit: headCommit,
});
return data;
},
// Deactivate project (soft delete)
deactivate: async (projectId: string): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = { ...mockProjects[idx], is_active: false, updated_at: now };
return mockProjects[idx];
}
const { data } = await api.patch<Project>("/projects/" + projectId, { is_active: false });
return data;
},
// Delete project permanently
delete: async (projectId: string): Promise<void> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx !== -1) mockProjects.splice(idx, 1);
return;
}
await api.delete("/projects/" + projectId);
},
// Grant agent access to project
grantAccess: async (projectId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.post("/projects/" + projectId + "/access/" + agentId);
},
// Revoke agent access from project
revokeAccess: async (projectId: string, agentId: string): Promise<void> => {
if (isMockMode()) {
return;
}
await api.delete("/projects/" + projectId + "/access/" + agentId);
},
// Trigger git sync for project
sync: async (projectId: string): Promise<Project> => {
if (isMockMode()) {
const idx = mockProjects.findIndex((p) => p.id === projectId);
if (idx === -1) throw new Error("Project not found");
const now = new Date().toISOString();
mockProjects[idx] = {
...mockProjects[idx],
last_synced_at: now,
updated_at: now,
};
return mockProjects[idx];
}
const { data } = await api.post<Project>("/projects/" + projectId + "/sync");
return data;
},
};
+68
View File
@@ -0,0 +1,68 @@
import api from "./client";
import type {
AssignmentScope,
ModelProvider,
} from "@/types";
// Matches the backend's CatalogEntryResponse.
export interface CatalogEntry {
model_name: string;
provider_type: ModelProvider;
display_name: string;
}
export interface OllamaKeyStatus {
has_key: boolean;
enabled: boolean;
}
export interface ModelAssignment {
id: string;
scope: AssignmentScope;
scope_value: string | null;
provider_type: ModelProvider;
model_name: string;
}
export type RoutingMode = "anthropic" | "ollama" | "mix";
export interface ModeSnapshot {
mode: RoutingMode;
assignments: ModelAssignment[];
}
export interface ApplyModePayload {
mode: RoutingMode;
default_model?: string;
// Required when mode=mix: map of agent_slug → model_name.
per_agent?: Record<string, string>;
}
export const providersApi = {
catalog: async (): Promise<CatalogEntry[]> => {
const { data } = await api.get<CatalogEntry[]>("/providers/catalog");
return data;
},
getOllamaKey: async (): Promise<OllamaKeyStatus> => {
const { data } = await api.get<OllamaKeyStatus>("/providers/ollama-key");
return data;
},
setOllamaKey: async (apiKey: string): Promise<OllamaKeyStatus> => {
const { data } = await api.put<OllamaKeyStatus>("/providers/ollama-key", {
api_key: apiKey,
});
return data;
},
getMode: async (): Promise<ModeSnapshot> => {
const { data } = await api.get<ModeSnapshot>("/providers");
return data;
},
applyMode: async (payload: ApplyModePayload): Promise<ModeSnapshot> => {
const { data } = await api.post<ModeSnapshot>("/providers", payload);
return data;
},
};
+182
View File
@@ -0,0 +1,182 @@
import api from "./client";
import type { Session } from "@/types";
import { SessionStatus, SessionScope } from "@/types";
import { isMockMode, mockSessions } from "@/lib/mock-data";
// Session-Task link response from API
export interface SessionTaskLinkResponse {
id: string;
session_id: string;
task_id: string;
is_primary: boolean;
relationship_type: string;
added_at: string;
added_by: string | null;
}
export interface SessionCreate {
group_id: string;
scope?: string;
}
export const sessionsApi = {
// List sessions for a group
listByGroup: async (groupId: string, limit: number = 50): Promise<Session[]> => {
if (isMockMode()) {
return (mockSessions as Session[]).slice(0, limit);
}
const { data } = await api.get<{ items: Session[]; total: number }>("/sessions", {
params: { group_id: groupId, limit },
});
return data.items;
},
// Get session by ID
get: async (sessionId: string): Promise<Session> => {
if (isMockMode()) {
const session = mockSessions.find((s) => s.id === sessionId);
if (session) return session as Session;
throw new Error("Session not found");
}
const { data } = await api.get<Session>("/sessions/" + sessionId);
return data;
},
// Get sessions linked to a task
getForTask: async (taskId: string): Promise<SessionTaskLinkResponse[]> => {
if (isMockMode()) {
return []; // No mock session-task links
}
const { data } = await api.get<SessionTaskLinkResponse[]>("/sessions/for-task/" + taskId);
return data;
},
// Close a session
close: async (sessionId: string): Promise<Session> => {
if (isMockMode()) {
const idx = mockSessions.findIndex((s) => s.id === sessionId);
if (idx !== -1) {
const session = mockSessions[idx] as Session;
const closedSession: Session = {
...session,
status: "closed" as SessionStatus,
closed_at: new Date().toISOString(),
};
(mockSessions as Session[])[idx] = closedSession;
return closedSession;
}
throw new Error("Session not found");
}
const { data } = await api.post<Session>("/sessions/" + sessionId + "/close");
return data;
},
// Link a task to a session (PM only)
linkTask: async (
sessionId: string,
taskId: string,
isPrimary: boolean = false,
relationshipType: string = "discussion"
): Promise<SessionTaskLinkResponse> => {
const { data } = await api.post<SessionTaskLinkResponse>(
"/sessions/" + sessionId + "/add-task",
{
task_id: taskId,
is_primary: isPrimary,
relationship_type: relationshipType,
}
);
return data;
},
// Unlink a task from a session (PM only)
unlinkTask: async (sessionId: string, taskId: string): Promise<void> => {
await api.delete("/sessions/" + sessionId + "/remove-task", {
data: { task_id: taskId },
});
},
// Get tasks linked to a session
getTasksForSession: async (sessionId: string): Promise<SessionTaskLinkResponse[]> => {
// Note: Backend doesn't have a dedicated GET endpoint for session tasks
// Task links are returned with session detail via get()
const session = await sessionsApi.get(sessionId);
// Return empty array if session doesn't have task_links
return (session as Session & { task_links?: SessionTaskLinkResponse[] }).task_links || [];
},
// Create a session for tasks (PM only)
createForTasks: async (
taskIds: string[],
channelSlug: string,
relationshipType: string = "discussion"
): Promise<{ session: Session; links: SessionTaskLinkResponse[] }> => {
const { data } = await api.post<{ session: Session; links: SessionTaskLinkResponse[] }>(
"/sessions/for-tasks",
{
task_ids: taskIds,
channel_slug: channelSlug,
relationship_type: relationshipType,
}
);
return data;
},
// Create a new session directly
create: async (session: SessionCreate): Promise<Session> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newSession: Session = {
id: `session-${Date.now()}`,
group_id: session.group_id,
status: SessionStatus.ACTIVE,
scope: (session.scope as SessionScope) || SessionScope.CELL,
message_count: 0,
total_content_length: 0,
started_at: now,
last_activity_at: now,
closed_at: null,
task_links: [],
};
(mockSessions as Session[]).push(newSession);
return newSession;
}
const { data } = await api.post<Session>("/sessions", session);
return data;
},
// Update a task link in a session
updateTaskLink: async (
sessionId: string,
taskId: string,
updates: { is_primary?: boolean; relationship_type?: string }
): Promise<SessionTaskLinkResponse> => {
if (isMockMode()) {
return {
id: `link-${Date.now()}`,
session_id: sessionId,
task_id: taskId,
is_primary: updates.is_primary ?? false,
relationship_type: updates.relationship_type ?? "discussion",
added_at: new Date().toISOString(),
added_by: null,
};
}
const { data } = await api.post<SessionTaskLinkResponse>(
"/sessions/" + sessionId + "/update-task",
{ task_id: taskId, ...updates }
);
return data;
},
};
export const groupsApi = {
// Get groups for a channel
listByChannel: async (channelId: string): Promise<unknown[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<unknown[]>("/channels/" + channelId + "/groups");
return data;
},
};
+169
View File
@@ -0,0 +1,169 @@
/**
* Stream API Client
*
* API functions for transcription and extraction operations.
*/
import api from "./client";
import { isMockMode } from "@/lib/mock-data";
// =============================================================================
// Types
// =============================================================================
export interface StreamChunkRequest {
channel_name: string;
audio_data: string; // Base64 encoded
chunk_index: number;
is_final?: boolean;
}
export interface StreamChunkResponse {
status: string;
chunk_index: number;
queued: boolean;
}
export interface StreamCompleteRequest {
channel_name: string;
session_id?: string;
}
export interface StreamCompleteResponse {
status: string;
channel_name: string;
total_chunks: number;
transcription_id?: string;
}
export interface ExtractionRequest {
content: string;
extraction_type: "tasks" | "decisions" | "action_items" | "summary";
context?: string;
}
export interface ExtractionResponse {
extraction_type: string;
results: Record<string, unknown>[];
confidence: number;
}
export interface TranscriptionStats {
total_transcriptions: number;
active_channels: number;
chunks_processed: number;
avg_processing_time_ms: number;
}
export interface ChannelPermissions {
channel_name: string;
can_transcribe: boolean;
can_extract: boolean;
rate_limit?: number;
}
// =============================================================================
// API Client
// =============================================================================
export const streamApi = {
// ===========================================================================
// TRANSCRIPTION ENDPOINTS
// ===========================================================================
/**
* Submit an audio chunk for transcription
*/
submitChunk: async (request: StreamChunkRequest): Promise<StreamChunkResponse> => {
if (isMockMode()) {
return {
status: "queued",
chunk_index: request.chunk_index,
queued: true,
};
}
const { data } = await api.post<StreamChunkResponse>("/stream/chunk", request);
return data;
},
/**
* Complete a transcription session
*/
complete: async (request: StreamCompleteRequest): Promise<StreamCompleteResponse> => {
if (isMockMode()) {
return {
status: "completed",
channel_name: request.channel_name,
total_chunks: 10,
transcription_id: `trans-${Date.now()}`,
};
}
const { data } = await api.post<StreamCompleteResponse>("/stream/complete", request);
return data;
},
// ===========================================================================
// EXTRACTION ENDPOINTS
// ===========================================================================
/**
* Extract structured information from content
*/
extract: async (request: ExtractionRequest): Promise<ExtractionResponse> => {
if (isMockMode()) {
return {
extraction_type: request.extraction_type,
results: [],
confidence: 0.85,
};
}
const { data } = await api.post<ExtractionResponse>("/stream/extract", request);
return data;
},
// ===========================================================================
// STATS & PERMISSIONS
// ===========================================================================
/**
* Get transcription statistics
*/
getStats: async (): Promise<TranscriptionStats> => {
if (isMockMode()) {
return {
total_transcriptions: 100,
active_channels: 5,
chunks_processed: 1500,
avg_processing_time_ms: 250,
};
}
const { data } = await api.get<TranscriptionStats>("/stream/stats");
return data;
},
/**
* Get all channel permissions
*/
getPermissions: async (): Promise<ChannelPermissions[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<ChannelPermissions[]>("/stream/permissions");
return data;
},
/**
* Get permissions for a specific channel
*/
getChannelPermissions: async (channelName: string): Promise<ChannelPermissions> => {
if (isMockMode()) {
return {
channel_name: channelName,
can_transcribe: true,
can_extract: true,
};
}
const { data } = await api.get<ChannelPermissions>(`/stream/permissions/channel/${channelName}`);
return data;
},
};
+651
View File
@@ -0,0 +1,651 @@
import api from "./client";
import { TaskStatus, Complexity, TaskNature, TaskType } from "@/types";
import type {
Task,
TaskCreate,
Team,
ProgressRequest,
CheckpointRequest,
CommitRequest,
SoftBlockRequest,
EscalateRequest,
EscalateResponse,
TaskCountResponse,
} from "@/types";
import { isMockMode, mockTasks } from "@/lib/mock-data";
export interface TaskFilters {
status?: TaskStatus;
team?: Team;
limit?: number;
offset?: number;
}
export const tasksApi = {
// List tasks with optional filters
list: async (filters?: TaskFilters): Promise<Task[]> => {
if (isMockMode()) {
let tasks = [...mockTasks];
if (filters?.status) {
tasks = tasks.filter((t) => t.status === filters.status);
}
if (filters?.team) {
tasks = tasks.filter((t) => t.team === filters.team);
}
return tasks;
}
const params = new URLSearchParams();
if (filters?.status) params.append("status", filters.status);
if (filters?.team) params.append("team", filters.team);
if (filters?.limit) params.append("limit", String(filters.limit));
if (filters?.offset) params.append("offset", String(filters.offset));
const url = "/tasks?" + params.toString();
const { data } = await api.get<Task[]>(url);
return data;
},
// Get single task
get: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const task = mockTasks.find((t) => t.id === taskId);
if (!task) throw new Error("Task not found");
return task;
}
const { data } = await api.get<Task>("/tasks/" + taskId);
return data;
},
// Create task
create: async (task: TaskCreate): Promise<Task> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newTask: Task = {
id: `task-${Date.now()}`,
title: task.title,
description: task.description,
team: task.team,
priority: task.priority ?? 2,
sequence: mockTasks.length + 1, // Auto-increment sequence
estimated_complexity: task.estimated_complexity ?? Complexity.MEDIUM,
nature: task.nature ?? TaskNature.TECHNICAL,
task_type: task.task_type ?? TaskType.CODE,
project_id: task.project_id,
docs_complete: false,
pr_created: false,
pm_approvals: {},
acceptance_criteria: task.acceptance_criteria,
parent_task_id: task.parent_task_id ?? null,
target_date: task.target_date ?? null,
status: task.status ?? TaskStatus.PENDING,
dependency_ids: [],
blocker_ids: [],
created_at: now,
updated_at: now,
created_by: "00000000-0000-0000-0000-000000000001", // CEO
assigned_to: null,
claimed_at: null,
started_at: null,
completed_at: null,
self_verified: false,
qa_verified: null,
plan: null,
progress_updates: [],
checkpoints: [],
commits: [],
dev_notes: null,
qa_notes: null,
auditor_notes: null,
quick_context: null,
sessions: [],
branch_name: null,
pr_number: null,
pr_url: null,
};
mockTasks.push(newTask);
return newTask;
}
const { data } = await api.post<Task>("/tasks", task);
return data;
},
// Update task
update: async (taskId: string, updates: Partial<Task>): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
mockTasks[idx] = { ...mockTasks[idx], ...updates, updated_at: new Date().toISOString() };
return mockTasks[idx];
}
const { data } = await api.put<Task>("/tasks/" + taskId, updates);
return data;
},
// Delete task
delete: async (taskId: string): Promise<void> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx !== -1) mockTasks.splice(idx, 1);
return;
}
await api.delete("/tasks/" + taskId);
},
// =========================================================================
// LIFECYCLE ACTIONS
// =========================================================================
claim: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.CLAIMED, claimed_at: now, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/claim");
return data;
},
start: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, started_at: now, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/start");
return data;
},
block: async (taskId: string, blockerId?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const blockerIds = blockerId ? [...mockTasks[idx].blocker_ids, blockerId] : mockTasks[idx].blocker_ids;
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.BLOCKED, blocker_ids: blockerIds, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/block", { blocker_id: blockerId });
return data;
},
unblock: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, blocker_ids: [], updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/unblock");
return data;
},
pause: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PAUSED, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/pause");
return data;
},
resume: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.IN_PROGRESS, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/resume");
return data;
},
verify: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.VERIFYING, self_verified: true, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/verify");
return data;
},
submitQa: async (taskId: string, devNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_QA, dev_notes: devNotes ?? null, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/submit-qa", { dev_notes: devNotes });
return data;
},
passQa: async (taskId: string, qaNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_DOCUMENTATION, qa_verified: true, qa_notes: qaNotes ?? null, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/pass-qa", { qa_notes: qaNotes });
return data;
},
failQa: async (taskId: string, qaNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.NEEDS_REVISION, qa_verified: false, qa_notes: qaNotes ?? null, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/fail-qa", { qa_notes: qaNotes });
return data;
},
complete: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.COMPLETED, completed_at: now, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/complete");
return data;
},
cancel: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.CANCELLED, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/cancel");
return data;
},
// Note: reopen is not supported by backend - use update with status change instead
reopen: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PENDING, completed_at: null, updated_at: now };
return mockTasks[idx];
}
// Backend doesn't have /reopen endpoint - use update instead
const { data } = await api.put<Task>("/tasks/" + taskId, { status: TaskStatus.PENDING });
return data;
},
// Activate a task from BACKLOG status (PM only)
activate: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.PENDING, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/activate");
return data;
},
// Mark documentation as complete (Documenter only)
docsComplete: async (taskId: string, docNotes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = { ...mockTasks[idx], status: TaskStatus.AWAITING_PM_REVIEW, updated_at: now };
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/docs-complete", docNotes ?? null);
return data;
},
// =========================================================================
// CONVENIENCE METHODS
// =========================================================================
getMyTasks: async (): Promise<Task[]> => {
if (isMockMode()) {
// Return tasks assigned to "me" (mock: any assigned task)
return mockTasks.filter((t) => t.assigned_to !== null);
}
const { data } = await api.get<Task[]>("/tasks/my");
return data;
},
getPending: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.PENDING);
}
const { data } = await api.get<Task[]>("/tasks/pending");
return data;
},
getBlocked: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.BLOCKED);
}
const { data } = await api.get<Task[]>("/tasks/blocked");
return data;
},
getAwaitingQa: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_QA);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-qa");
return data;
},
getTeamTasks: async (team: Team): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.team === team);
}
const { data } = await api.get<Task[]>("/tasks/team/" + team);
return data;
},
getAwaitingDocs: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_DOCUMENTATION);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-docs");
return data;
},
getSubtasks: async (taskId: string): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.parent_task_id === taskId);
}
const { data } = await api.get<Task[]>("/tasks/" + taskId + "/subtasks");
return data;
},
// =========================================================================
// STATS
// =========================================================================
getStats: async (): Promise<TaskCountResponse> => {
if (isMockMode()) {
const counts: Record<string, number> = {};
mockTasks.forEach((t) => {
counts[t.status] = (counts[t.status] || 0) + 1;
});
return { counts };
}
const { data } = await api.get<TaskCountResponse>("/tasks/stats");
return data;
},
getStatsByTeam: async (): Promise<TaskCountResponse> => {
if (isMockMode()) {
const counts: Record<string, number> = {};
mockTasks.forEach((t) => {
counts[t.team] = (counts[t.team] || 0) + 1;
});
return { counts };
}
const { data } = await api.get<TaskCountResponse>("/tasks/stats/by-team");
return data;
},
// =========================================================================
// PROGRESS TRACKING
// =========================================================================
addProgress: async (taskId: string, request: ProgressRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const update = {
timestamp: now,
agent_id: "00000000-0000-0000-0000-000000000001",
message: request.message,
percentage: request.percentage ?? null,
};
mockTasks[idx] = {
...mockTasks[idx],
progress_updates: [...mockTasks[idx].progress_updates, update],
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/progress", request);
return data;
},
addCheckpoint: async (taskId: string, request: CheckpointRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const checkpoint = {
id: `checkpoint-${Date.now()}`,
timestamp: now,
agent_id: "00000000-0000-0000-0000-000000000001",
state_summary: request.state_summary,
remaining_work: request.remaining_work,
notes: request.notes ?? null,
};
mockTasks[idx] = {
...mockTasks[idx],
checkpoints: [...mockTasks[idx].checkpoints, checkpoint],
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/checkpoint", request);
return data;
},
addCommit: async (taskId: string, request: CommitRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
const commit = {
hash: request.hash,
message: request.message,
timestamp: now,
author_agent_id: "00000000-0000-0000-0000-000000000001",
};
mockTasks[idx] = {
...mockTasks[idx],
commits: [...mockTasks[idx].commits, commit],
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/commit", request);
return data;
},
// =========================================================================
// SOFT BLOCK & ESCALATION
// =========================================================================
softBlock: async (taskId: string, request: SoftBlockRequest): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.BLOCKED,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/soft-block", request);
return data;
},
escalate: async (taskId: string, request: EscalateRequest): Promise<EscalateResponse> => {
if (isMockMode()) {
return {
status: "escalated",
task_id: taskId,
escalated_to: request.escalate_to || "cell-pm",
reason: request.reason,
message: "Task escalated successfully (mock)",
};
}
const { data } = await api.post<EscalateResponse>("/tasks/" + taskId + "/escalate", request);
return data;
},
submitPmReview: async (taskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.AWAITING_PM_REVIEW,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/submit-pm-review");
return data;
},
// =========================================================================
// CEO APPROVAL (CEO only - Human-in-the-Loop)
// =========================================================================
// Get tasks awaiting CEO approval
getAwaitingCeoApproval: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_CEO_APPROVAL);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-ceo-approval");
return data;
},
// CEO approves a task (completes it)
ceoApprove: async (taskId: string, notes?: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.COMPLETED,
completed_at: now,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/ceo-approve", { notes });
return data;
},
// CEO rejects a task (sends back for revision)
ceoReject: async (taskId: string, notes: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.NEEDS_REVISION,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/ceo-reject", { notes });
return data;
},
// =========================================================================
// ADDITIONAL CONVENIENCE METHODS
// =========================================================================
// Get tasks awaiting PM review
getAwaitingPmReview: async (): Promise<Task[]> => {
if (isMockMode()) {
return mockTasks.filter((t) => t.status === TaskStatus.AWAITING_PM_REVIEW);
}
const { data } = await api.get<Task[]>("/tasks/awaiting-pm-review");
return data;
},
// Get all descendants of a task (subtasks, sub-subtasks, etc.)
getDescendants: async (taskId: string): Promise<Task[]> => {
if (isMockMode()) {
// Simple mock: just return direct subtasks
return mockTasks.filter((t) => t.parent_task_id === taskId);
}
const { data } = await api.get<Task[]>("/tasks/" + taskId + "/descendants");
return data;
},
// Get sessions linked to a task
getSessions: async (taskId: string): Promise<{ session_id: string; is_primary: boolean; relationship_type: string }[]> => {
if (isMockMode()) {
return [];
}
const { data } = await api.get<{ session_id: string; is_primary: boolean; relationship_type: string }[]>(
"/tasks/" + taskId + "/sessions"
);
return data;
},
// Escalate task directly to CEO
escalateToCeo: async (taskId: string, reason: string): Promise<EscalateResponse> => {
if (isMockMode()) {
return {
status: "escalated",
task_id: taskId,
escalated_to: "ceo",
reason,
message: "Task escalated to CEO (mock)",
};
}
const { data } = await api.post<EscalateResponse>("/tasks/" + taskId + "/escalate-to-ceo", { reason });
return data;
},
// Substitute a task with another (create replacement)
substitute: async (taskId: string, replacementTaskId: string): Promise<Task> => {
if (isMockMode()) {
const idx = mockTasks.findIndex((t) => t.id === taskId);
if (idx === -1) throw new Error("Task not found");
const now = new Date().toISOString();
mockTasks[idx] = {
...mockTasks[idx],
status: TaskStatus.CANCELLED,
updated_at: now,
};
return mockTasks[idx];
}
const { data } = await api.post<Task>("/tasks/" + taskId + "/substitute", {
replacement_task_id: replacementTaskId,
});
return data;
},
};
+261
View File
@@ -0,0 +1,261 @@
import api from "./client";
import type {
WorkSession,
WorkSessionSummary,
WorkSessionCreate,
WorkSessionStatus,
} from "@/types";
import { isMockMode } from "@/lib/mock-data";
// Mock data for offline mode
const mockWorkSessions: WorkSession[] = [];
export interface WorkSessionFilters {
project_id?: string;
agent_id?: string;
status?: WorkSessionStatus;
active_only?: boolean;
}
export const workSessionsApi = {
// List work sessions with optional filters
list: async (filters?: WorkSessionFilters): Promise<WorkSessionSummary[]> => {
if (isMockMode()) {
let sessions = [...mockWorkSessions];
if (filters?.project_id) {
sessions = sessions.filter((s) => s.project_id === filters.project_id);
}
if (filters?.agent_id) {
sessions = sessions.filter((s) => s.agent_id === filters.agent_id);
}
if (filters?.status) {
sessions = sessions.filter((s) => s.status === filters.status);
}
if (filters?.active_only) {
sessions = sessions.filter((s) => s.status === "active");
}
return sessions.map((s) => ({
id: s.id,
task_id: s.task_id,
branch_name: s.branch_name,
status: s.status,
started_at: s.started_at,
has_pr: s.pr_number !== null,
}));
}
const params = new URLSearchParams();
if (filters?.project_id) params.append("project_id", filters.project_id);
if (filters?.agent_id) params.append("agent_id", filters.agent_id);
if (filters?.status) params.append("status", filters.status);
if (filters?.active_only) params.append("active_only", "true");
const url = "/work-sessions?" + params.toString();
const { data } = await api.get<WorkSessionSummary[]>(url);
return data;
},
// Get single work session
get: async (sessionId: string): Promise<WorkSession> => {
if (isMockMode()) {
const session = mockWorkSessions.find((s) => s.id === sessionId);
if (!session) throw new Error("Work session not found");
return session;
}
const { data } = await api.get<WorkSession>("/work-sessions/" + sessionId);
return data;
},
// Get active work session for a task
getForTask: async (taskId: string): Promise<WorkSession | null> => {
if (isMockMode()) {
const session = mockWorkSessions.find(
(s) => s.task_id === taskId && s.status === "active"
);
return session ?? null;
}
try {
const { data } = await api.get<WorkSession | null>("/work-sessions/task/" + taskId);
return data;
} catch {
return null;
}
},
// Create work session (Developer or PM)
create: async (session: WorkSessionCreate): Promise<WorkSession> => {
if (isMockMode()) {
const now = new Date().toISOString();
const newSession: WorkSession = {
id: `session-${Date.now()}`,
project_id: session.project_id,
task_id: session.task_id,
agent_id: "00000000-0000-0000-0000-000000000001", // CEO in mock
branch_name: session.branch_name,
base_branch: session.base_branch,
target_branch: session.target_branch,
started_at: now,
ended_at: null,
status: "active" as WorkSessionStatus,
commits: [],
files_modified: [],
pr_number: null,
pr_url: null,
pr_status: null,
pr_created_at: null,
pr_merged_at: null,
merged_by: null,
created_at: now,
updated_at: now,
};
mockWorkSessions.push(newSession);
return newSession;
}
const { data } = await api.post<WorkSession>("/work-sessions", session);
return data;
},
// Add a commit to the work session
addCommit: async (sessionId: string, commitSha: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
commits: [...mockWorkSessions[idx].commits, commitSha],
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/commits", {
commit_sha: commitSha,
});
return data;
},
// Add modified files to the work session
addFiles: async (sessionId: string, filePaths: string[]): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
const existingFiles = new Set(mockWorkSessions[idx].files_modified);
filePaths.forEach((f) => existingFiles.add(f));
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
files_modified: Array.from(existingFiles),
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/files", {
file_paths: filePaths,
});
return data;
},
// Record PR creation
createPR: async (sessionId: string, prNumber: number, prUrl: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
pr_number: prNumber,
pr_url: prUrl,
pr_status: "open",
pr_created_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/pr", {
pr_number: prNumber,
pr_url: prUrl,
});
return data;
},
// Update PR status
updatePRStatus: async (sessionId: string, prStatus: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
pr_status: prStatus,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.patch<WorkSession>("/work-sessions/" + sessionId + "/pr", {
pr_status: prStatus,
});
return data;
},
// Record PR merge (PM only)
mergePR: async (sessionId: string, mergedBy: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
pr_status: "merged",
pr_merged_at: now,
merged_by: mergedBy,
status: "completed" as WorkSessionStatus,
ended_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/pr/merge", {
merged_by: mergedBy,
});
return data;
},
// Complete the session
complete: async (sessionId: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
status: "completed" as WorkSessionStatus,
ended_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/complete");
return data;
},
// Abandon the session
abandon: async (sessionId: string, reason?: string): Promise<WorkSession> => {
if (isMockMode()) {
const idx = mockWorkSessions.findIndex((s) => s.id === sessionId);
if (idx === -1) throw new Error("Work session not found");
const now = new Date().toISOString();
mockWorkSessions[idx] = {
...mockWorkSessions[idx],
status: "abandoned" as WorkSessionStatus,
ended_at: now,
updated_at: now,
};
return mockWorkSessions[idx];
}
const params = reason ? `?reason=${encodeURIComponent(reason)}` : "";
const { data } = await api.post<WorkSession>("/work-sessions/" + sessionId + "/abandon" + params);
return data;
},
};