/** * configStore.ts - Configuration Management Store * * Extracted from gatewayStore.ts for Phase 11 Store Refactoring. * Manages settings, workspace, channels, skills, and models. */ import { create } from 'zustand'; import type { GatewayModelChoice } from '../lib/gateway-config'; import { setStoredGatewayUrl, setStoredGatewayToken } from '../lib/gateway-client'; import type { GatewayClient } from '../lib/gateway-client'; import { invoke } from '@tauri-apps/api/core'; // === Types === export interface QuickConfig { agentName?: string; agentRole?: string; userName?: string; userRole?: string; agentNickname?: string; scenarios?: string[]; workspaceDir?: string; gatewayUrl?: string; gatewayToken?: string; skillsExtraDirs?: string[]; mcpServices?: Array<{ id: string; name: string; enabled: boolean }>; theme?: 'light' | 'dark'; autoStart?: boolean; showToolCalls?: boolean; restrictFiles?: boolean; autoSaveContext?: boolean; fileWatching?: boolean; privacyOptIn?: boolean; } export interface WorkspaceInfo { path: string; resolvedPath: string; exists: boolean; fileCount: number; totalSize: number; } export interface ChannelInfo { id: string; type: string; name: string; label: string; status: 'active' | 'inactive' | 'error'; enabled?: boolean; accounts?: number; error?: string; config?: Record; } export interface ScheduledTask { id: string; name: string; schedule: string; status: 'active' | 'paused' | 'completed' | 'error'; lastRun?: string; nextRun?: string; description?: string; } export interface SkillInfo { id: string; name: string; path?: string; source?: 'builtin' | 'extra'; description?: string; version?: string; capabilities?: string[]; tags?: string[]; mode?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean; } // === Client Interface === export interface ConfigStoreClient { getWorkspaceInfo(): Promise; getQuickConfig(): Promise<{ quickConfig?: QuickConfig } | null>; saveQuickConfig(config: QuickConfig): Promise<{ quickConfig?: QuickConfig } | null>; listSkills(): Promise<{ skills?: SkillInfo[]; extraDirs?: string[] } | null>; getSkill(id: string): Promise<{ skill?: SkillInfo } | null>; createSkill(skill: { name: string; description?: string; triggers: Array<{ type: string; pattern?: string }>; actions: Array<{ type: string; params?: Record }>; enabled?: boolean; }): Promise<{ skill?: SkillInfo } | null>; updateSkill(id: string, updates: { name?: string; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean; }): Promise<{ skill?: SkillInfo } | null>; deleteSkill(id: string): Promise; listChannels(): Promise<{ channels?: ChannelInfo[] } | null>; getChannel(id: string): Promise<{ channel?: ChannelInfo } | null>; createChannel(channel: { type: string; name: string; config: Record; enabled?: boolean; }): Promise<{ channel?: ChannelInfo } | null>; updateChannel(id: string, updates: { name?: string; config?: Record; enabled?: boolean; }): Promise<{ channel?: ChannelInfo } | null>; deleteChannel(id: string): Promise; listScheduledTasks(): Promise<{ tasks?: ScheduledTask[] } | null>; createScheduledTask(task: { name: string; schedule: string; scheduleType: 'cron' | 'interval' | 'once'; target?: { type: 'agent' | 'hand' | 'workflow'; id: string; }; description?: string; enabled?: boolean; }): Promise; listModels(): Promise<{ models: GatewayModelChoice[] }>; getFeishuStatus(): Promise<{ configured?: boolean; accounts?: number } | null>; } // === Store State Slice === export interface ConfigStateSlice { quickConfig: QuickConfig; workspaceInfo: WorkspaceInfo | null; channels: ChannelInfo[]; scheduledTasks: ScheduledTask[]; skillsCatalog: SkillInfo[]; models: GatewayModelChoice[]; modelsLoading: boolean; modelsError: string | null; error: string | null; client: ConfigStoreClient | null; isLoading: boolean; } // === Store Actions Slice === export interface ConfigActionsSlice { setConfigStoreClient: (client: ConfigStoreClient) => void; loadQuickConfig: () => Promise; saveQuickConfig: (updates: Partial) => Promise; loadWorkspaceInfo: () => Promise; loadChannels: () => Promise; getChannel: (id: string) => Promise; createChannel: (channel: { type: string; name: string; config: Record; enabled?: boolean; }) => Promise; updateChannel: (id: string, updates: { name?: string; config?: Record; enabled?: boolean; }) => Promise; deleteChannel: (id: string) => Promise; loadScheduledTasks: () => Promise; createScheduledTask: (task: { name: string; schedule: string; scheduleType: 'cron' | 'interval' | 'once'; target?: { type: 'agent' | 'hand' | 'workflow'; id: string; }; description?: string; enabled?: boolean; }) => Promise; loadSkillsCatalog: () => Promise; getSkill: (id: string) => Promise; createSkill: (skill: { name: string; description?: string; triggers: Array<{ type: string; pattern?: string }>; actions: Array<{ type: string; params?: Record }>; enabled?: boolean; }) => Promise; updateSkill: (id: string, updates: { name?: string; description?: string; triggers?: Array<{ type: string; pattern?: string }>; actions?: Array<{ type: string; params?: Record }>; enabled?: boolean; }) => Promise; deleteSkill: (id: string) => Promise; loadModels: () => Promise; clearError: () => void; } // === Combined Store Type === export type ConfigStore = ConfigStateSlice & ConfigActionsSlice; export const useConfigStore = create((set, get) => ({ // Initial State quickConfig: {}, workspaceInfo: null, channels: [], scheduledTasks: [], skillsCatalog: [], models: [], modelsLoading: false, modelsError: null, error: null, client: null, isLoading: false, // Client Injection setConfigStoreClient: (client: ConfigStoreClient) => { set({ client }); }, // === Quick Config Actions === loadQuickConfig: async () => { const client = get().client; if (!client) return; try { const result = await client.getQuickConfig(); set({ quickConfig: result?.quickConfig || {} }); } catch { // Ignore if quick config not available } }, saveQuickConfig: async (updates: Partial) => { const client = get().client; if (!client) return; try { const nextConfig = { ...get().quickConfig, ...updates }; // Persist gateway URL/token to localStorage for reconnection if (nextConfig.gatewayUrl) { setStoredGatewayUrl(nextConfig.gatewayUrl); } if (Object.prototype.hasOwnProperty.call(updates, 'gatewayToken')) { setStoredGatewayToken(nextConfig.gatewayToken || ''); } const result = await client.saveQuickConfig(nextConfig); set({ quickConfig: result?.quickConfig || nextConfig }); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); } }, // === Workspace Actions === loadWorkspaceInfo: async () => { const client = get().client; if (!client) return; try { const info = await client.getWorkspaceInfo(); set({ workspaceInfo: info }); } catch { // Ignore if workspace info not available } }, // === Channel Actions === loadChannels: async () => { const client = get().client; if (!client) return; const channels: ChannelInfo[] = []; try { // Try listing channels from Gateway const result = await client.listChannels(); if (result?.channels) { set({ channels: result.channels }); return; } } catch { // channels.list may not be available, fallback to probing } // Fallback: probe known channels individually try { const feishu = await client.getFeishuStatus(); channels.push({ id: 'feishu', type: 'feishu', name: 'feishu', label: '飞书 (Feishu)', status: feishu?.configured ? 'active' : 'inactive', accounts: feishu?.accounts || 0, }); } catch { channels.push({ id: 'feishu', type: 'feishu', name: 'feishu', label: '飞书 (Feishu)', status: 'inactive' }); } set({ channels }); }, getChannel: async (id: string) => { const client = get().client; if (!client) return undefined; try { const result = await client.getChannel(id); if (result?.channel) { // Update the channel in the local state if it exists const currentChannels = get().channels; const existingIndex = currentChannels.findIndex(c => c.id === id); if (existingIndex >= 0) { const updatedChannels = [...currentChannels]; updatedChannels[existingIndex] = result.channel; set({ channels: updatedChannels }); } return result.channel as ChannelInfo; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, createChannel: async (channel) => { const client = get().client; if (!client) return undefined; try { const result = await client.createChannel(channel); if (result?.channel) { // Add the new channel to local state const currentChannels = get().channels; set({ channels: [...currentChannels, result.channel as ChannelInfo] }); return result.channel as ChannelInfo; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, updateChannel: async (id, updates) => { const client = get().client; if (!client) return undefined; try { const result = await client.updateChannel(id, updates); if (result?.channel) { // Update the channel in local state const currentChannels = get().channels; const updatedChannels = currentChannels.map(c => c.id === id ? (result.channel as ChannelInfo) : c ); set({ channels: updatedChannels }); return result.channel as ChannelInfo; } return undefined; } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); return undefined; } }, deleteChannel: async (id) => { const client = get().client; if (!client) return; try { await client.deleteChannel(id); // Remove the channel from local state const currentChannels = get().channels; set({ channels: currentChannels.filter(c => c.id !== id) }); } catch (err: unknown) { set({ error: err instanceof Error ? err.message : String(err) }); } }, // === Scheduled Task Actions === loadScheduledTasks: async () => { const client = get().client; if (!client) return; try { const result = await client.listScheduledTasks(); const tasks = result?.tasks || []; set({ scheduledTasks: tasks }); // Persist to localStorage as fallback try { localStorage.setItem('zclaw-scheduled-tasks', JSON.stringify(tasks)); } catch { /* ignore */ } } catch { // Fallback: load from localStorage try { const stored = localStorage.getItem('zclaw-scheduled-tasks'); if (stored) { set({ scheduledTasks: JSON.parse(stored) }); } } catch { /* ignore */ } } }, createScheduledTask: async (task) => { const client = get().client; if (!client) return undefined; try { const result = await client.createScheduledTask(task); const newTask: ScheduledTask = { id: result.id, name: result.name, schedule: result.schedule, status: result.status as 'active' | 'paused' | 'completed' | 'error', lastRun: result.lastRun, nextRun: result.nextRun, description: result.description, }; set((state) => { const tasks = [...state.scheduledTasks, newTask]; try { localStorage.setItem('zclaw-scheduled-tasks', JSON.stringify(tasks)); } catch { /* ignore */ } return { scheduledTasks: tasks }; }); return newTask; } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Failed to create scheduled task'; set({ error: errorMessage }); return undefined; } }, // === Skill Actions === loadSkillsCatalog: async () => { const client = get().client; if (!client) return; try { const result = await client.listSkills(); set({ skillsCatalog: result?.skills || [] }); if (result?.extraDirs) { set((state) => ({ quickConfig: { ...state.quickConfig, skillsExtraDirs: result.extraDirs, }, })); } } catch { // Ignore if skills list not available } }, getSkill: async (id: string) => { const client = get().client; if (!client) return undefined; try { const result = await client.getSkill(id); return result?.skill as SkillInfo | undefined; } catch { return undefined; } }, createSkill: async (skill) => { const client = get().client; if (!client) return undefined; try { const result = await client.createSkill(skill); const newSkill = result?.skill as SkillInfo | undefined; if (newSkill) { set((state) => ({ skillsCatalog: [...state.skillsCatalog, newSkill], })); } return newSkill; } catch { return undefined; } }, updateSkill: async (id, updates) => { const client = get().client; if (!client) return undefined; try { const result = await client.updateSkill(id, updates); const updatedSkill = result?.skill as SkillInfo | undefined; if (updatedSkill) { set((state) => ({ skillsCatalog: state.skillsCatalog.map((s) => s.id === id ? updatedSkill : s ), })); } return updatedSkill; } catch { return undefined; } }, deleteSkill: async (id) => { const client = get().client; if (!client) return; try { await client.deleteSkill(id); set((state) => ({ skillsCatalog: state.skillsCatalog.filter((s) => s.id !== id), })); } catch { // Ignore deletion errors } }, // === Model Actions === loadModels: async () => { const client = get().client; if (!client) return; try { set({ modelsLoading: true, modelsError: null }); const result = await client.listModels(); const models: GatewayModelChoice[] = result?.models || []; set({ models, modelsLoading: false }); } catch (err: unknown) { const message = err instanceof Error ? err.message : 'Failed to load models'; set({ modelsError: message, modelsLoading: false }); } }, // === Utility === clearError: () => { set({ error: null }); }, })); // Re-export types for convenience export type { QuickConfig as QuickConfigType, WorkspaceInfo as WorkspaceInfoType, ChannelInfo as ChannelInfoType, ScheduledTask as ScheduledTaskType, SkillInfo as SkillInfoType, }; // === Client Injection === import type { KernelClient } from '../lib/kernel-client'; /** * Helper to create a ConfigStoreClient adapter from a GatewayClient. */ function createConfigClientFromGateway(client: GatewayClient): ConfigStoreClient { return { getWorkspaceInfo: () => client.getWorkspaceInfo(), getQuickConfig: () => client.getQuickConfig(), saveQuickConfig: (config) => client.saveQuickConfig(config), listSkills: () => client.listSkills(), getSkill: (id) => client.getSkill(id), createSkill: (skill) => client.createSkill(skill), updateSkill: (id, updates) => client.updateSkill(id, updates), deleteSkill: (id) => client.deleteSkill(id), listChannels: () => client.listChannels(), getChannel: (id) => client.getChannel(id), createChannel: (channel) => client.createChannel(channel), updateChannel: (id, updates) => client.updateChannel(id, updates), deleteChannel: (id) => client.deleteChannel(id), listScheduledTasks: () => client.listScheduledTasks(), createScheduledTask: async (task) => { const result = await client.createScheduledTask(task); return { id: result.id, name: result.name, schedule: result.schedule, status: result.status as 'active' | 'paused' | 'completed' | 'error', }; }, listModels: () => client.listModels(), getFeishuStatus: () => client.getFeishuStatus(), }; } /** * Helper to create a ConfigStoreClient adapter from a KernelClient. */ function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient { return { getWorkspaceInfo: async () => { try { const status = await client.status(); return { path: '', resolvedPath: '', exists: status.initialized as boolean, fileCount: 0, totalSize: 0, }; } catch { return null; } }, getQuickConfig: async () => { // Read from localStorage in kernel mode try { const stored = localStorage.getItem('zclaw-quick-config'); if (stored) { return { quickConfig: JSON.parse(stored) }; } } catch { /* ignore */ } return { quickConfig: {} }; }, saveQuickConfig: async (config) => { // Persist to localStorage in kernel mode try { localStorage.setItem('zclaw-quick-config', JSON.stringify(config)); } catch { /* ignore */ } return { quickConfig: config }; }, listSkills: async () => { try { const result = await client.listSkills(); if (result?.skills) { return { skills: result.skills.map((s) => ({ id: s.id, name: s.name, description: s.description || '', version: s.version, // Use capabilities directly capabilities: s.capabilities || [], tags: s.tags || [], mode: s.mode, // Map triggers to the expected format triggers: (s.triggers || []).map((t: string) => ({ type: 'keyword', pattern: t, })), // Create actions from capabilities for UI display actions: (s.capabilities || []).map((cap: string) => ({ type: cap, params: undefined, })), enabled: s.enabled ?? true, category: s.category, })), }; } return { skills: [] }; } catch { return { skills: [] }; } }, getSkill: async (id: string) => { return { skill: { id, name: id, description: '' } }; }, createSkill: async (skill) => { try { const result = await client.createSkill(skill); if (result?.skill) { return { skill: { id: result.skill.id, name: result.skill.name, description: result.skill.description, version: result.skill.version, capabilities: result.skill.capabilities, tags: result.skill.tags, mode: result.skill.mode, enabled: result.skill.enabled, triggers: (result.skill.triggers || []).map((t: string) => ({ type: 'keyword', pattern: t })), category: result.skill.category, } as SkillInfo, }; } return null; } catch { return null; } }, updateSkill: async (id, updates) => { try { const result = await client.updateSkill(id, updates); if (result?.skill) { return { skill: { id: result.skill.id, name: result.skill.name, description: result.skill.description, version: result.skill.version, capabilities: result.skill.capabilities, tags: result.skill.tags, mode: result.skill.mode, enabled: result.skill.enabled, triggers: (result.skill.triggers || []).map((t: string) => ({ type: 'keyword', pattern: t })), category: result.skill.category, } as SkillInfo, }; } return null; } catch { return null; } }, deleteSkill: async (id) => { try { await client.deleteSkill(id); } catch { // Ignore deletion errors } }, listChannels: async () => ({ channels: [] }), getChannel: async () => null, createChannel: async () => null, updateChannel: async () => null, deleteChannel: async () => {}, listScheduledTasks: async () => { try { const tasks = await invoke('scheduled_task_list'); return { tasks }; } catch { return { tasks: [] }; } }, createScheduledTask: async (task) => { const result = await invoke<{ id: string; name: string; schedule: string; status: string }>( 'scheduled_task_create', { request: task }, ); return { ...result, status: result.status as 'active' | 'paused' | 'completed' | 'error' }; }, listModels: async () => { try { const status = await client.status(); return { models: status.defaultModel ? [{ id: status.defaultModel as string, name: status.defaultModel as string, provider: (status.defaultProvider as string) || 'default', }] : [], }; } catch { return { models: [] }; } }, getFeishuStatus: async () => null, }; } /** * Sets the client for the config store. * Called by the coordinator during initialization. */ export function setConfigStoreClient(client: unknown): void { let configClient: ConfigStoreClient; // Check if it's a KernelClient (has listHands method) if (client && typeof client === 'object' && 'listHands' in client) { configClient = createConfigClientFromKernel(client as KernelClient); } else if (client && typeof client === 'object') { // It's GatewayClient configClient = createConfigClientFromGateway(client as GatewayClient); } else { // Fallback stub client configClient = { getWorkspaceInfo: async () => null, getQuickConfig: async () => null, saveQuickConfig: async () => null, listSkills: async () => ({ skills: [] }), getSkill: async () => null, createSkill: async () => null, updateSkill: async () => null, deleteSkill: async () => {}, listChannels: async () => ({ channels: [] }), getChannel: async () => null, createChannel: async () => null, updateChannel: async () => null, deleteChannel: async () => {}, listScheduledTasks: async () => ({ tasks: [] }), createScheduledTask: async () => { throw new Error('Not implemented'); }, listModels: async () => ({ models: [] }), getFeishuStatus: async () => null, }; } useConfigStore.getState().setConfigStoreClient(configClient); }