diff --git a/packages/desktop/src/App.tsx b/packages/desktop/src/App.tsx index 9d230c7..c068cd8 100644 --- a/packages/desktop/src/App.tsx +++ b/packages/desktop/src/App.tsx @@ -10,6 +10,7 @@ import { CommandPanel, MCPPanel, HooksPanel, + AgentsPanel, Toaster, listSessions, createSession, @@ -25,6 +26,7 @@ export function App() { const [showCommands, setShowCommands] = useState(false); const [showMCP, setShowMCP] = useState(false); const [showHooks, setShowHooks] = useState(false); + const [showAgents, setShowAgents] = useState(false); const [sessionTitleUpdate, setSessionTitleUpdate] = useState<{ sessionId: string; name: string } | null>(null); // 初始化:加载或创建会话 @@ -98,6 +100,7 @@ export function App() { onOpenCommands={() => setShowCommands(true)} onOpenMCP={() => setShowMCP(true)} onOpenHooks={() => setShowHooks(true)} + onOpenAgents={() => setShowAgents(true)} /> ) : (
@@ -130,6 +133,9 @@ export function App() { {/* Hooks 面板 */} {showHooks && setShowHooks(false)} />} + {/* Agents 面板 */} + {showAgents && setShowAgents(false)} />} + {/* Toast 通知 */}
diff --git a/packages/desktop/src/pages/Chat.tsx b/packages/desktop/src/pages/Chat.tsx index fb0317b..f85b743 100644 --- a/packages/desktop/src/pages/Chat.tsx +++ b/packages/desktop/src/pages/Chat.tsx @@ -3,7 +3,7 @@ */ import { useEffect, useRef } from 'react'; -import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal, Plug, Zap } from 'lucide-react'; +import { WifiOff, MessageSquare, Settings, FolderOpen, Terminal, Plug, Zap, Bot } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useChat, @@ -23,6 +23,7 @@ interface ChatPageProps { onOpenCommands?: () => void; onOpenMCP?: () => void; onOpenHooks?: () => void; + onOpenAgents?: () => void; } export function ChatPage({ @@ -34,6 +35,7 @@ export function ChatPage({ onOpenCommands, onOpenMCP, onOpenHooks, + onOpenAgents, }: ChatPageProps) { const { messages, @@ -127,8 +129,21 @@ export function ChatPage({ {/* 工具栏按钮 */} - {(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks) && ( + {(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents) && (
+ {/* Agents 按钮 */} + {onOpenAgents && ( + + + + )} + {/* Hooks 按钮 */} {onOpenHooks && ( >; +} + +// Core Agent 模块类型 +interface AgentModule { + agentRegistry: { + init: (workdir: string) => Promise; + get: (name: string) => AgentInfo | undefined; + list: () => AgentInfo[]; + }; + loadAgentConfig: (workdir: string) => Promise; + saveAgentConfig: (workdir: string, config: AgentConfigFile, format?: 'json' | 'yaml') => Promise; + presetAgents: Record>; + isPresetAgent: (name: string) => boolean; +} + +// API 响应类型 +interface AgentListItem { + name: string; + description: string; + mode: AgentMode; + isPreset: boolean; + isCustomized: boolean; + model?: string; + maxSteps?: number; +} + +interface AgentDefaults { + maxSteps?: number; + model?: AgentModelConfig; + permission?: AgentPermission; +} + +export const agentsRouter = new Hono(); + +// Core 模块缓存 +let agentModule: AgentModule | null = null; +let initialized = false; + +/** + * 初始化 Agent 模块 + */ +async function initAgentModule(): Promise { + if (agentModule) return agentModule; + + try { + const corePath = '@ai-assistant/core'; + const core = (await import(corePath)) as Record; + + if ( + !core.agentRegistry || + typeof core.loadAgentConfig !== 'function' || + typeof core.saveAgentConfig !== 'function' || + !core.presetAgents || + typeof core.isPresetAgent !== 'function' + ) { + console.warn('[Agents] Core module missing Agent exports'); + return null; + } + + agentModule = { + agentRegistry: core.agentRegistry as AgentModule['agentRegistry'], + loadAgentConfig: core.loadAgentConfig as AgentModule['loadAgentConfig'], + saveAgentConfig: core.saveAgentConfig as AgentModule['saveAgentConfig'], + presetAgents: core.presetAgents as AgentModule['presetAgents'], + isPresetAgent: core.isPresetAgent as AgentModule['isPresetAgent'], + }; + + console.log('[Agents] Agent module initialized'); + return agentModule; + } catch (error) { + console.warn('[Agents] Failed to load Agent module:', error); + return null; + } +} + +/** + * 确保 AgentRegistry 已初始化 + */ +async function ensureRegistryInitialized(): Promise { + const module = await initAgentModule(); + if (!module) return false; + + if (!initialized) { + const config = getConfig(); + await module.agentRegistry.init(config.workdir); + initialized = true; + } + + return true; +} + +/** + * 将 AgentInfo 转换为 AgentListItem + */ +function toListItem(agent: AgentInfo, module: AgentModule, customAgentNames: Set): AgentListItem { + const isPreset = module.isPresetAgent(agent.name); + return { + name: agent.name, + description: agent.description, + mode: agent.mode, + isPreset, + isCustomized: customAgentNames.has(agent.name), + model: agent.model?.model, + maxSteps: agent.maxSteps, + }; +} + +/** + * GET /agents - 获取所有 Agent 列表 + */ +agentsRouter.get('/', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + if (!(await ensureRegistryInitialized())) { + return c.json( + { + success: false, + error: 'Failed to initialize agent registry', + }, + 503 + ); + } + + const config = getConfig(); + const userConfig = await module.loadAgentConfig(config.workdir); + const customAgentNames = new Set(Object.keys(userConfig?.agents || {})); + + const agents = module.agentRegistry.list(); + const items = agents.map((agent) => toListItem(agent, module, customAgentNames)); + + return c.json({ + success: true, + data: items, + }); +}); + +/** + * GET /agents/presets - 获取预设 Agent 列表 + */ +agentsRouter.get('/presets', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + const presets = Object.entries(module.presetAgents).map(([name, agent]) => ({ + name, + description: agent.description, + mode: agent.mode, + isPreset: true, + isCustomized: false, + model: agent.model?.model, + maxSteps: agent.maxSteps, + })); + + return c.json({ + success: true, + data: presets, + }); +}); + +/** + * GET /agents/defaults - 获取全局默认配置 + */ +agentsRouter.get('/defaults', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + const config = getConfig(); + const userConfig = await module.loadAgentConfig(config.workdir); + + return c.json({ + success: true, + data: userConfig?.defaults || {}, + }); +}); + +/** + * PUT /agents/defaults - 更新全局默认配置 + */ +agentsRouter.put('/defaults', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + try { + const newDefaults = await c.req.json(); + const config = getConfig(); + + // 加载现有配置 + let userConfig = await module.loadAgentConfig(config.workdir); + if (!userConfig) { + userConfig = {}; + } + + // 更新 defaults + userConfig.defaults = newDefaults; + + // 保存配置 + await module.saveAgentConfig(config.workdir, userConfig); + + // 重新初始化 registry 以应用新配置 + initialized = false; + await ensureRegistryInitialized(); + + return c.json({ + success: true, + data: newDefaults, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to update defaults', + }, + 500 + ); + } +}); + +/** + * GET /agents/:name - 获取单个 Agent 详情 + */ +agentsRouter.get('/:name', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + if (!(await ensureRegistryInitialized())) { + return c.json( + { + success: false, + error: 'Failed to initialize agent registry', + }, + 503 + ); + } + + const name = c.req.param('name'); + const agent = module.agentRegistry.get(name); + + if (!agent) { + return c.json( + { + success: false, + error: `Agent '${name}' not found`, + }, + 404 + ); + } + + const config = getConfig(); + const userConfig = await module.loadAgentConfig(config.workdir); + const isPreset = module.isPresetAgent(name); + const isCustomized = !!(userConfig?.agents && name in userConfig.agents); + + return c.json({ + success: true, + data: { + ...agent, + isPreset, + isCustomized, + }, + }); +}); + +/** + * POST /agents - 创建新 Agent + */ +agentsRouter.post('/', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + try { + const body = await c.req.json<{ name: string } & Omit>(); + const { name, ...agentConfig } = body; + + if (!name || typeof name !== 'string') { + return c.json( + { + success: false, + error: 'Agent name is required', + }, + 400 + ); + } + + // 检查名称是否与预设冲突 + if (module.isPresetAgent(name)) { + return c.json( + { + success: false, + error: `Cannot create agent with preset name '${name}'. Use PUT to customize a preset.`, + }, + 400 + ); + } + + const config = getConfig(); + let userConfig = await module.loadAgentConfig(config.workdir); + if (!userConfig) { + userConfig = {}; + } + if (!userConfig.agents) { + userConfig.agents = {}; + } + + // 检查是否已存在 + if (name in userConfig.agents) { + return c.json( + { + success: false, + error: `Agent '${name}' already exists`, + }, + 409 + ); + } + + // 添加新 Agent + userConfig.agents[name] = agentConfig; + + // 保存配置 + await module.saveAgentConfig(config.workdir, userConfig); + + // 重新初始化 registry + initialized = false; + await ensureRegistryInitialized(); + + const createdAgent = module.agentRegistry.get(name); + + return c.json({ + success: true, + data: createdAgent + ? { + ...createdAgent, + isPreset: false, + isCustomized: false, + } + : { name, ...agentConfig, isPreset: false, isCustomized: false }, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to create agent', + }, + 500 + ); + } +}); + +/** + * PUT /agents/:name - 更新 Agent + */ +agentsRouter.put('/:name', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + try { + const name = c.req.param('name'); + const agentConfig = await c.req.json>(); + + const config = getConfig(); + let userConfig = await module.loadAgentConfig(config.workdir); + if (!userConfig) { + userConfig = {}; + } + if (!userConfig.agents) { + userConfig.agents = {}; + } + + // 更新 Agent(支持覆盖预设) + userConfig.agents[name] = agentConfig; + + // 保存配置 + await module.saveAgentConfig(config.workdir, userConfig); + + // 重新初始化 registry + initialized = false; + await ensureRegistryInitialized(); + + const updatedAgent = module.agentRegistry.get(name); + const isPreset = module.isPresetAgent(name); + + return c.json({ + success: true, + data: updatedAgent + ? { + ...updatedAgent, + isPreset, + isCustomized: true, + } + : { name, ...agentConfig, isPreset, isCustomized: true }, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to update agent', + }, + 500 + ); + } +}); + +/** + * DELETE /agents/:name - 删除 Agent + */ +agentsRouter.delete('/:name', async (c) => { + const module = await initAgentModule(); + + if (!module) { + return c.json( + { + success: false, + error: 'Agent module not available', + }, + 503 + ); + } + + try { + const name = c.req.param('name'); + const config = getConfig(); + const userConfig = await module.loadAgentConfig(config.workdir); + + if (!userConfig?.agents || !(name in userConfig.agents)) { + // 如果是预设 Agent,返回特定错误 + if (module.isPresetAgent(name)) { + return c.json( + { + success: false, + error: `Cannot delete preset agent '${name}'`, + }, + 400 + ); + } + + return c.json( + { + success: false, + error: `Agent '${name}' not found in user configuration`, + }, + 404 + ); + } + + // 删除 Agent + delete userConfig.agents[name]; + + // 保存配置 + await module.saveAgentConfig(config.workdir, userConfig); + + // 重新初始化 registry + initialized = false; + await ensureRegistryInitialized(); + + return c.json({ + success: true, + data: null, + }); + } catch (error) { + return c.json( + { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete agent', + }, + 500 + ); + } +}); diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 5bd4fa4..1ae4619 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -11,3 +11,4 @@ export { filesRouter, setWorkingDirectory, getWorkingDirectory } from './files.j export { commandsRouter } from './commands.js'; export { mcpRouter } from './mcp.js'; export { hooksRouter } from './hooks.js'; +export { agentsRouter } from './agents.js'; diff --git a/packages/ui/src/api/client.ts b/packages/ui/src/api/client.ts index 615ddae..8abd4ef 100644 --- a/packages/ui/src/api/client.ts +++ b/packages/ui/src/api/client.ts @@ -24,6 +24,10 @@ import type { FileHookConfig, ShellCommandConfig, HookTestResult, + AgentListItem, + AgentDetail, + AgentInput, + AgentDefaults, } from './types.js'; // Re-export types @@ -54,6 +58,19 @@ export type { FileHookConfig, ShellCommandConfig, HookTestResult, + // Agent types + AgentMode, + AgentModelConfig, + AgentToolConfig, + PermissionRule, + AgentBashPermission, + AgentFilePermission, + AgentGitPermission, + AgentPermission, + AgentListItem, + AgentDetail, + AgentInput, + AgentDefaults, } from './types.js'; // API Configuration @@ -476,3 +493,98 @@ export async function testHookCommand(command: ShellCommandConfig): Promise<{ }> { return request('POST', '/hooks/test', command); } + +// ============ Agents API ============ + +/** + * 获取所有 Agent 列表 + */ +export async function listAgents(): Promise<{ + success: boolean; + data: AgentListItem[]; + error?: string; +}> { + return request('GET', '/agents'); +} + +/** + * 获取单个 Agent 详情 + */ +export async function getAgent(name: string): Promise<{ + success: boolean; + data?: AgentDetail; + error?: string; +}> { + return request('GET', `/agents/${encodeURIComponent(name)}`); +} + +/** + * 创建新 Agent + */ +export async function createAgent( + name: string, + config: AgentInput +): Promise<{ + success: boolean; + data?: AgentDetail; + error?: string; +}> { + return request('POST', '/agents', { name, ...config }); +} + +/** + * 更新 Agent + */ +export async function updateAgent( + name: string, + config: AgentInput +): Promise<{ + success: boolean; + data?: AgentDetail; + error?: string; +}> { + return request('PUT', `/agents/${encodeURIComponent(name)}`, config); +} + +/** + * 删除 Agent + */ +export async function deleteAgent(name: string): Promise<{ + success: boolean; + error?: string; +}> { + return request('DELETE', `/agents/${encodeURIComponent(name)}`); +} + +/** + * 获取预设 Agent 列表 + */ +export async function listPresetAgents(): Promise<{ + success: boolean; + data: AgentListItem[]; + error?: string; +}> { + return request('GET', '/agents/presets'); +} + +/** + * 获取全局默认配置 + */ +export async function getAgentDefaults(): Promise<{ + success: boolean; + data: AgentDefaults; + error?: string; +}> { + return request('GET', '/agents/defaults'); +} + +/** + * 更新全局默认配置 + */ +export async function updateAgentDefaults(defaults: AgentDefaults): Promise<{ + success: boolean; + data: AgentDefaults; + error?: string; +}> { + return request('PUT', '/agents/defaults', defaults); +} diff --git a/packages/ui/src/api/types.ts b/packages/ui/src/api/types.ts index 68c33c8..14e3dda 100644 --- a/packages/ui/src/api/types.ts +++ b/packages/ui/src/api/types.ts @@ -281,3 +281,146 @@ export interface HookTestResult { /** 执行时间(毫秒) */ duration: number; } + +// ============ Agent 相关 ============ + +/** Agent 运行模式 */ +export type AgentMode = 'primary' | 'subagent' | 'all'; + +/** Agent 模型配置 */ +export interface AgentModelConfig { + /** 提供商 */ + provider?: 'anthropic' | 'deepseek' | 'openai'; + /** 模型名称 */ + model?: string; + /** 温度参数 (0-1) */ + temperature?: number; + /** Top-P 采样 */ + topP?: number; + /** 最大 Token 数 */ + maxTokens?: number; +} + +/** Agent 工具配置 */ +export interface AgentToolConfig { + /** 禁用的工具列表 */ + disabled?: string[]; + /** 只启用的工具列表(与 disabled 互斥) */ + enabled?: string[]; + /** 禁止嵌套 Task 调用 */ + noTask?: boolean; +} + +/** 权限规则 */ +export interface PermissionRule { + /** 匹配模式(glob 或正则) */ + pattern: string; + /** 操作:允许或拒绝 */ + action: 'allow' | 'deny'; +} + +/** Bash 权限配置 */ +export interface AgentBashPermission { + /** 规则列表 */ + rules?: PermissionRule[]; + /** 默认操作 */ + defaultAction?: 'allow' | 'deny'; +} + +/** 文件权限配置 */ +export interface AgentFilePermission { + /** 读取权限 */ + read?: { rules?: PermissionRule[]; defaultAction?: 'allow' | 'deny' }; + /** 写入权限 */ + write?: { rules?: PermissionRule[]; defaultAction?: 'allow' | 'deny' }; +} + +/** Git 权限配置 */ +export interface AgentGitPermission { + /** 允许的命令 */ + commands?: string[]; + /** 是否允许 push */ + allowPush?: boolean; + /** 是否允许 force 操作 */ + allowForce?: boolean; +} + +/** Agent 权限配置 */ +export interface AgentPermission { + /** Bash 命令权限 */ + bash?: AgentBashPermission; + /** 文件操作权限 */ + file?: AgentFilePermission; + /** Git 操作权限 */ + git?: AgentGitPermission; +} + +/** Agent 列表项 */ +export interface AgentListItem { + /** Agent 名称 */ + name: string; + /** 描述 */ + description: string; + /** 运行模式 */ + mode: AgentMode; + /** 是否为预设 Agent */ + isPreset: boolean; + /** 是否被用户自定义覆盖 */ + isCustomized: boolean; + /** 使用的模型 */ + model?: string; + /** 最大执行步数 */ + maxSteps?: number; +} + +/** Agent 完整详情 */ +export interface AgentDetail { + /** Agent 名称 */ + name: string; + /** 描述 */ + description: string; + /** 运行模式 */ + mode: AgentMode; + /** 是否为预设 */ + isPreset: boolean; + /** 是否被自定义 */ + isCustomized?: boolean; + /** System Prompt */ + prompt?: string; + /** 模型配置 */ + model?: AgentModelConfig; + /** 工具配置 */ + tools?: AgentToolConfig; + /** 权限配置 */ + permission?: AgentPermission; + /** 最大执行步数 */ + maxSteps?: number; +} + +/** 创建/更新 Agent 输入 */ +export interface AgentInput { + /** 描述 */ + description: string; + /** 运行模式 */ + mode: AgentMode; + /** System Prompt */ + prompt?: string; + /** 模型配置 */ + model?: AgentModelConfig; + /** 工具配置 */ + tools?: AgentToolConfig; + /** 权限配置 */ + permission?: AgentPermission; + /** 最大执行步数 */ + maxSteps?: number; +} + +/** 全局默认配置 */ +export interface AgentDefaults { + /** 最大执行步数 */ + maxSteps?: number; + /** 模型配置 */ + model?: AgentModelConfig; + /** 权限配置 */ + permission?: AgentPermission; +} diff --git a/packages/ui/src/components/AgentDefaultsEditor.tsx b/packages/ui/src/components/AgentDefaultsEditor.tsx new file mode 100644 index 0000000..784f3cb --- /dev/null +++ b/packages/ui/src/components/AgentDefaultsEditor.tsx @@ -0,0 +1,320 @@ +/** + * AgentDefaultsEditor Component + * + * 全局默认配置编辑器:配置所有 Agent 的默认参数 + */ + +import { useState, useEffect } from 'react'; +import { + X, + Save, + Settings, + AlertCircle, +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { toast } from 'sonner'; +import { cn } from '../utils/cn'; +import { modalOverlay, modalContent, smoothTransition } from '../utils/animations'; +import { Button } from '../primitives/Button'; +import { + getAgentDefaults, + updateAgentDefaults, + type AgentDefaults, + type AgentModelConfig, +} from '../api/client.js'; + +interface AgentDefaultsEditorProps { + onClose: () => void; + onSave: () => void; + /** 是否启用响应式布局 */ + responsive?: boolean; +} + +export function AgentDefaultsEditor({ + onClose, + onSave, + responsive = false, +}: AgentDefaultsEditorProps) { + // 表单状态 + const [maxSteps, setMaxSteps] = useState(undefined); + + // 模型配置 + const [modelProvider, setModelProvider] = useState(''); + const [modelName, setModelName] = useState(''); + const [temperature, setTemperature] = useState(undefined); + const [maxTokens, setMaxTokens] = useState(undefined); + + // UI 状态 + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + // 加载现有配置 + useEffect(() => { + const loadDefaults = async () => { + setLoading(true); + try { + const result = await getAgentDefaults(); + if (result.success && result.data) { + const defaults = result.data; + + setMaxSteps(defaults.maxSteps); + + if (defaults.model) { + setModelProvider(defaults.model.provider || ''); + setModelName(defaults.model.model || ''); + setTemperature(defaults.model.temperature); + setMaxTokens(defaults.model.maxTokens); + } + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load defaults'); + } finally { + setLoading(false); + } + }; + + loadDefaults(); + }, []); + + // 构建配置对象 + const buildDefaults = (): AgentDefaults => { + const defaults: AgentDefaults = {}; + + if (maxSteps !== undefined) { + defaults.maxSteps = maxSteps; + } + + // 模型配置 + const model: AgentModelConfig = {}; + if (modelProvider) { + model.provider = modelProvider as 'anthropic' | 'deepseek' | 'openai'; + } + if (modelName) { + model.model = modelName; + } + if (temperature !== undefined) { + model.temperature = temperature; + } + if (maxTokens !== undefined) { + model.maxTokens = maxTokens; + } + if (Object.keys(model).length > 0) { + defaults.model = model; + } + + return defaults; + }; + + // 保存 + const handleSave = async () => { + setSaving(true); + setError(null); + + try { + const defaults = buildDefaults(); + const result = await updateAgentDefaults(defaults); + + if (result.success) { + toast.success('Default settings saved'); + onSave(); + } else { + setError(result.error || 'Save failed'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Save failed'); + } finally { + setSaving(false); + } + }; + + return ( + + + e.stopPropagation()} + className={cn( + 'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col', + responsive + ? 'w-full md:w-full md:max-w-lg md:mx-4 rounded-t-2xl md:rounded-lg' + : 'rounded-lg w-full max-w-lg mx-4' + )} + > + {/* Header */} +
+ {responsive && ( +
+ )} +
+

+ + Global Defaults +

+

+ These settings apply to all agents unless overridden +

+
+ +
+ + {/* Content */} +
+ {loading ? ( +
+
+
+ ) : ( + <> + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {/* Execution Limits */} +
+

Execution Limits

+
+ + + setMaxSteps(e.target.value ? parseInt(e.target.value) : undefined) + } + placeholder="15" + min="1" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + /> +

+ Maximum number of tool call steps for all agents +

+
+
+ + {/* Model Configuration */} +
+

Default Model

+
+ {/* Provider */} +
+ + +
+ + {/* Model */} +
+ + setModelName(e.target.value)} + placeholder="claude-sonnet-4-20250514" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + /> +
+ + {/* Temperature */} +
+ + + setTemperature(e.target.value ? parseFloat(e.target.value) : undefined) + } + placeholder="0.7" + min="0" + max="1" + step="0.1" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + /> +
+ + {/* Max Tokens */} +
+ + + setMaxTokens(e.target.value ? parseInt(e.target.value) : undefined) + } + placeholder="8192" + min="1" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + /> +
+
+
+ + {/* Info */} +
+

+ These defaults will be applied to all agents. Individual agents can override + these settings in their own configuration. +

+
+ + )} +
+ + {/* Footer */} +
+ + +
+ + + + ); +} diff --git a/packages/ui/src/components/AgentEditor.tsx b/packages/ui/src/components/AgentEditor.tsx new file mode 100644 index 0000000..fc75793 --- /dev/null +++ b/packages/ui/src/components/AgentEditor.tsx @@ -0,0 +1,592 @@ +/** + * AgentEditor Component + * + * Agent 创建/编辑器:配置 Agent 的各项参数 + */ + +import { useState, useEffect } from 'react'; +import { + X, + Save, + Bot, + ChevronDown, + ChevronRight, + AlertCircle, +} from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { toast } from 'sonner'; +import { cn } from '../utils/cn'; +import { modalOverlay, modalContent, smoothTransition } from '../utils/animations'; +import { Button } from '../primitives/Button'; +import { + getAgent, + createAgent, + updateAgent, + type AgentInput, + type AgentMode, + type AgentModelConfig, + type AgentToolConfig, +} from '../api/client.js'; + +interface AgentEditorProps { + /** 编辑现有 Agent 的名称(新建时为 undefined) */ + agentName?: string; + /** 新建时的默认名称 */ + defaultName?: string; + /** 复制自哪个 Agent */ + copyFrom?: string; + onClose: () => void; + onSave: () => void; + /** 是否启用响应式布局 */ + responsive?: boolean; +} + +// 可折叠区域组件 +function CollapsibleSection({ + title, + children, + defaultOpen = false, +}: { + title: string; + children: React.ReactNode; + defaultOpen?: boolean; +}) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+ + + {isOpen && ( + +
{children}
+
+ )} +
+
+ ); +} + +export function AgentEditor({ + agentName, + defaultName = '', + copyFrom, + onClose, + onSave, + responsive = false, +}: AgentEditorProps) { + const isNewAgent = !agentName; + + // 表单状态 + const [name, setName] = useState(defaultName); + const [description, setDescription] = useState(''); + const [mode, setMode] = useState('primary'); + const [prompt, setPrompt] = useState(''); + const [maxSteps, setMaxSteps] = useState(undefined); + + // 模型配置 + const [modelProvider, setModelProvider] = useState(''); + const [modelName, setModelName] = useState(''); + const [temperature, setTemperature] = useState(undefined); + const [maxTokens, setMaxTokens] = useState(undefined); + + // 工具配置 + const [toolMode, setToolMode] = useState<'all' | 'enabled' | 'disabled'>('all'); + const [toolList, setToolList] = useState(''); + const [noTask, setNoTask] = useState(false); + + // UI 状态 + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + // 加载现有 Agent 数据 + useEffect(() => { + const loadAgent = async () => { + const targetName = agentName || copyFrom; + if (!targetName) return; + + setLoading(true); + try { + const result = await getAgent(targetName); + if (result.success && result.data) { + const agent = result.data; + + if (!agentName) { + // 复制模式:不复制名称 + setName(defaultName); + } else { + setName(agent.name); + } + + setDescription(agent.description); + setMode(agent.mode); + setPrompt(agent.prompt || ''); + setMaxSteps(agent.maxSteps); + + // 模型配置 + if (agent.model) { + setModelProvider(agent.model.provider || ''); + setModelName(agent.model.model || ''); + setTemperature(agent.model.temperature); + setMaxTokens(agent.model.maxTokens); + } + + // 工具配置 + if (agent.tools) { + if (agent.tools.enabled && agent.tools.enabled.length > 0) { + setToolMode('enabled'); + setToolList(agent.tools.enabled.join(', ')); + } else if (agent.tools.disabled && agent.tools.disabled.length > 0) { + setToolMode('disabled'); + setToolList(agent.tools.disabled.join(', ')); + } else { + setToolMode('all'); + } + setNoTask(agent.tools.noTask || false); + } + } else { + setError(result.error || 'Failed to load agent'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load agent'); + } finally { + setLoading(false); + } + }; + + loadAgent(); + }, [agentName, copyFrom, defaultName]); + + // 构建配置对象 + const buildConfig = (): AgentInput => { + const config: AgentInput = { + description, + mode, + }; + + if (prompt.trim()) { + config.prompt = prompt; + } + + if (maxSteps !== undefined) { + config.maxSteps = maxSteps; + } + + // 模型配置 + const model: AgentModelConfig = {}; + if (modelProvider) { + model.provider = modelProvider as 'anthropic' | 'deepseek' | 'openai'; + } + if (modelName) { + model.model = modelName; + } + if (temperature !== undefined) { + model.temperature = temperature; + } + if (maxTokens !== undefined) { + model.maxTokens = maxTokens; + } + if (Object.keys(model).length > 0) { + config.model = model; + } + + // 工具配置 + const tools: AgentToolConfig = {}; + if (toolMode === 'enabled' && toolList.trim()) { + tools.enabled = toolList.split(',').map((t) => t.trim()).filter(Boolean); + } else if (toolMode === 'disabled' && toolList.trim()) { + tools.disabled = toolList.split(',').map((t) => t.trim()).filter(Boolean); + } + if (noTask) { + tools.noTask = true; + } + if (Object.keys(tools).length > 0) { + config.tools = tools; + } + + return config; + }; + + // 保存 + const handleSave = async () => { + // 验证 + if (!name.trim()) { + setError('Agent name is required'); + return; + } + if (!description.trim()) { + setError('Description is required'); + return; + } + + setSaving(true); + setError(null); + + try { + const config = buildConfig(); + + let result; + if (isNewAgent) { + result = await createAgent(name.trim(), config); + } else { + result = await updateAgent(name.trim(), config); + } + + if (result.success) { + toast.success(isNewAgent ? `Agent "${name}" created` : `Agent "${name}" updated`); + onSave(); + } else { + setError(result.error || 'Save failed'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Save failed'); + } finally { + setSaving(false); + } + }; + + return ( + + + e.stopPropagation()} + className={cn( + 'bg-gray-800 max-h-[90vh] overflow-hidden flex flex-col', + responsive + ? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg' + : 'rounded-lg w-full max-w-2xl mx-4' + )} + > + {/* Header */} +
+ {responsive && ( +
+ )} +
+

+ + {isNewAgent ? 'Create Agent' : `Edit: ${agentName}`} +

+ {copyFrom && ( +

Copying from: {copyFrom}

+ )} +
+
+ + +
+
+ + {/* Content */} +
+ {loading ? ( +
+
+
+ ) : ( + <> + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {/* Basic Info */} +
+

Basic Information

+ + {/* Name */} +
+ + setName(e.target.value)} + disabled={!isNewAgent} + placeholder="my-agent" + className={cn( + 'w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm', + 'focus:outline-none focus:border-primary-500', + !isNewAgent && 'opacity-50 cursor-not-allowed' + )} + /> + {!isNewAgent && ( +

Name cannot be changed after creation

+ )} +
+ + {/* Description */} +
+ + setDescription(e.target.value)} + placeholder="A helpful coding assistant" + className="w-full px-3 py-2 bg-gray-900 border border-gray-700 rounded-lg text-sm focus:outline-none focus:border-primary-500" + /> +
+ + {/* Mode */} +
+ +
+ {(['primary', 'subagent', 'all'] as const).map((m) => ( + + ))} +
+

+ {mode === 'primary' + ? 'Can be used as the main agent' + : mode === 'subagent' + ? 'Can only be spawned by other agents' + : 'Can be used in both modes'} +

+
+
+ + {/* System Prompt */} + +
+