mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: add skills and subagents configuration support
- Updated .gitignore to include skills directory. - Introduced agent discovery functionality to scan for AGENT.md files in user and project directories. - Added new API endpoint for discovering filesystem agents. - Implemented UI components for managing skills and viewing custom subagents. - Enhanced settings helpers to retrieve skills configuration and custom subagents. - Updated agent service to incorporate skills and subagents in task delegation. These changes enhance the capabilities of the system by allowing users to define and manage skills and custom subagents effectively.
This commit is contained in:
3
.claude/.gitignore
vendored
3
.claude/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
hans/
|
||||
hans/
|
||||
skills/
|
||||
234
apps/server/src/lib/agent-discovery.ts
Normal file
234
apps/server/src/lib/agent-discovery.ts
Normal file
@@ -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<AgentDefinition | null> {
|
||||
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<FilesystemAgent[]> {
|
||||
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<FilesystemAgent[]> {
|
||||
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;
|
||||
}
|
||||
@@ -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<Record<string, import('@automaker/types').AgentDefinition> | 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
61
apps/server/src/routes/settings/routes/discover-agents.ts
Normal file
61
apps/server/src/routes/settings/routes/discover-agents.ts
Normal file
@@ -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',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useCliStatus } from '../hooks/use-cli-status';
|
||||
import { ClaudeCliStatus } from '../cli-status/claude-cli-status';
|
||||
import { ClaudeMdSettings } from '../claude/claude-md-settings';
|
||||
import { ClaudeUsageSection } from '../api-keys/claude-usage-section';
|
||||
import { SkillsSection } from './claude-settings-tab/skills-section';
|
||||
import { SubagentsSection } from './claude-settings-tab/subagents-section';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
export function ClaudeSettingsTab() {
|
||||
@@ -42,6 +44,13 @@ export function ClaudeSettingsTab() {
|
||||
autoLoadClaudeMd={autoLoadClaudeMd}
|
||||
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
|
||||
/>
|
||||
|
||||
{/* Skills Configuration */}
|
||||
<SkillsSection />
|
||||
|
||||
{/* Custom Subagents */}
|
||||
<SubagentsSection />
|
||||
|
||||
{showUsageTracking && <ClaudeUsageSection />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Subagents Hook - Manages custom subagent definitions
|
||||
*
|
||||
* Provides read-only view of custom subagent configurations
|
||||
* used for specialized task delegation. Supports:
|
||||
* - Programmatic agents (stored in settings JSON) - global and project-level
|
||||
* - Filesystem agents (AGENT.md files in .claude/agents/) - user and project-level (read-only)
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { AgentDefinition } from '@automaker/types';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
|
||||
export type SubagentScope = 'global' | 'project';
|
||||
export type SubagentType = 'programmatic' | 'filesystem';
|
||||
export type FilesystemSource = 'user' | 'project';
|
||||
|
||||
export interface SubagentWithScope {
|
||||
name: string;
|
||||
definition: AgentDefinition;
|
||||
scope: SubagentScope; // For programmatic agents
|
||||
type: SubagentType;
|
||||
// For filesystem agents:
|
||||
source?: FilesystemSource;
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
interface FilesystemAgent {
|
||||
name: string;
|
||||
definition: AgentDefinition;
|
||||
source: FilesystemSource;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export function useSubagents() {
|
||||
const { settings, currentProject, projectSettings } = useAppStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]);
|
||||
const [filesystemAgents, setFilesystemAgents] = useState<FilesystemAgent[]>([]);
|
||||
|
||||
// Fetch filesystem agents
|
||||
const fetchFilesystemAgents = async () => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']);
|
||||
|
||||
if (data.success) {
|
||||
setFilesystemAgents(data.agents || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch filesystem agents:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch filesystem agents on mount and when project changes
|
||||
useEffect(() => {
|
||||
fetchFilesystemAgents();
|
||||
}, [currentProject?.path]);
|
||||
|
||||
// Merge programmatic and filesystem agents
|
||||
useEffect(() => {
|
||||
const globalSubagents = settings?.customSubagents || {};
|
||||
const projectSubagents = projectSettings?.customSubagents || {};
|
||||
|
||||
const merged: SubagentWithScope[] = [];
|
||||
|
||||
// Add programmatic global agents
|
||||
Object.entries(globalSubagents).forEach(([name, definition]) => {
|
||||
merged.push({ name, definition, scope: 'global', type: 'programmatic' });
|
||||
});
|
||||
|
||||
// Add programmatic project agents (override globals with same name)
|
||||
Object.entries(projectSubagents).forEach(([name, definition]) => {
|
||||
const globalIndex = merged.findIndex((s) => s.name === name && s.scope === 'global');
|
||||
if (globalIndex !== -1) {
|
||||
merged.splice(globalIndex, 1);
|
||||
}
|
||||
merged.push({ name, definition, scope: 'project', type: 'programmatic' });
|
||||
});
|
||||
|
||||
// Add filesystem agents
|
||||
filesystemAgents.forEach(({ name, definition, source, filePath }) => {
|
||||
// Remove any programmatic agents with the same name (filesystem takes precedence)
|
||||
const programmaticIndex = merged.findIndex((s) => s.name === name);
|
||||
if (programmaticIndex !== -1) {
|
||||
merged.splice(programmaticIndex, 1);
|
||||
}
|
||||
|
||||
merged.push({
|
||||
name,
|
||||
definition,
|
||||
scope: source === 'user' ? 'global' : 'project',
|
||||
type: 'filesystem',
|
||||
source,
|
||||
filePath,
|
||||
});
|
||||
});
|
||||
|
||||
setSubagentsWithScope(merged);
|
||||
}, [settings?.customSubagents, projectSettings?.customSubagents, filesystemAgents]);
|
||||
|
||||
return {
|
||||
subagentsWithScope,
|
||||
isLoading,
|
||||
hasProject: !!currentProject,
|
||||
refreshFilesystemAgents: fetchFilesystemAgents,
|
||||
};
|
||||
}
|
||||
@@ -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,109 @@
|
||||
/**
|
||||
* Subagents Section - UI for viewing filesystem-based agents
|
||||
*
|
||||
* Displays agents discovered from:
|
||||
* - User-level: ~/.claude/agents/
|
||||
* - Project-level: .claude/agents/
|
||||
*
|
||||
* Read-only view - agents are managed by editing .md files directly.
|
||||
*/
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Bot, RefreshCw, Loader2, Users, ExternalLink } from 'lucide-react';
|
||||
import { useSubagents } from './hooks/use-subagents';
|
||||
import { SubagentCard } from './subagent-card';
|
||||
|
||||
export function SubagentsSection() {
|
||||
const { subagentsWithScope, isLoading, hasProject, refreshFilesystemAgents } = useSubagents();
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await refreshFilesystemAgents();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-linear-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border/30">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-violet-500/20 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5 text-violet-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-base flex items-center gap-2">
|
||||
Custom Subagents
|
||||
{subagentsWithScope.length > 0 && (
|
||||
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-violet-500/20 text-violet-500">
|
||||
{subagentsWithScope.length} agent{subagentsWithScope.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Specialized agents Claude delegates to automatically
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isLoading}
|
||||
title="Refresh agents from disk"
|
||||
className="gap-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
<span className="text-xs">Refresh</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
{subagentsWithScope.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Users className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm font-medium">No agents found</p>
|
||||
<p className="text-xs mt-2 max-w-sm mx-auto">
|
||||
Create <code className="text-xs bg-muted px-1 rounded">.md</code> files in{' '}
|
||||
<code className="text-xs bg-muted px-1 rounded">~/.claude/agents/</code>
|
||||
{hasProject && (
|
||||
<>
|
||||
{' or '}
|
||||
<code className="text-xs bg-muted px-1 rounded">.claude/agents/</code>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<a
|
||||
href="https://code.claude.com/docs/en/agents"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 mt-4 text-xs text-brand-500 hover:text-brand-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
View Agents documentation
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{subagentsWithScope.map((agent) => (
|
||||
<SubagentCard
|
||||
key={`${agent.type}-${agent.source || agent.scope}-${agent.name}`}
|
||||
agent={agent}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,7 @@ export type {
|
||||
McpStdioServerConfig,
|
||||
McpSSEServerConfig,
|
||||
McpHttpServerConfig,
|
||||
AgentDefinition,
|
||||
} from './provider.js';
|
||||
|
||||
// Feature types
|
||||
|
||||
@@ -62,6 +62,20 @@ export interface McpHttpServerConfig {
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, AgentDefinition>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string, import('./provider.js').AgentDefinition>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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<string, import('./provider.js').AgentDefinition>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user