feat: add skills and subagents configuration support

- Updated .gitignore to include skills directory.
- Introduced agent discovery functionality to scan for AGENT.md files in user and project directories.
- Added new API endpoint for discovering filesystem agents.
- Implemented UI components for managing skills and viewing custom subagents.
- Enhanced settings helpers to retrieve skills configuration and custom subagents.
- Updated agent service to incorporate skills and subagents in task delegation.

These changes enhance the capabilities of the system by allowing users to define and manage skills and custom subagents effectively.
This commit is contained in:
Shirone
2026-01-06 04:31:57 +01:00
parent 5d675561ba
commit 236989bf6e
19 changed files with 1098 additions and 6 deletions

View File

@@ -4,6 +4,8 @@ import { useCliStatus } from '../hooks/use-cli-status';
import { ClaudeCliStatus } from '../cli-status/claude-cli-status';
import { ClaudeMdSettings } from '../claude/claude-md-settings';
import { ClaudeUsageSection } from '../api-keys/claude-usage-section';
import { SkillsSection } from './claude-settings-tab/skills-section';
import { SubagentsSection } from './claude-settings-tab/subagents-section';
import { Info } from 'lucide-react';
export function ClaudeSettingsTab() {
@@ -42,6 +44,13 @@ export function ClaudeSettingsTab() {
autoLoadClaudeMd={autoLoadClaudeMd}
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
/>
{/* Skills Configuration */}
<SkillsSection />
{/* Custom Subagents */}
<SubagentsSection />
{showUsageTracking && <ClaudeUsageSection />}
</div>
);

View File

@@ -0,0 +1,6 @@
/**
* Hooks barrel export for Claude Settings Tab
*/
export { useSkillsSettings } from './use-skills-settings';
export { useSubagents } from './use-subagents';

View File

@@ -0,0 +1,55 @@
/**
* Skills Settings Hook - Manages Skills configuration state
*
* Provides state management for enabling/disabling Skills and
* configuring which sources to load Skills from (user/project).
*/
import { useState } from 'react';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
export function useSkillsSettings() {
const { settings } = useAppStore();
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();
await api.settings.updateGlobal({ enableSkills: newEnabled });
toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled');
} catch (error) {
toast.error('Failed to update skills settings');
console.error(error);
} finally {
setIsLoading(false);
}
};
const updateSources = async (newSources: Array<'user' | 'project'>) => {
setIsLoading(true);
try {
const api = getElectronAPI();
await api.settings.updateGlobal({ skillsSources: newSources });
toast.success('Skills sources updated');
} catch (error) {
toast.error('Failed to update skills sources');
console.error(error);
} finally {
setIsLoading(false);
}
};
return {
enabled,
sources,
updateEnabled,
updateSources,
isLoading,
};
}

View File

@@ -0,0 +1,109 @@
/**
* Subagents Hook - Manages custom subagent definitions
*
* 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)
*/
import { useState, useEffect } 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 FilesystemSource = 'user' | 'project';
export interface SubagentWithScope {
name: string;
definition: AgentDefinition;
scope: SubagentScope; // For programmatic agents
type: SubagentType;
// For filesystem agents:
source?: FilesystemSource;
filePath?: string;
}
interface FilesystemAgent {
name: string;
definition: AgentDefinition;
source: FilesystemSource;
filePath: string;
}
export function useSubagents() {
const { settings, currentProject, projectSettings } = useAppStore();
const [isLoading, setIsLoading] = useState(false);
const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]);
const [filesystemAgents, setFilesystemAgents] = useState<FilesystemAgent[]>([]);
// Fetch filesystem agents
const fetchFilesystemAgents = async () => {
try {
const api = getElectronAPI();
const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']);
if (data.success) {
setFilesystemAgents(data.agents || []);
}
} catch (error) {
console.error('Failed to fetch filesystem agents:', error);
}
};
// 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]);
return {
subagentsWithScope,
isLoading,
hasProject: !!currentProject,
refreshFilesystemAgents: fetchFilesystemAgents,
};
}

View File

@@ -0,0 +1,7 @@
/**
* Claude Settings Tab components barrel export
*/
export { SkillsSection } from './skills-section';
export { SubagentsSection } from './subagents-section';
export { SubagentCard } from './subagent-card';

View File

@@ -0,0 +1,169 @@
/**
* Skills Section - UI for managing Skills configuration
*
* Allows users to enable/disable Skills and select which directories
* to load Skills from (user ~/.claude/skills/ or project .claude/skills/).
*/
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { Zap, Globe, FolderOpen, ExternalLink, Sparkles } from 'lucide-react';
import { useSkillsSettings } from './hooks/use-skills-settings';
export function SkillsSection() {
const { enabled, sources, updateEnabled, updateSources, isLoading } = useSkillsSettings();
const toggleSource = (source: 'user' | 'project') => {
if (sources.includes(source)) {
updateSources(sources.filter((s: 'user' | 'project') => s !== source));
} else {
updateSources([...sources, source]);
}
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-linear-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border/30">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center">
<Zap className="w-5 h-5 text-amber-500" />
</div>
<div>
<h3 className="font-semibold text-base flex items-center gap-2">
Skills
{enabled && (
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-500">
{sources.length} source{sources.length !== 1 ? 's' : ''} active
</span>
)}
</h3>
<p className="text-sm text-muted-foreground mt-0.5">
Filesystem-based capabilities Claude invokes autonomously
</p>
</div>
</div>
<Switch
id="enable-skills"
checked={enabled}
onCheckedChange={updateEnabled}
disabled={isLoading}
/>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* Sources Selection */}
{enabled && (
<div className="space-y-3">
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
Load Skills from
</Label>
<div className="grid gap-2">
{/* User Skills Option */}
<label
htmlFor="source-user"
className={cn(
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
sources.includes('user')
? 'border-amber-500/50 bg-amber-500/10'
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
)}
>
<Checkbox
id="source-user"
checked={sources.includes('user')}
onCheckedChange={() => toggleSource('user')}
disabled={isLoading}
className="data-[state=checked]:bg-amber-500 data-[state=checked]:border-amber-500"
/>
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
<Globe className="w-4 h-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium">User Skills</span>
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
~/.claude/skills/ Available across all projects
</span>
</div>
</label>
{/* Project Skills Option */}
<label
htmlFor="source-project"
className={cn(
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
sources.includes('project')
? 'border-amber-500/50 bg-amber-500/10'
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
)}
>
<Checkbox
id="source-project"
checked={sources.includes('project')}
onCheckedChange={() => toggleSource('project')}
disabled={isLoading}
className="data-[state=checked]:bg-amber-500 data-[state=checked]:border-amber-500"
/>
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
<FolderOpen className="w-4 h-4 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<span className="text-sm font-medium">Project Skills</span>
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
.claude/skills/ Version-controlled with project
</span>
</div>
</label>
</div>
</div>
)}
{/* Help Text */}
{enabled && (
<div className="rounded-xl border border-border/30 bg-muted/30 p-4 space-y-3">
<div className="flex items-start gap-3">
<div className="w-6 h-6 rounded-md bg-brand-500/20 flex items-center justify-center shrink-0 mt-0.5">
<Sparkles className="w-3.5 h-3.5 text-brand-500" />
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p className="font-medium text-foreground/80">Auto-Discovery</p>
<p>
Skills are automatically discovered when agents start. Define skills as{' '}
<code className="text-xs bg-muted px-1 rounded">SKILL.md</code> files.
</p>
</div>
</div>
<a
href="https://code.claude.com/docs/en/skills"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-xs text-brand-500 hover:text-brand-400 transition-colors"
>
<ExternalLink className="w-3.5 h-3.5" />
View Skills documentation
</a>
</div>
)}
{/* Disabled State Empty Message */}
{!enabled && (
<div className="text-center py-6 text-muted-foreground">
<Zap className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">Skills are disabled</p>
<p className="text-xs mt-1">Enable to load filesystem-based capabilities</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,138 @@
/**
* Subagent Card - Display card for a single subagent definition
*
* Shows the subagent's name, description, model, tool count, scope, and type.
* Read-only view - agents are managed by editing .md files directly.
*/
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Markdown } from '@/components/ui/markdown';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
import {
Globe,
FolderOpen,
ChevronDown,
ChevronRight,
Bot,
Cpu,
Wrench,
FileCode,
} from 'lucide-react';
import type { SubagentWithScope } from './hooks/use-subagents';
interface SubagentCardProps {
agent: SubagentWithScope;
}
export function SubagentCard({ agent }: SubagentCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
const { name, definition, scope, filePath } = agent;
const toolCount = definition.tools?.length ?? 'all';
const modelDisplay =
definition.model === 'inherit' || !definition.model
? 'Inherit'
: definition.model.charAt(0).toUpperCase() + definition.model.slice(1);
// Scope icon and label
const ScopeIcon = scope === 'global' ? Globe : FolderOpen;
const scopeLabel = scope === 'global' ? 'User' : 'Project';
// Model color based on type
const getModelColor = () => {
const model = definition.model?.toLowerCase();
if (model === 'opus') return 'text-violet-500 bg-violet-500/10 border-violet-500/30';
if (model === 'sonnet') return 'text-blue-500 bg-blue-500/10 border-blue-500/30';
if (model === 'haiku') return 'text-emerald-500 bg-emerald-500/10 border-emerald-500/30';
return 'text-muted-foreground bg-muted/50 border-border/50';
};
return (
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
<div
className={cn(
'rounded-xl border transition-all duration-200',
'border-border/50 bg-accent/20',
'hover:bg-accent/30 hover:border-border/70'
)}
>
{/* Main Card Content */}
<div className="flex items-start gap-3 p-4">
{/* Agent Icon */}
<div className="w-9 h-9 rounded-lg bg-violet-500/15 flex items-center justify-center shrink-0 mt-0.5">
<Bot className="w-4 h-4 text-violet-500" />
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{/* Header Row */}
<div className="flex items-center gap-2 flex-wrap">
<h4 className="font-medium text-sm">{name}</h4>
<Badge
variant="outline"
size="sm"
className={cn('flex items-center gap-1', getModelColor())}
>
<Cpu className="h-3 w-3" />
{modelDisplay}
</Badge>
<Badge variant="muted" size="sm" className="flex items-center gap-1">
<Wrench className="h-3 w-3" />
{toolCount === 'all' ? 'All' : toolCount} tools
</Badge>
<Badge variant="muted" size="sm" className="flex items-center gap-1">
<ScopeIcon className="h-3 w-3" />
{scopeLabel}
</Badge>
</div>
{/* Description */}
<p className="text-sm text-muted-foreground mt-1.5 line-clamp-2">
{definition.description}
</p>
{/* File Path */}
{filePath && (
<div className="flex items-center gap-1.5 mt-2 text-xs text-muted-foreground/60">
<FileCode className="h-3 w-3" />
<span className="font-mono truncate">{filePath}</span>
</div>
)}
</div>
{/* Expand Button */}
<CollapsibleTrigger asChild>
<button
className={cn(
'p-1.5 rounded-md transition-colors shrink-0',
'hover:bg-muted/50 text-muted-foreground hover:text-foreground',
'cursor-pointer'
)}
title={isExpanded ? 'Hide prompt' : 'View prompt'}
>
{isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</button>
</CollapsibleTrigger>
</div>
{/* Expandable Prompt Section */}
<CollapsibleContent>
<div className="px-4 pb-4 pt-0">
<div className="ml-12 rounded-lg border border-border/30 bg-muted/30 p-4 overflow-auto max-h-64">
<div className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
System Prompt
</div>
<Markdown className="text-xs prose-sm">{definition.prompt}</Markdown>
</div>
</div>
</CollapsibleContent>
</div>
</Collapsible>
);
}

View File

@@ -0,0 +1,109 @@
/**
* Subagents Section - UI for viewing filesystem-based agents
*
* Displays agents discovered from:
* - User-level: ~/.claude/agents/
* - Project-level: .claude/agents/
*
* Read-only view - agents are managed by editing .md files directly.
*/
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Bot, RefreshCw, Loader2, Users, ExternalLink } from 'lucide-react';
import { useSubagents } from './hooks/use-subagents';
import { SubagentCard } from './subagent-card';
export function SubagentsSection() {
const { subagentsWithScope, isLoading, hasProject, refreshFilesystemAgents } = useSubagents();
const handleRefresh = async () => {
await refreshFilesystemAgents();
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-linear-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border/30">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-violet-500/20 flex items-center justify-center">
<Bot className="w-5 h-5 text-violet-500" />
</div>
<div>
<h3 className="font-semibold text-base flex items-center gap-2">
Custom Subagents
{subagentsWithScope.length > 0 && (
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-violet-500/20 text-violet-500">
{subagentsWithScope.length} agent{subagentsWithScope.length !== 1 ? 's' : ''}
</span>
)}
</h3>
<p className="text-sm text-muted-foreground mt-0.5">
Specialized agents Claude delegates to automatically
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
disabled={isLoading}
title="Refresh agents from disk"
className="gap-2"
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="text-xs">Refresh</span>
</Button>
</div>
{/* Content */}
<div className="p-6">
{subagentsWithScope.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<Users className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p className="text-sm font-medium">No agents found</p>
<p className="text-xs mt-2 max-w-sm mx-auto">
Create <code className="text-xs bg-muted px-1 rounded">.md</code> files in{' '}
<code className="text-xs bg-muted px-1 rounded">~/.claude/agents/</code>
{hasProject && (
<>
{' or '}
<code className="text-xs bg-muted px-1 rounded">.claude/agents/</code>
</>
)}
</p>
<a
href="https://code.claude.com/docs/en/agents"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 mt-4 text-xs text-brand-500 hover:text-brand-400 transition-colors"
>
<ExternalLink className="w-3.5 h-3.5" />
View Agents documentation
</a>
</div>
) : (
<div className="space-y-3">
{subagentsWithScope.map((agent) => (
<SubagentCard
key={`${agent.type}-${agent.source || agent.scope}-${agent.name}`}
agent={agent}
/>
))}
</div>
)}
</div>
</div>
);
}