feat: implement subagents configuration management

- Added a new function to retrieve subagents configuration from settings, allowing users to enable/disable subagents and select sources for loading them.
- Updated the AgentService to incorporate subagents configuration, dynamically adding tools based on the settings.
- Enhanced the UI components to manage subagents, including a settings section for enabling/disabling and selecting sources.
- Introduced a new hook for managing subagents settings state and interactions.

These changes improve the flexibility and usability of subagents within the application, enhancing user experience and configuration options.
This commit is contained in:
Shirone
2026-01-07 10:32:42 +01:00
parent fe13d47b24
commit 5c601ff200
9 changed files with 345 additions and 77 deletions

View File

@@ -0,0 +1,63 @@
/**
* Subagents Settings Hook - Manages Subagents configuration state
*
* Provides state management for enabling/disabling Subagents and
* configuring which sources to load Subagents from (user/project).
*/
import { useState } from 'react';
import { useAppStore } from '@/store/app-store';
import { toast } from 'sonner';
import { getElectronAPI } from '@/lib/electron';
export function useSubagentsSettings() {
const enabled = useAppStore((state) => state.enableSubagents);
const sources = useAppStore((state) => state.subagentsSources);
const [isLoading, setIsLoading] = useState(false);
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({ enableSubagents: newEnabled });
// Update local store after successful server update
useAppStore.setState({ enableSubagents: newEnabled });
toast.success(newEnabled ? 'Subagents enabled' : 'Subagents disabled');
} catch (error) {
toast.error('Failed to update subagents settings');
console.error(error);
} finally {
setIsLoading(false);
}
};
const updateSources = async (newSources: Array<'user' | 'project'>) => {
setIsLoading(true);
try {
const api = getElectronAPI();
if (!api.settings) {
throw new Error('Settings API not available');
}
await api.settings.updateGlobal({ subagentsSources: newSources });
// Update local store after successful server update
useAppStore.setState({ subagentsSources: newSources });
toast.success('Subagents sources updated');
} catch (error) {
toast.error('Failed to update subagents sources');
console.error(error);
} finally {
setIsLoading(false);
}
};
return {
enabled,
sources,
updateEnabled,
updateSources,
isLoading,
};
}

View File

@@ -1,26 +1,62 @@
/**
* Subagents Section - UI for viewing filesystem-based agents
* Subagents Section - UI for managing Subagents configuration
*
* Allows users to enable/disable Subagents and select which directories
* to load Subagents from (user ~/.claude/agents/ or project .claude/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 { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
import { Bot, RefreshCw, Loader2, Users, ExternalLink } from 'lucide-react';
import {
Bot,
RefreshCw,
Loader2,
Users,
ExternalLink,
Globe,
FolderOpen,
Sparkles,
} from 'lucide-react';
import { useSubagents } from './hooks/use-subagents';
import { useSubagentsSettings } from './hooks/use-subagents-settings';
import { SubagentCard } from './subagent-card';
export function SubagentsSection() {
const { subagentsWithScope, isLoading, hasProject, refreshFilesystemAgents } = useSubagents();
const {
subagentsWithScope,
isLoading: isLoadingAgents,
hasProject,
refreshFilesystemAgents,
} = useSubagents();
const {
enabled,
sources,
updateEnabled,
updateSources,
isLoading: isLoadingSettings,
} = useSubagentsSettings();
const isLoading = isLoadingAgents || isLoadingSettings;
const handleRefresh = async () => {
await refreshFilesystemAgents();
};
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(
@@ -39,7 +75,7 @@ export function SubagentsSection() {
<div>
<h3 className="font-semibold text-base flex items-center gap-2">
Custom Subagents
{subagentsWithScope.length > 0 && (
{enabled && 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>
@@ -50,57 +86,169 @@ export function SubagentsSection() {
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
<Switch
id="enable-subagents"
checked={enabled}
onCheckedChange={updateEnabled}
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>
<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 Subagents from
</Label>
<div className="grid gap-2">
{/* User Subagents Option */}
<label
htmlFor="subagent-source-user"
className={cn(
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
sources.includes('user')
? 'border-violet-500/50 bg-violet-500/10'
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
)}
>
<Checkbox
id="subagent-source-user"
checked={sources.includes('user')}
onCheckedChange={() => toggleSource('user')}
disabled={isLoading}
className="data-[state=checked]:bg-violet-500 data-[state=checked]:border-violet-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 Subagents</span>
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
~/.claude/agents/ Available across all projects
</span>
</div>
</label>
{/* Project Subagents Option */}
<label
htmlFor="subagent-source-project"
className={cn(
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
sources.includes('project')
? 'border-violet-500/50 bg-violet-500/10'
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
)}
>
<Checkbox
id="subagent-source-project"
checked={sources.includes('project')}
onCheckedChange={() => toggleSource('project')}
disabled={isLoading}
className="data-[state=checked]:bg-violet-500 data-[state=checked]:border-violet-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 Subagents</span>
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
.claude/agents/ Version-controlled with project
</span>
</div>
</label>
</div>
</div>
)}
{/* Agents List */}
{enabled && (
<>
{/* Refresh Button */}
<div className="flex items-center justify-between">
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
Discovered Agents
</Label>
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
disabled={isLoading}
title="Refresh agents from disk"
className="gap-1.5 h-7 px-2 text-xs"
>
{isLoadingAgents ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
Refresh
</Button>
</div>
{subagentsWithScope.length === 0 ? (
<div className="text-center py-6 text-muted-foreground border border-dashed border-border/50 rounded-xl">
<Users className="w-10 h-10 mx-auto mb-2 opacity-30" />
<p className="text-sm font-medium">No agents found</p>
<p className="text-xs mt-1 max-w-sm mx-auto">
Create <code className="text-xs bg-muted px-1 rounded">.md</code> files in{' '}
{sources.includes('user') && (
<code className="text-xs bg-muted px-1 rounded">~/.claude/agents/</code>
)}
{sources.includes('user') && sources.includes('project') && ' or '}
{sources.includes('project') && (
<code className="text-xs bg-muted px-1 rounded">.claude/agents/</code>
)}
</p>
</div>
) : (
<div className="space-y-2">
{subagentsWithScope.map((agent) => (
<SubagentCard
key={`${agent.type}-${agent.source || agent.scope}-${agent.name}`}
agent={agent}
/>
))}
</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>
Subagents are automatically discovered when agents start. Define agents as{' '}
<code className="text-xs bg-muted px-1 rounded">AGENT.md</code> files or{' '}
<code className="text-xs bg-muted px-1 rounded">agent-name.md</code> files.
</p>
</div>
</div>
<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"
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 Agents documentation
</a>
</div>
) : (
<div className="space-y-3">
{subagentsWithScope.map((agent) => (
<SubagentCard
key={`${agent.type}-${agent.source || agent.scope}-${agent.name}`}
agent={agent}
/>
))}
)}
{/* Disabled State Empty Message */}
{!enabled && (
<div className="text-center py-6 text-muted-foreground">
<Bot className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">Subagents are disabled</p>
<p className="text-xs mt-1">Enable to load custom agent definitions</p>
</div>
)}
</div>