/** * Agent Store - Manages clones/agents, usage statistics, and plugin status * * Extracted from gatewayStore.ts for Phase 11 Store Refactoring. * This store focuses on agent/clone CRUD operations and related metadata. */ import { create } from 'zustand'; import type { GatewayClient } from '../lib/gateway-client'; // === Types === export interface Clone { id: string; name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string; workspaceDir?: string; workspaceResolvedPath?: string; restrictFiles?: boolean; privacyOptIn?: boolean; userName?: string; userRole?: string; createdAt: string; bootstrapReady?: boolean; bootstrapFiles?: Array<{ name: string; path: string; exists: boolean }>; updatedAt?: string; } export interface UsageStats { totalSessions: number; totalMessages: number; totalTokens: number; byModel: Record; } export interface PluginStatus { id: string; name?: string; status: 'active' | 'inactive' | 'error' | 'loading'; version?: string; description?: string; } export interface CloneCreateOptions { name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string; workspaceDir?: string; restrictFiles?: boolean; privacyOptIn?: boolean; userName?: string; userRole?: string; } // === Store State === interface AgentStateSlice { clones: Clone[]; usageStats: UsageStats | null; pluginStatus: PluginStatus[]; isLoading: boolean; error: string | null; } // === Store Actions === interface AgentActionsSlice { loadClones: () => Promise; createClone: (opts: CloneCreateOptions) => Promise; updateClone: (id: string, updates: Partial) => Promise; deleteClone: (id: string) => Promise; loadUsageStats: () => Promise; loadPluginStatus: () => Promise; setError: (error: string | null) => void; clearError: () => void; } // === Store Interface === export type AgentStore = AgentStateSlice & AgentActionsSlice; // === Client Injection === // For coordinator to inject client - avoids direct import coupling let _client: GatewayClient | null = null; /** * Sets the gateway client for the agent store. * Called by the coordinator during initialization. */ export const setAgentStoreClient = (client: unknown): void => { _client = client as GatewayClient; }; /** * Gets the gateway client. * Returns null if not set (coordinator must initialize first). */ const getClient = (): GatewayClient | null => _client; // === Store Implementation === export const useAgentStore = create((set, get) => ({ // Initial state clones: [], usageStats: null, pluginStatus: [], isLoading: false, error: null, // Actions loadClones: async () => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized, skipping loadClones'); return; } try { set({ isLoading: true, error: null }); const result = await client.listClones(); const clones = result?.clones || result?.agents || []; set({ clones, isLoading: false }); // Set default agent ID if we have agents and none is set if (clones.length > 0 && clones[0].id) { const currentDefault = client.getDefaultAgentId(); // Only set if the default doesn't exist in the list const defaultExists = clones.some((c: Clone) => c.id === currentDefault); if (!defaultExists) { client.setDefaultAgentId(clones[0].id); } } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); // Don't throw - clone loading is non-critical } }, createClone: async (opts: CloneCreateOptions) => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized'); return undefined; } try { set({ isLoading: true, error: null }); const result = await client.createClone(opts); await get().loadClones(); // Refresh the list return result?.clone; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); return undefined; } }, updateClone: async (id: string, updates: Partial) => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized'); return undefined; } try { set({ isLoading: true, error: null }); const result = await client.updateClone(id, updates); await get().loadClones(); // Refresh the list return result?.clone; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); return undefined; } }, deleteClone: async (id: string) => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized'); return; } try { set({ isLoading: true, error: null }); await client.deleteClone(id); await get().loadClones(); // Refresh the list } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : String(err); set({ error: errorMessage, isLoading: false }); } }, loadUsageStats: async () => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized, skipping loadUsageStats'); return; } try { const stats = await client.getUsageStats(); set({ usageStats: stats }); } catch { // Usage stats are non-critical, ignore errors silently } }, loadPluginStatus: async () => { const client = getClient(); if (!client) { console.warn('[AgentStore] Client not initialized, skipping loadPluginStatus'); return; } try { const result = await client.getPluginStatus(); set({ pluginStatus: result?.plugins || [] }); } catch { // Plugin status is non-critical, ignore errors silently } }, setError: (error: string | null) => { set({ error }); }, clearError: () => { set({ error: null }); }, })); // === Selectors === /** * Get a clone by ID */ export const selectCloneById = (id: string) => (state: AgentStore): Clone | undefined => state.clones.find((clone) => clone.id === id); /** * Get all active plugins */ export const selectActivePlugins = (state: AgentStore): PluginStatus[] => state.pluginStatus.filter((plugin) => plugin.status === 'active'); /** * Check if any operation is in progress */ export const selectIsLoading = (state: AgentStore): boolean => state.isLoading;