mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
merge: sync with upstream v0.9.0rc branch
This commit is contained in:
@@ -5,6 +5,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() {
|
||||
@@ -43,6 +45,13 @@ export function ClaudeSettingsTab() {
|
||||
autoLoadClaudeMd={autoLoadClaudeMd}
|
||||
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
|
||||
/>
|
||||
|
||||
{/* Skills Configuration */}
|
||||
<SkillsSection />
|
||||
|
||||
{/* Custom Subagents */}
|
||||
<SubagentsSection />
|
||||
|
||||
{showUsageTracking && <ClaudeUsageSection />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Hooks barrel export for Claude Settings Tab
|
||||
*/
|
||||
|
||||
export { useSkillsSettings } from './use-skills-settings';
|
||||
export { useSubagents } from './use-subagents';
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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 enabled = useAppStore((state) => state.enableSkills);
|
||||
const sources = useAppStore((state) => state.skillsSources);
|
||||
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({ 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');
|
||||
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({ 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');
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
enabled,
|
||||
sources,
|
||||
updateEnabled,
|
||||
updateSources,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Subagents Hook - Manages custom subagent definitions
|
||||
*
|
||||
* Provides read-only view of custom subagent configurations
|
||||
* used for specialized task delegation. Supports:
|
||||
* - 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, 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 = 'filesystem';
|
||||
export type FilesystemSource = 'user' | 'project';
|
||||
|
||||
export interface SubagentWithScope {
|
||||
name: string;
|
||||
definition: AgentDefinition;
|
||||
scope: SubagentScope;
|
||||
type: SubagentType;
|
||||
source: FilesystemSource;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface FilesystemAgent {
|
||||
name: string;
|
||||
definition: AgentDefinition;
|
||||
source: FilesystemSource;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export function useSubagents() {
|
||||
const currentProject = useAppStore((state) => state.currentProject);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]);
|
||||
|
||||
// Fetch filesystem agents
|
||||
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 && 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();
|
||||
}, [fetchFilesystemAgents]);
|
||||
|
||||
return {
|
||||
subagentsWithScope,
|
||||
isLoading,
|
||||
hasProject: !!currentProject,
|
||||
refreshFilesystemAgents: fetchFilesystemAgents,
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
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,
|
||||
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: 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(
|
||||
'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
|
||||
{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>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Specialized agents Claude delegates to automatically
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable-subagents"
|
||||
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 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="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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -728,6 +728,83 @@ export interface ElectronAPI {
|
||||
codex?: {
|
||||
getUsage: () => Promise<CodexUsageResponse>;
|
||||
};
|
||||
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
|
||||
|
||||
@@ -23,8 +23,6 @@ import type {
|
||||
SpecRegenerationEvent,
|
||||
SuggestionType,
|
||||
GitHubAPI,
|
||||
GitHubIssue,
|
||||
GitHubPR,
|
||||
IssueValidationInput,
|
||||
IssueValidationEvent,
|
||||
IdeationAPI,
|
||||
@@ -1891,6 +1889,26 @@ export class HttpApiClient implements ElectronAPI {
|
||||
migratedProjectCount: number;
|
||||
errors: string[];
|
||||
}> => this.post('/api/settings/migrate', { data }),
|
||||
|
||||
// Filesystem agents discovery (read-only)
|
||||
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;
|
||||
}> => this.post('/api/settings/agents/discover', { projectPath, sources }),
|
||||
};
|
||||
|
||||
// Sessions API
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { getHttpApiClient } from './http-api-client';
|
||||
import { getElectronAPI } from './electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
|
||||
const logger = createLogger('WorkspaceConfig');
|
||||
@@ -33,9 +32,17 @@ function joinPath(...parts: string[]): string {
|
||||
*/
|
||||
async function getDefaultDocumentsPath(): Promise<string | null> {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const documentsPath = await api.getPath('documents');
|
||||
return joinPath(documentsPath, 'Automaker');
|
||||
// In Electron mode, use the native getPath API directly from the preload script
|
||||
// This returns the actual system Documents folder (e.g., C:\Users\<user>\Documents on Windows)
|
||||
// Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents'
|
||||
if (typeof window !== 'undefined' && (window as any).electronAPI?.getPath) {
|
||||
const documentsPath = await (window as any).electronAPI.getPath('documents');
|
||||
return joinPath(documentsPath, 'Automaker');
|
||||
}
|
||||
|
||||
// In web mode (no Electron), we can't access the user's Documents folder
|
||||
// Return null to let the caller use other fallback mechanisms (like server's DATA_DIR)
|
||||
return null;
|
||||
} catch (error) {
|
||||
logger.error('Failed to get documents path:', error);
|
||||
return null;
|
||||
@@ -76,6 +83,7 @@ export async function getDefaultWorkspaceDirectory(): Promise<string | null> {
|
||||
|
||||
// Try to get Documents/Automaker
|
||||
const documentsPath = await getDefaultDocumentsPath();
|
||||
logger.info('Default documentsPath resolved to:', documentsPath);
|
||||
if (documentsPath) {
|
||||
return documentsPath;
|
||||
}
|
||||
|
||||
@@ -574,6 +574,14 @@ 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
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1170,6 +1178,10 @@ const initialState: AppState = {
|
||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||
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
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user