refactor: enhance skills and subagents settings management

- Updated useSkillsSettings and useSubagents hooks to improve state management and error handling.
- Added new settings API methods for skills configuration and agent discovery.
- Refactored app-store to include enableSkills and skillsSources state management.
- Enhanced settings migration to sync skills configuration with the server.

These changes streamline the management of skills and subagents, ensuring better integration and user experience.
This commit is contained in:
Shirone
2026-01-06 23:43:31 +01:00
parent 236989bf6e
commit 33acf502ed
5 changed files with 129 additions and 60 deletions

View File

@@ -11,17 +11,20 @@ import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
export function useSkillsSettings() { export function useSkillsSettings() {
const { settings } = useAppStore(); const enabled = useAppStore((state) => state.enableSkills);
const sources = useAppStore((state) => state.skillsSources);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const enabled = settings?.enableSkills ?? true;
const sources = settings?.skillsSources ?? ['user', 'project'];
const updateEnabled = async (newEnabled: boolean) => { const updateEnabled = async (newEnabled: boolean) => {
setIsLoading(true); setIsLoading(true);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
await api.settings.updateGlobal({ enableSkills: newEnabled }); await api.settings.updateGlobal({ enableSkills: newEnabled });
// Update local store after successful server update
useAppStore.setState({ enableSkills: newEnabled });
toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled'); toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled');
} catch (error) { } catch (error) {
toast.error('Failed to update skills settings'); toast.error('Failed to update skills settings');
@@ -35,7 +38,12 @@ export function useSkillsSettings() {
setIsLoading(true); setIsLoading(true);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
await api.settings.updateGlobal({ skillsSources: newSources }); await api.settings.updateGlobal({ skillsSources: newSources });
// Update local store after successful server update
useAppStore.setState({ skillsSources: newSources });
toast.success('Skills sources updated'); toast.success('Skills sources updated');
} catch (error) { } catch (error) {
toast.error('Failed to update skills sources'); toast.error('Failed to update skills sources');

View File

@@ -3,27 +3,28 @@
* *
* Provides read-only view of custom subagent configurations * Provides read-only view of custom subagent configurations
* used for specialized task delegation. Supports: * 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 (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 { useAppStore } from '@/store/app-store';
import type { AgentDefinition } from '@automaker/types'; import type { AgentDefinition } from '@automaker/types';
import { getElectronAPI } from '@/lib/electron'; import { getElectronAPI } from '@/lib/electron';
export type SubagentScope = 'global' | 'project'; export type SubagentScope = 'global' | 'project';
export type SubagentType = 'programmatic' | 'filesystem'; export type SubagentType = 'filesystem';
export type FilesystemSource = 'user' | 'project'; export type FilesystemSource = 'user' | 'project';
export interface SubagentWithScope { export interface SubagentWithScope {
name: string; name: string;
definition: AgentDefinition; definition: AgentDefinition;
scope: SubagentScope; // For programmatic agents scope: SubagentScope;
type: SubagentType; type: SubagentType;
// For filesystem agents: source: FilesystemSource;
source?: FilesystemSource; filePath: string;
filePath?: string;
} }
interface FilesystemAgent { interface FilesystemAgent {
@@ -34,71 +35,46 @@ interface FilesystemAgent {
} }
export function useSubagents() { export function useSubagents() {
const { settings, currentProject, projectSettings } = useAppStore(); const currentProject = useAppStore((state) => state.currentProject);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]); const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]);
const [filesystemAgents, setFilesystemAgents] = useState<FilesystemAgent[]>([]);
// Fetch filesystem agents // Fetch filesystem agents
const fetchFilesystemAgents = async () => { const fetchFilesystemAgents = useCallback(async () => {
setIsLoading(true);
try { try {
const api = getElectronAPI(); const api = getElectronAPI();
if (!api.settings) {
console.warn('Settings API not available');
return;
}
const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']); const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']);
if (data.success) { if (data.success && data.agents) {
setFilesystemAgents(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) { } catch (error) {
console.error('Failed to fetch filesystem agents:', error); console.error('Failed to fetch filesystem agents:', error);
} finally {
setIsLoading(false);
} }
}; }, [currentProject?.path]);
// Fetch filesystem agents on mount and when project changes // Fetch filesystem agents on mount and when project changes
useEffect(() => { useEffect(() => {
fetchFilesystemAgents(); fetchFilesystemAgents();
}, [currentProject?.path]); }, [fetchFilesystemAgents]);
// 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]);
return { return {
subagentsWithScope, subagentsWithScope,

View File

@@ -234,6 +234,8 @@ export async function syncSettingsToServer(): Promise<boolean> {
keyboardShortcuts: state.keyboardShortcuts, keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles, aiProfiles: state.aiProfiles,
mcpServers: state.mcpServers, mcpServers: state.mcpServers,
enableSkills: state.enableSkills,
skillsSources: state.skillsSources,
promptCustomization: state.promptCustomization, promptCustomization: state.promptCustomization,
projects: state.projects, projects: state.projects,
trashedProjects: state.trashedProjects, trashedProjects: state.trashedProjects,

View File

@@ -758,6 +758,83 @@ export interface ElectronAPI {
}>; }>;
}; };
ideation?: IdeationAPI; ideation?: IdeationAPI;
settings?: {
getStatus: () => Promise<{
success: boolean;
hasGlobalSettings: boolean;
hasCredentials: boolean;
dataDir: string;
needsMigration: boolean;
}>;
getGlobal: () => Promise<{
success: boolean;
settings?: Record<string, unknown>;
error?: string;
}>;
updateGlobal: (updates: Record<string, unknown>) => Promise<{
success: boolean;
settings?: Record<string, unknown>;
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<string, unknown>;
error?: string;
}>;
updateProject: (
projectPath: string,
updates: Record<string, unknown>
) => Promise<{
success: boolean;
settings?: Record<string, unknown>;
error?: string;
}>;
migrate: (data: Record<string, string>) => 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 // Note: Window interface is declared in @/types/electron.d.ts

View File

@@ -512,6 +512,10 @@ export interface AppState {
// MCP Servers // MCP Servers
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use 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 // Prompt Customization
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement 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) enableSandboxMode: false, // Default to disabled (can be enabled for additional security)
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog) skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
mcpServers: [], // No MCP servers configured by default 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 promptCustomization: {}, // Empty by default - all prompts use built-in defaults
aiProfiles: DEFAULT_AI_PROFILES, aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null, projectAnalysis: null,