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:
537
desktop/src/store/configStore.ts
Normal file
537
desktop/src/store/configStore.ts
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// === 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;
|
||||
label: string;
|
||||
status: 'active' | 'inactive' | 'error';
|
||||
accounts?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
triggers?: Array<{ type: string; pattern?: string }>;
|
||||
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
// === Client Interface ===
|
||||
|
||||
export interface ConfigStoreClient {
|
||||
getWorkspaceInfo(): Promise<WorkspaceInfo | null>;
|
||||
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<string, unknown> }>;
|
||||
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<string, unknown> }>;
|
||||
enabled?: boolean;
|
||||
}): Promise<{ skill?: SkillInfo } | null>;
|
||||
deleteSkill(id: string): Promise<void>;
|
||||
listChannels(): Promise<{ channels?: ChannelInfo[] } | null>;
|
||||
getChannel(id: string): Promise<{ channel?: ChannelInfo } | null>;
|
||||
createChannel(channel: {
|
||||
type: string;
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}): Promise<{ channel?: ChannelInfo } | null>;
|
||||
updateChannel(id: string, updates: {
|
||||
name?: string;
|
||||
config?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}): Promise<{ channel?: ChannelInfo } | null>;
|
||||
deleteChannel(id: string): Promise<void>;
|
||||
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<ScheduledTask>;
|
||||
listModels(): Promise<{ models: GatewayModelChoice[] }>;
|
||||
getFeishuStatus(): Promise<{ configured?: boolean; accounts?: number } | null>;
|
||||
}
|
||||
|
||||
// === Store State & Actions ===
|
||||
|
||||
interface ConfigStore {
|
||||
// State
|
||||
quickConfig: QuickConfig;
|
||||
workspaceInfo: WorkspaceInfo | null;
|
||||
channels: ChannelInfo[];
|
||||
scheduledTasks: ScheduledTask[];
|
||||
skillsCatalog: SkillInfo[];
|
||||
models: GatewayModelChoice[];
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
error: string | null;
|
||||
|
||||
// Client reference (injected)
|
||||
client: ConfigStoreClient | null;
|
||||
|
||||
// Client injection
|
||||
setConfigStoreClient: (client: ConfigStoreClient) => void;
|
||||
|
||||
// Quick Config Actions
|
||||
loadQuickConfig: () => Promise<void>;
|
||||
saveQuickConfig: (updates: Partial<QuickConfig>) => Promise<void>;
|
||||
|
||||
// Workspace Actions
|
||||
loadWorkspaceInfo: () => Promise<void>;
|
||||
|
||||
// Channel Actions
|
||||
loadChannels: () => Promise<void>;
|
||||
getChannel: (id: string) => Promise<ChannelInfo | undefined>;
|
||||
createChannel: (channel: {
|
||||
type: string;
|
||||
name: string;
|
||||
config: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ChannelInfo | undefined>;
|
||||
updateChannel: (id: string, updates: {
|
||||
name?: string;
|
||||
config?: Record<string, unknown>;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ChannelInfo | undefined>;
|
||||
deleteChannel: (id: string) => Promise<void>;
|
||||
|
||||
// Scheduled Task Actions
|
||||
loadScheduledTasks: () => Promise<void>;
|
||||
createScheduledTask: (task: {
|
||||
name: string;
|
||||
schedule: string;
|
||||
scheduleType: 'cron' | 'interval' | 'once';
|
||||
target?: {
|
||||
type: 'agent' | 'hand' | 'workflow';
|
||||
id: string;
|
||||
};
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
}) => Promise<ScheduledTask | undefined>;
|
||||
|
||||
// Skill Actions
|
||||
loadSkillsCatalog: () => Promise<void>;
|
||||
getSkill: (id: string) => Promise<SkillInfo | undefined>;
|
||||
createSkill: (skill: {
|
||||
name: string;
|
||||
description?: string;
|
||||
triggers: Array<{ type: string; pattern?: string }>;
|
||||
actions: Array<{ type: string; params?: Record<string, unknown> }>;
|
||||
enabled?: boolean;
|
||||
}) => Promise<SkillInfo | undefined>;
|
||||
updateSkill: (id: string, updates: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
triggers?: Array<{ type: string; pattern?: string }>;
|
||||
actions?: Array<{ type: string; params?: Record<string, unknown> }>;
|
||||
enabled?: boolean;
|
||||
}) => Promise<SkillInfo | undefined>;
|
||||
deleteSkill: (id: string) => Promise<void>;
|
||||
|
||||
// Model Actions
|
||||
loadModels: () => Promise<void>;
|
||||
|
||||
// Utility
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigStore>((set, get) => ({
|
||||
// Initial State
|
||||
quickConfig: {},
|
||||
workspaceInfo: null,
|
||||
channels: [],
|
||||
scheduledTasks: [],
|
||||
skillsCatalog: [],
|
||||
models: [],
|
||||
modelsLoading: false,
|
||||
modelsError: null,
|
||||
error: null,
|
||||
client: null,
|
||||
|
||||
// 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<QuickConfig>) => {
|
||||
const client = get().client;
|
||||
if (!client) return;
|
||||
|
||||
try {
|
||||
const nextConfig = { ...get().quickConfig, ...updates };
|
||||
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',
|
||||
label: 'Feishu',
|
||||
status: feishu?.configured ? 'active' : 'inactive',
|
||||
accounts: feishu?.accounts || 0,
|
||||
});
|
||||
} catch {
|
||||
channels.push({ id: 'feishu', type: '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();
|
||||
set({ scheduledTasks: result?.tasks || [] });
|
||||
} catch {
|
||||
// Ignore if heartbeat.tasks not available
|
||||
}
|
||||
},
|
||||
|
||||
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) => ({
|
||||
scheduledTasks: [...state.scheduledTasks, newTask],
|
||||
}));
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user