diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts index c6ab231a..233e0fdd 100644 --- a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts @@ -11,17 +11,20 @@ import { toast } from 'sonner'; import { getElectronAPI } from '@/lib/electron'; export function useSkillsSettings() { - const { settings } = useAppStore(); + const enabled = useAppStore((state) => state.enableSkills); + const sources = useAppStore((state) => state.skillsSources); const [isLoading, setIsLoading] = useState(false); - const enabled = settings?.enableSkills ?? true; - const sources = settings?.skillsSources ?? ['user', 'project']; - const updateEnabled = async (newEnabled: boolean) => { setIsLoading(true); try { const api = getElectronAPI(); + if (!api.settings) { + throw new Error('Settings API not available'); + } await api.settings.updateGlobal({ enableSkills: newEnabled }); + // Update local store after successful server update + useAppStore.setState({ enableSkills: newEnabled }); toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled'); } catch (error) { toast.error('Failed to update skills settings'); @@ -35,7 +38,12 @@ export function useSkillsSettings() { setIsLoading(true); try { const api = getElectronAPI(); + if (!api.settings) { + throw new Error('Settings API not available'); + } await api.settings.updateGlobal({ skillsSources: newSources }); + // Update local store after successful server update + useAppStore.setState({ skillsSources: newSources }); toast.success('Skills sources updated'); } catch (error) { toast.error('Failed to update skills sources'); diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts index d3b1ba04..50f82393 100644 --- a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts @@ -3,27 +3,28 @@ * * Provides read-only view of custom subagent configurations * used for specialized task delegation. Supports: - * - Programmatic agents (stored in settings JSON) - global and project-level * - Filesystem agents (AGENT.md files in .claude/agents/) - user and project-level (read-only) + * + * Filesystem agents are discovered via the server API and displayed in the UI. + * Agent definitions in settings JSON are used server-side only. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useAppStore } from '@/store/app-store'; import type { AgentDefinition } from '@automaker/types'; import { getElectronAPI } from '@/lib/electron'; export type SubagentScope = 'global' | 'project'; -export type SubagentType = 'programmatic' | 'filesystem'; +export type SubagentType = 'filesystem'; export type FilesystemSource = 'user' | 'project'; export interface SubagentWithScope { name: string; definition: AgentDefinition; - scope: SubagentScope; // For programmatic agents + scope: SubagentScope; type: SubagentType; - // For filesystem agents: - source?: FilesystemSource; - filePath?: string; + source: FilesystemSource; + filePath: string; } interface FilesystemAgent { @@ -34,71 +35,46 @@ interface FilesystemAgent { } export function useSubagents() { - const { settings, currentProject, projectSettings } = useAppStore(); + const currentProject = useAppStore((state) => state.currentProject); const [isLoading, setIsLoading] = useState(false); const [subagentsWithScope, setSubagentsWithScope] = useState([]); - const [filesystemAgents, setFilesystemAgents] = useState([]); // Fetch filesystem agents - const fetchFilesystemAgents = async () => { + const fetchFilesystemAgents = useCallback(async () => { + setIsLoading(true); try { const api = getElectronAPI(); + if (!api.settings) { + console.warn('Settings API not available'); + return; + } const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']); - if (data.success) { - setFilesystemAgents(data.agents || []); + if (data.success && data.agents) { + // Transform filesystem agents to SubagentWithScope format + const agents: SubagentWithScope[] = data.agents.map( + ({ name, definition, source, filePath }: FilesystemAgent) => ({ + name, + definition, + scope: source === 'user' ? 'global' : 'project', + type: 'filesystem' as const, + source, + filePath, + }) + ); + setSubagentsWithScope(agents); } } catch (error) { console.error('Failed to fetch filesystem agents:', error); + } finally { + setIsLoading(false); } - }; + }, [currentProject?.path]); // Fetch filesystem agents on mount and when project changes useEffect(() => { fetchFilesystemAgents(); - }, [currentProject?.path]); - - // Merge programmatic and filesystem agents - useEffect(() => { - const globalSubagents = settings?.customSubagents || {}; - const projectSubagents = projectSettings?.customSubagents || {}; - - const merged: SubagentWithScope[] = []; - - // Add programmatic global agents - Object.entries(globalSubagents).forEach(([name, definition]) => { - merged.push({ name, definition, scope: 'global', type: 'programmatic' }); - }); - - // Add programmatic project agents (override globals with same name) - Object.entries(projectSubagents).forEach(([name, definition]) => { - const globalIndex = merged.findIndex((s) => s.name === name && s.scope === 'global'); - if (globalIndex !== -1) { - merged.splice(globalIndex, 1); - } - merged.push({ name, definition, scope: 'project', type: 'programmatic' }); - }); - - // Add filesystem agents - filesystemAgents.forEach(({ name, definition, source, filePath }) => { - // Remove any programmatic agents with the same name (filesystem takes precedence) - const programmaticIndex = merged.findIndex((s) => s.name === name); - if (programmaticIndex !== -1) { - merged.splice(programmaticIndex, 1); - } - - merged.push({ - name, - definition, - scope: source === 'user' ? 'global' : 'project', - type: 'filesystem', - source, - filePath, - }); - }); - - setSubagentsWithScope(merged); - }, [settings?.customSubagents, projectSettings?.customSubagents, filesystemAgents]); + }, [fetchFilesystemAgents]); return { subagentsWithScope, diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 3674036b..91374e6e 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -234,6 +234,8 @@ export async function syncSettingsToServer(): Promise { keyboardShortcuts: state.keyboardShortcuts, aiProfiles: state.aiProfiles, mcpServers: state.mcpServers, + enableSkills: state.enableSkills, + skillsSources: state.skillsSources, promptCustomization: state.promptCustomization, projects: state.projects, trashedProjects: state.trashedProjects, diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index d81b46b6..6c2bea8b 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -758,6 +758,83 @@ export interface ElectronAPI { }>; }; ideation?: IdeationAPI; + settings?: { + getStatus: () => Promise<{ + success: boolean; + hasGlobalSettings: boolean; + hasCredentials: boolean; + dataDir: string; + needsMigration: boolean; + }>; + getGlobal: () => Promise<{ + success: boolean; + settings?: Record; + error?: string; + }>; + updateGlobal: (updates: Record) => Promise<{ + success: boolean; + settings?: Record; + error?: string; + }>; + getCredentials: () => Promise<{ + success: boolean; + credentials?: { + anthropic: { configured: boolean; masked: string }; + google: { configured: boolean; masked: string }; + openai: { configured: boolean; masked: string }; + }; + error?: string; + }>; + updateCredentials: (updates: { + apiKeys?: { anthropic?: string; google?: string; openai?: string }; + }) => Promise<{ + success: boolean; + credentials?: { + anthropic: { configured: boolean; masked: string }; + google: { configured: boolean; masked: string }; + openai: { configured: boolean; masked: string }; + }; + error?: string; + }>; + getProject: (projectPath: string) => Promise<{ + success: boolean; + settings?: Record; + error?: string; + }>; + updateProject: ( + projectPath: string, + updates: Record + ) => Promise<{ + success: boolean; + settings?: Record; + error?: string; + }>; + migrate: (data: Record) => Promise<{ + success: boolean; + migratedGlobalSettings: boolean; + migratedCredentials: boolean; + migratedProjectCount: number; + errors: string[]; + }>; + discoverAgents: ( + projectPath?: string, + sources?: Array<'user' | 'project'> + ) => Promise<{ + success: boolean; + agents?: Array<{ + name: string; + definition: { + description: string; + prompt: string; + tools?: string[]; + model?: 'sonnet' | 'opus' | 'haiku' | 'inherit'; + }; + source: 'user' | 'project'; + filePath: string; + }>; + error?: string; + }>; + }; } // Note: Window interface is declared in @/types/electron.d.ts diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index d799b1a7..6429fadc 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -512,6 +512,10 @@ export interface AppState { // MCP Servers mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use + // Skills Configuration + enableSkills: boolean; // Enable Skills functionality (loads from .claude/skills/ directories) + skillsSources: Array<'user' | 'project'>; // Which directories to load Skills from + // Prompt Customization promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement @@ -1022,6 +1026,8 @@ const initialState: AppState = { enableSandboxMode: false, // Default to disabled (can be enabled for additional security) skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog) mcpServers: [], // No MCP servers configured by default + enableSkills: true, // Skills enabled by default + skillsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default promptCustomization: {}, // Empty by default - all prompts use built-in defaults aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null,