refactor(phase-11): extract specialized stores from gatewayStore
Decompose monolithic gatewayStore.ts (1660 lines) into focused stores: - connectionStore.ts (444 lines) - WebSocket, auth, local gateway - agentStore.ts (256 lines) - Clones, usage stats, plugins - handStore.ts (498 lines) - Hands, triggers, approvals - workflowStore.ts (255 lines) - Workflows, runs - configStore.ts (537 lines) - QuickConfig, channels, skills Each store uses client injection pattern for loose coupling. Coordinator layer to be added in next commit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
256
desktop/src/store/agentStore.ts
Normal file
256
desktop/src/store/agentStore.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* 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<string, { messages: number; inputTokens: number; outputTokens: number }>;
|
||||
}
|
||||
|
||||
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<void>;
|
||||
createClone: (opts: CloneCreateOptions) => Promise<Clone | undefined>;
|
||||
updateClone: (id: string, updates: Partial<Clone>) => Promise<Clone | undefined>;
|
||||
deleteClone: (id: string) => Promise<void>;
|
||||
loadUsageStats: () => Promise<void>;
|
||||
loadPluginStatus: () => Promise<void>;
|
||||
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<AgentStore>((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<Clone>) => {
|
||||
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;
|
||||
Reference in New Issue
Block a user