diff --git a/.claude/.gitignore b/.claude/.gitignore index 735e81ff..2cad7c3c 100644 --- a/.claude/.gitignore +++ b/.claude/.gitignore @@ -1 +1,2 @@ -hans/ \ No newline at end of file +hans/ +skills/ \ No newline at end of file diff --git a/apps/server/src/lib/agent-discovery.ts b/apps/server/src/lib/agent-discovery.ts new file mode 100644 index 00000000..955cbb1d --- /dev/null +++ b/apps/server/src/lib/agent-discovery.ts @@ -0,0 +1,234 @@ +/** + * Agent Discovery - Scans filesystem for AGENT.md files + * + * Discovers agents from: + * - ~/.claude/agents/ (user-level, global) + * - .claude/agents/ (project-level) + * + * Similar to Skills, but for custom subagents defined in AGENT.md files. + */ + +import path from 'path'; +import os from 'os'; +import { createLogger } from '@automaker/utils'; +import { secureFs, systemPaths } from '@automaker/platform'; +import type { AgentDefinition } from '@automaker/types'; + +const logger = createLogger('AgentDiscovery'); + +export interface FilesystemAgent { + name: string; // Directory name (e.g., 'code-reviewer') + definition: AgentDefinition; + source: 'user' | 'project'; + filePath: string; // Full path to AGENT.md +} + +/** + * Parse agent .md file frontmatter and content + * Format: + * --- + * name: agent-name # Optional + * description: When to use this agent + * tools: tool1, tool2, tool3 # Optional (comma or space separated list) + * model: sonnet # Optional: sonnet, opus, haiku + * --- + * System prompt content here... + */ +async function parseAgentFile( + filePath: string, + isSystemPath: boolean +): Promise { + try { + const content = isSystemPath + ? ((await systemPaths.systemPathReadFile(filePath, 'utf-8')) as string) + : ((await secureFs.readFile(filePath, 'utf-8')) as string); + + // Extract frontmatter + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!frontmatterMatch) { + logger.warn(`Invalid agent file format (missing frontmatter): ${filePath}`); + return null; + } + + const [, frontmatter, prompt] = frontmatterMatch; + + // Parse description (required) + const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim(); + if (!description) { + logger.warn(`Missing description in agent file: ${filePath}`); + return null; + } + + // Parse tools (optional) - supports both comma-separated and space-separated + const toolsMatch = frontmatter.match(/tools:\s*(.+)/); + const tools = toolsMatch + ? toolsMatch[1] + .split(/[,\s]+/) // Split by comma or whitespace + .map((t) => t.trim()) + .filter((t) => t && t !== '') + : undefined; + + // Parse model (optional) + const modelMatch = frontmatter.match(/model:\s*(\w+)/); + const model = modelMatch + ? (modelMatch[1].trim() as 'sonnet' | 'opus' | 'haiku' | 'inherit') + : undefined; + + return { + description, + prompt: prompt.trim(), + tools, + model, + }; + } catch (error) { + logger.error(`Failed to parse agent file: ${filePath}`, error); + return null; + } +} + +/** + * Scan a directory for agent .md files + * Agents can be in two formats: + * 1. Flat: agent-name.md (file directly in agents/) + * 2. Subdirectory: agent-name/AGENT.md (folder + file, similar to Skills) + */ +async function scanAgentsDirectory( + baseDir: string, + source: 'user' | 'project' +): Promise { + const agents: FilesystemAgent[] = []; + const isSystemPath = source === 'user'; // User directories use systemPaths + + try { + // Check if directory exists + const exists = isSystemPath + ? await systemPaths.systemPathExists(baseDir) + : await secureFs + .access(baseDir) + .then(() => true) + .catch(() => false); + + if (!exists) { + logger.debug(`Directory does not exist: ${baseDir}`); + return agents; + } + + // Read all entries in the directory + if (isSystemPath) { + // For system paths (user directory) + const entryNames = await systemPaths.systemPathReaddir(baseDir); + for (const entryName of entryNames) { + const entryPath = path.join(baseDir, entryName); + const stat = await systemPaths.systemPathStat(entryPath); + + // Check for flat .md file format (agent-name.md) + if (stat.isFile() && entryName.endsWith('.md')) { + const agentName = entryName.slice(0, -3); // Remove .md extension + const definition = await parseAgentFile(entryPath, true); + if (definition) { + agents.push({ + name: agentName, + definition, + source, + filePath: entryPath, + }); + logger.debug(`Discovered ${source} agent (flat): ${agentName}`); + } + } + // Check for subdirectory format (agent-name/AGENT.md) + else if (stat.isDirectory()) { + const agentFilePath = path.join(entryPath, 'AGENT.md'); + const agentFileExists = await systemPaths.systemPathExists(agentFilePath); + + if (agentFileExists) { + const definition = await parseAgentFile(agentFilePath, true); + if (definition) { + agents.push({ + name: entryName, + definition, + source, + filePath: agentFilePath, + }); + logger.debug(`Discovered ${source} agent (subdirectory): ${entryName}`); + } + } + } + } + } else { + // For project paths (use secureFs) + const entries = await secureFs.readdir(baseDir, { withFileTypes: true }); + for (const entry of entries) { + // Check for flat .md file format (agent-name.md) + if (entry.isFile() && entry.name.endsWith('.md')) { + const agentName = entry.name.slice(0, -3); // Remove .md extension + const agentFilePath = path.join(baseDir, entry.name); + const definition = await parseAgentFile(agentFilePath, false); + if (definition) { + agents.push({ + name: agentName, + definition, + source, + filePath: agentFilePath, + }); + logger.debug(`Discovered ${source} agent (flat): ${agentName}`); + } + } + // Check for subdirectory format (agent-name/AGENT.md) + else if (entry.isDirectory()) { + const agentDir = path.join(baseDir, entry.name); + const agentFilePath = path.join(agentDir, 'AGENT.md'); + + const agentFileExists = await secureFs + .access(agentFilePath) + .then(() => true) + .catch(() => false); + + if (agentFileExists) { + const definition = await parseAgentFile(agentFilePath, false); + if (definition) { + agents.push({ + name: entry.name, + definition, + source, + filePath: agentFilePath, + }); + logger.debug(`Discovered ${source} agent (subdirectory): ${entry.name}`); + } + } + } + } + } + } catch (error) { + logger.error(`Failed to scan agents directory: ${baseDir}`, error); + } + + return agents; +} + +/** + * Discover all filesystem-based agents from user and project sources + */ +export async function discoverFilesystemAgents( + projectPath?: string, + sources: Array<'user' | 'project'> = ['user', 'project'] +): Promise { + const agents: FilesystemAgent[] = []; + + // Discover user-level agents from ~/.claude/agents/ + if (sources.includes('user')) { + const userAgentsDir = path.join(os.homedir(), '.claude', 'agents'); + const userAgents = await scanAgentsDirectory(userAgentsDir, 'user'); + agents.push(...userAgents); + logger.info(`Discovered ${userAgents.length} user-level agents from ${userAgentsDir}`); + } + + // Discover project-level agents from .claude/agents/ + if (sources.includes('project') && projectPath) { + const projectAgentsDir = path.join(projectPath, '.claude', 'agents'); + const projectAgents = await scanAgentsDirectory(projectAgentsDir, 'project'); + agents.push(...projectAgents); + logger.info(`Discovered ${projectAgents.length} project-level agents from ${projectAgentsDir}`); + } + + return agents; +} diff --git a/apps/server/src/lib/settings-helpers.ts b/apps/server/src/lib/settings-helpers.ts index 9a322994..07564085 100644 --- a/apps/server/src/lib/settings-helpers.ts +++ b/apps/server/src/lib/settings-helpers.ts @@ -269,3 +269,60 @@ export async function getPromptCustomization( enhancement: mergeEnhancementPrompts(customization.enhancement), }; } + +/** + * Get Skills configuration from settings. + * Returns configuration for enabling skills and which sources to load from. + * + * @param settingsService - Settings service instance + * @returns Skills configuration with enabled state, sources, and tool inclusion flag + */ +export async function getSkillsConfiguration(settingsService: SettingsService): Promise<{ + enabled: boolean; + sources: Array<'user' | 'project'>; + shouldIncludeInTools: boolean; +}> { + const settings = await settingsService.getGlobalSettings(); + const enabled = settings.enableSkills ?? true; // Default enabled + const sources = settings.skillsSources ?? ['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. + * + * @param settingsService - Settings service instance + * @param projectPath - Path to the project for loading project-specific subagents + * @returns Record of agent names to definitions, or undefined if none configured + */ +export async function getCustomSubagents( + settingsService: SettingsService, + projectPath?: string +): Promise | undefined> { + // Get global subagents + const globalSettings = await settingsService.getGlobalSettings(); + const globalSubagents = globalSettings.customSubagents || {}; + + // If no project path, return only global subagents + if (!projectPath) { + return Object.keys(globalSubagents).length > 0 ? globalSubagents : undefined; + } + + // Get project-specific subagents + const projectSettings = await settingsService.getProjectSettings(projectPath); + const projectSubagents = projectSettings.customSubagents || {}; + + // Merge: project-level takes precedence + const merged = { + ...globalSubagents, + ...projectSubagents, + }; + + return Object.keys(merged).length > 0 ? merged : undefined; +} diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index 50e378be..5d033a3b 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -72,7 +72,17 @@ 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']; + const defaultTools = [ + 'Read', + 'Write', + 'Edit', + 'Glob', + 'Grep', + 'Bash', + 'WebSearch', + 'WebFetch', + 'Skill', + ]; // AUTONOMOUS MODE: Always bypass permissions and allow unrestricted tools // Only restrict tools when no MCP servers are configured @@ -104,6 +114,8 @@ export class ClaudeProvider extends BaseProvider { ...(options.mcpServers && { mcpServers: options.mcpServers }), // Extended thinking configuration ...(maxThinkingTokens && { maxThinkingTokens }), + // Subagents configuration for specialized task delegation + ...(options.agents && { agents: options.agents }), }; // Build prompt payload diff --git a/apps/server/src/routes/settings/index.ts b/apps/server/src/routes/settings/index.ts index cc164856..6f6f6d40 100644 --- a/apps/server/src/routes/settings/index.ts +++ b/apps/server/src/routes/settings/index.ts @@ -23,6 +23,7 @@ import { createGetProjectHandler } from './routes/get-project.js'; import { createUpdateProjectHandler } from './routes/update-project.js'; import { createMigrateHandler } from './routes/migrate.js'; import { createStatusHandler } from './routes/status.js'; +import { createDiscoverAgentsHandler } from './routes/discover-agents.js'; /** * Create settings router with all endpoints @@ -39,6 +40,7 @@ import { createStatusHandler } from './routes/status.js'; * - POST /project - Get project settings (requires projectPath in body) * - PUT /project - Update project settings * - POST /migrate - Migrate settings from localStorage + * - POST /agents/discover - Discover filesystem agents from .claude/agents/ (read-only) * * @param settingsService - Instance of SettingsService for file I/O * @returns Express Router configured with all settings endpoints @@ -72,5 +74,8 @@ export function createSettingsRoutes(settingsService: SettingsService): Router { // Migration from localStorage router.post('/migrate', createMigrateHandler(settingsService)); + // Filesystem agents discovery (read-only) + router.post('/agents/discover', createDiscoverAgentsHandler()); + return router; } diff --git a/apps/server/src/routes/settings/routes/discover-agents.ts b/apps/server/src/routes/settings/routes/discover-agents.ts new file mode 100644 index 00000000..aee4a2a2 --- /dev/null +++ b/apps/server/src/routes/settings/routes/discover-agents.ts @@ -0,0 +1,61 @@ +/** + * Discover Agents Route - Returns filesystem-based agents from .claude/agents/ + * + * Scans both user-level (~/.claude/agents/) and project-level (.claude/agents/) + * directories for AGENT.md files and returns parsed agent definitions. + */ + +import type { Request, Response } from 'express'; +import { discoverFilesystemAgents } from '../../../lib/agent-discovery.js'; +import { createLogger } from '@automaker/utils'; + +const logger = createLogger('DiscoverAgentsRoute'); + +interface DiscoverAgentsRequest { + projectPath?: string; + sources?: Array<'user' | 'project'>; +} + +/** + * Create handler for discovering filesystem agents + * + * POST /api/settings/agents/discover + * Body: { projectPath?: string, sources?: ['user', 'project'] } + * + * Returns: + * { + * success: true, + * agents: Array<{ + * name: string, + * definition: AgentDefinition, + * source: 'user' | 'project', + * filePath: string + * }> + * } + */ +export function createDiscoverAgentsHandler() { + return async (req: Request, res: Response) => { + try { + const { projectPath, sources = ['user', 'project'] } = req.body as DiscoverAgentsRequest; + + logger.info( + `Discovering agents from sources: ${sources.join(', ')}${projectPath ? ` (project: ${projectPath})` : ''}` + ); + + const agents = await discoverFilesystemAgents(projectPath, sources); + + logger.info(`Discovered ${agents.length} filesystem agents`); + + res.json({ + success: true, + agents, + }); + } catch (error) { + logger.error('Failed to discover agents:', error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : 'Failed to discover agents', + }); + } + }; +} diff --git a/apps/server/src/services/agent-service.ts b/apps/server/src/services/agent-service.ts index 7736fd6a..e8678d63 100644 --- a/apps/server/src/services/agent-service.ts +++ b/apps/server/src/services/agent-service.ts @@ -24,6 +24,8 @@ import { filterClaudeMdFromContext, getMCPServersFromSettings, getPromptCustomization, + getSkillsConfiguration, + getCustomSubagents, } from '../lib/settings-helpers.js'; interface Message { @@ -241,6 +243,16 @@ export class AgentService { // Load MCP servers from settings (global setting only) const mcpServers = await getMCPServersFromSettings(this.settingsService, '[AgentService]'); + // Get Skills configuration from settings + const skillsConfig = this.settingsService + ? 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; + // Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.) const contextResult = await loadContextFiles({ projectPath: effectiveWorkDir, @@ -275,7 +287,44 @@ export class AgentService { // Extract model, maxTurns, and allowedTools from SDK options const effectiveModel = sdkOptions.model!; const maxTurns = sdkOptions.maxTurns; - const allowedTools = sdkOptions.allowedTools as string[] | undefined; + let allowedTools = sdkOptions.allowedTools as string[] | undefined; + + // Build merged settingSources array (filter to only 'user' and 'project') + const settingSources: Array<'user' | 'project'> = []; + if (sdkOptions.settingSources) { + sdkOptions.settingSources.forEach((source) => { + if (source === 'user' || source === 'project') { + if (!settingSources.includes(source)) { + settingSources.push(source); + } + } + }); + } + // Merge skills sources (avoid duplicates) + if (skillsConfig.enabled && skillsConfig.sources.length > 0) { + skillsConfig.sources.forEach((source) => { + if (!settingSources.includes(source)) { + settingSources.push(source); + } + }); + } + + // Enhance allowedTools with Skills and Subagents tools + 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')) { + allowedTools.push('Skill'); + } + // Add Task tool if custom subagents are configured + if ( + customSubagents && + Object.keys(customSubagents).length > 0 && + !allowedTools.includes('Task') + ) { + allowedTools.push('Task'); + } + } // Get provider for this model const provider = ProviderFactory.getProviderForModel(effectiveModel); @@ -290,10 +339,11 @@ export class AgentService { allowedTools: allowedTools, abortController: session.abortController!, conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, - settingSources: sdkOptions.settingSources, + settingSources: settingSources.length > 0 ? settingSources : undefined, sandbox: sdkOptions.sandbox, // Pass sandbox configuration sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration + agents: customSubagents, // Pass custom subagents for task delegation }; // Build prompt content with images diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx index 57b2fe97..b8885100 100644 --- a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx @@ -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 */} + + + {/* Custom Subagents */} + + {showUsageTracking && } ); diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/index.ts b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/index.ts new file mode 100644 index 00000000..f2f57d04 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/index.ts @@ -0,0 +1,6 @@ +/** + * Hooks barrel export for Claude Settings Tab + */ + +export { useSkillsSettings } from './use-skills-settings'; +export { useSubagents } from './use-subagents'; diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts new file mode 100644 index 00000000..c6ab231a --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts @@ -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, + }; +} diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts new file mode 100644 index 00000000..d3b1ba04 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts @@ -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([]); + const [filesystemAgents, setFilesystemAgents] = useState([]); + + // 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, + }; +} diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/index.ts b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/index.ts new file mode 100644 index 00000000..8b79a1f8 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/index.ts @@ -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'; diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/skills-section.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/skills-section.tsx new file mode 100644 index 00000000..856184ad --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/skills-section.tsx @@ -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 ( +
+ {/* Header */} +
+
+
+ +
+
+

+ Skills + {enabled && ( + + {sources.length} source{sources.length !== 1 ? 's' : ''} active + + )} +

+

+ Filesystem-based capabilities Claude invokes autonomously +

+
+
+ +
+ + {/* Content */} +
+ {/* Sources Selection */} + {enabled && ( +
+ +
+ {/* User Skills Option */} + + + {/* Project Skills Option */} + +
+
+ )} + + {/* Help Text */} + {enabled && ( +
+
+
+ +
+
+

Auto-Discovery

+

+ Skills are automatically discovered when agents start. Define skills as{' '} + SKILL.md files. +

+
+
+ + + View Skills documentation + +
+ )} + + {/* Disabled State Empty Message */} + {!enabled && ( +
+ +

Skills are disabled

+

Enable to load filesystem-based capabilities

+
+ )} +
+
+ ); +} diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagent-card.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagent-card.tsx new file mode 100644 index 00000000..0b4e0371 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagent-card.tsx @@ -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 ( + +
+ {/* Main Card Content */} +
+ {/* Agent Icon */} +
+ +
+ + {/* Content */} +
+ {/* Header Row */} +
+

{name}

+ + + {modelDisplay} + + + + {toolCount === 'all' ? 'All' : toolCount} tools + + + + {scopeLabel} + +
+ + {/* Description */} +

+ {definition.description} +

+ + {/* File Path */} + {filePath && ( +
+ + {filePath} +
+ )} +
+ + {/* Expand Button */} + + + +
+ + {/* Expandable Prompt Section */} + +
+
+
+ System Prompt +
+ {definition.prompt} +
+
+
+
+
+ ); +} diff --git a/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx new file mode 100644 index 00000000..c6bcd979 --- /dev/null +++ b/apps/ui/src/components/views/settings-view/providers/claude-settings-tab/subagents-section.tsx @@ -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 ( +
+ {/* Header */} +
+
+
+ +
+
+

+ Custom Subagents + {subagentsWithScope.length > 0 && ( + + {subagentsWithScope.length} agent{subagentsWithScope.length !== 1 ? 's' : ''} + + )} +

+

+ Specialized agents Claude delegates to automatically +

+
+
+ +
+ + {/* Content */} +
+ {subagentsWithScope.length === 0 ? ( +
+ +

No agents found

+

+ Create .md files in{' '} + ~/.claude/agents/ + {hasProject && ( + <> + {' or '} + .claude/agents/ + + )} +

+ + + View Agents documentation + +
+ ) : ( +
+ {subagentsWithScope.map((agent) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 75c1e6c5..7ce0ab60 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -23,8 +23,6 @@ import type { SpecRegenerationEvent, SuggestionType, GitHubAPI, - GitHubIssue, - GitHubPR, IssueValidationInput, IssueValidationEvent, IdeationAPI, @@ -1677,6 +1675,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 diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index 57784b2a..004261a7 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -17,6 +17,7 @@ export type { McpStdioServerConfig, McpSSEServerConfig, McpHttpServerConfig, + AgentDefinition, } from './provider.js'; // Feature types diff --git a/libs/types/src/provider.ts b/libs/types/src/provider.ts index 5b3549a6..71e15975 100644 --- a/libs/types/src/provider.ts +++ b/libs/types/src/provider.ts @@ -62,6 +62,20 @@ export interface McpHttpServerConfig { headers?: Record; } +/** + * Subagent definition for specialized task delegation + */ +export interface AgentDefinition { + /** Natural language description of when to use this agent */ + description: string; + /** System prompt defining the agent's role and behavior */ + prompt: string; + /** Restricted tool list (if omitted, inherits all tools) */ + tools?: string[]; + /** Model override for this agent */ + model?: 'sonnet' | 'opus' | 'haiku' | 'inherit'; +} + /** * Options for executing a query via a provider */ @@ -90,6 +104,11 @@ export interface ExecuteOptions { * Only applies to Claude models; Cursor models handle thinking internally. */ thinkingLevel?: ThinkingLevel; + /** + * Custom subagents for specialized task delegation + * Key: agent name, Value: agent definition + */ + agents?: Record; } /** diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index 0220da3a..4a8814db 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -486,6 +486,29 @@ export interface GlobalSettings { // Prompt Customization /** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */ promptCustomization?: PromptCustomization; + + // Skills Configuration + /** + * Enable Skills functionality (loads from .claude/skills/ directories) + * @default true + */ + enableSkills?: boolean; + + /** + * Which directories to load Skills from + * - 'user': ~/.claude/skills/ (personal skills) + * - 'project': .claude/skills/ (project-specific skills) + * @default ['user', 'project'] + */ + skillsSources?: Array<'user' | 'project'>; + + // Subagents Configuration + /** + * Custom subagent definitions for specialized task delegation + * Key: agent name (e.g., 'code-reviewer', 'test-runner') + * Value: agent configuration + */ + customSubagents?: Record; } /** @@ -585,6 +608,15 @@ export interface ProjectSettings { // Claude Agent SDK Settings /** Auto-load CLAUDE.md files using SDK's settingSources option (project override) */ autoLoadClaudeMd?: boolean; + + // Subagents Configuration + /** + * Project-specific custom subagent definitions for specialized task delegation + * Merged with global customSubagents, project-level takes precedence + * Key: agent name (e.g., 'code-reviewer', 'test-runner') + * Value: agent configuration + */ + customSubagents?: Record; } /**