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

@@ -293,6 +293,29 @@ export async function getSkillsConfiguration(settingsService: SettingsService):
};
}
/**
* Get Subagents configuration from settings.
* Returns configuration for enabling subagents and which sources to load from.
*
* @param settingsService - Settings service instance
* @returns Subagents configuration with enabled state, sources, and tool inclusion flag
*/
export async function getSubagentsConfiguration(settingsService: SettingsService): Promise<{
enabled: boolean;
sources: Array<'user' | 'project'>;
shouldIncludeInTools: boolean;
}> {
const settings = await settingsService.getGlobalSettings();
const enabled = settings.enableSubagents ?? true; // Default enabled
const sources = settings.subagentsSources ?? ['user', 'project']; // Default both sources
return {
enabled,
sources,
shouldIncludeInTools: enabled && sources.length > 0,
};
}
/**
* Get custom subagents from settings, merging global and project-level definitions.
* Project-level subagents take precedence over global ones with the same name.

View File

@@ -72,17 +72,10 @@ export class ClaudeProvider extends BaseProvider {
// Build Claude SDK options
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
const hasMcpServers = options.mcpServers && Object.keys(options.mcpServers).length > 0;
const defaultTools = [
'Read',
'Write',
'Edit',
'Glob',
'Grep',
'Bash',
'WebSearch',
'WebFetch',
'Skill',
];
// Base tools available to all agents
// Note: 'Skill' and 'Task' tools are added dynamically by agent-service.ts
// based on whether skills/subagents are enabled in settings
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
// AUTONOMOUS MODE: Always bypass permissions and allow unrestricted tools
// Only restrict tools when no MCP servers are configured

View File

@@ -25,6 +25,7 @@ import {
getMCPServersFromSettings,
getPromptCustomization,
getSkillsConfiguration,
getSubagentsConfiguration,
getCustomSubagents,
} from '../lib/settings-helpers.js';
@@ -248,10 +249,16 @@ export class AgentService {
? await getSkillsConfiguration(this.settingsService)
: { enabled: false, sources: [] as Array<'user' | 'project'>, shouldIncludeInTools: false };
// Get custom subagents from settings (merge global + project-level)
const customSubagents = this.settingsService
? await getCustomSubagents(this.settingsService, effectiveWorkDir)
: undefined;
// Get Subagents configuration from settings
const subagentsConfig = this.settingsService
? await getSubagentsConfiguration(this.settingsService)
: { enabled: false, sources: [] as Array<'user' | 'project'>, shouldIncludeInTools: false };
// Get custom subagents from settings (merge global + project-level) only if enabled
const customSubagents =
this.settingsService && subagentsConfig.enabled
? await getCustomSubagents(this.settingsService, effectiveWorkDir)
: undefined;
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
const contextResult = await loadContextFiles({
@@ -297,18 +304,34 @@ export class AgentService {
const settingSources = [...new Set([...sdkSettingSources, ...skillSettingSources])];
// Enhance allowedTools with Skills and Subagents tools
// These tools are not in the provider's default set - they're added dynamically based on settings
const needsSkillTool = skillsConfig.shouldIncludeInTools;
const needsTaskTool =
subagentsConfig.shouldIncludeInTools &&
customSubagents &&
Object.keys(customSubagents).length > 0;
// Base tools that match the provider's default set
const baseTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
if (allowedTools) {
allowedTools = [...allowedTools]; // Create a copy to avoid mutating SDK options
// Add Skill tool if skills are enabled
if (skillsConfig.shouldIncludeInTools && !allowedTools.includes('Skill')) {
if (needsSkillTool && !allowedTools.includes('Skill')) {
allowedTools.push('Skill');
}
// Add Task tool if custom subagents are configured
if (
customSubagents &&
Object.keys(customSubagents).length > 0 &&
!allowedTools.includes('Task')
) {
if (needsTaskTool && !allowedTools.includes('Task')) {
allowedTools.push('Task');
}
} else if (needsSkillTool || needsTaskTool) {
// If no allowedTools specified but we need to add Skill/Task tools,
// build the full list including base tools
allowedTools = [...baseTools];
if (needsSkillTool) {
allowedTools.push('Skill');
}
if (needsTaskTool) {
allowedTools.push('Task');
}
}

View File

@@ -96,17 +96,9 @@ describe('claude-provider.ts', () => {
expect(sdk.query).toHaveBeenCalledWith({
prompt: 'Test',
options: expect.objectContaining({
allowedTools: [
'Read',
'Write',
'Edit',
'Glob',
'Grep',
'Bash',
'WebSearch',
'WebFetch',
'Skill',
],
// Note: 'Skill' and 'Task' tools are added dynamically by agent-service.ts
// based on settings, not included in base default tools
allowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'],
}),
});
});

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>

View File

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

View File

@@ -516,6 +516,10 @@ export interface AppState {
enableSkills: boolean; // Enable Skills functionality (loads from .claude/skills/ directories)
skillsSources: Array<'user' | 'project'>; // Which directories to load Skills from
// Subagents Configuration
enableSubagents: boolean; // Enable Custom Subagents functionality (loads from .claude/agents/ directories)
subagentsSources: Array<'user' | 'project'>; // Which directories to load Subagents from
// Prompt Customization
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
@@ -1028,6 +1032,8 @@ const initialState: AppState = {
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
enableSubagents: true, // Subagents enabled by default
subagentsSources: ['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,

View File

@@ -504,7 +504,21 @@ export interface GlobalSettings {
// Subagents Configuration
/**
* Custom subagent definitions for specialized task delegation
* Enable Custom Subagents functionality (loads from .claude/agents/ directories)
* @default true
*/
enableSubagents?: boolean;
/**
* Which directories to load Subagents from
* - 'user': ~/.claude/agents/ (personal agents)
* - 'project': .claude/agents/ (project-specific agents)
* @default ['user', 'project']
*/
subagentsSources?: Array<'user' | 'project'>;
/**
* Custom subagent definitions for specialized task delegation (programmatic)
* Key: agent name (e.g., 'code-reviewer', 'test-runner')
* Value: agent configuration
*/
@@ -707,6 +721,10 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
enableSandboxMode: false,
skipSandboxWarning: false,
mcpServers: [],
enableSkills: true,
skillsSources: ['user', 'project'],
enableSubagents: true,
subagentsSources: ['user', 'project'],
};
/** Default credentials (empty strings - user must provide API keys) */