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';
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');

View File

@@ -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<SubagentWithScope[]>([]);
const [filesystemAgents, setFilesystemAgents] = useState<FilesystemAgent[]>([]);
// 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,

View File

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

View File

@@ -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<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

View File

@@ -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,