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 && (
+
+ )}
+
+ {/* Execution Limits */}
+
+
Execution Limits
+
+
Default Max Steps
+
+ 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 */}
+
+ Provider
+ setModelProvider(e.target.value)}
+ 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"
+ >
+ Default
+ Anthropic
+ OpenAI
+ DeepSeek
+
+
+
+ {/* Model */}
+
+ 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 */}
+
+ 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 */}
+
+ 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 */}
+
+
+ Cancel
+
+
+
+ {saving ? 'Saving...' : 'Save'}
+
+
+
+
+
+ );
+}
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 (
+
+
setIsOpen(!isOpen)}
+ >
+ {title}
+ {isOpen ? : }
+
+
+ {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}
+ )}
+
+
+
+
+ {saving ? 'Saving...' : 'Save'}
+
+
+
+
+
+
+
+ {/* Content */}
+
+ {loading ? (
+
+ ) : (
+ <>
+ {/* Error */}
+ {error && (
+
+ )}
+
+ {/* Basic Info */}
+
+
Basic Information
+
+ {/* Name */}
+
+
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 */}
+
+ 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 */}
+
+
Mode *
+
+ {(['primary', 'subagent', 'all'] as const).map((m) => (
+ setMode(m)}
+ className={cn(
+ 'px-3 py-2 rounded-lg text-sm transition-colors',
+ mode === m
+ ? 'bg-primary-500 text-white'
+ : 'bg-gray-900 text-gray-400 hover:bg-gray-800'
+ )}
+ >
+ {m === 'primary' ? 'Primary' : m === 'subagent' ? 'Subagent' : 'Both'}
+
+ ))}
+
+
+ {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 */}
+
+
+
+
+ {/* Model Configuration */}
+
+
+ {/* Provider */}
+
+ Provider
+ setModelProvider(e.target.value)}
+ 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"
+ >
+ Default
+ Anthropic
+ OpenAI
+ DeepSeek
+
+
+
+ {/* Model */}
+
+ 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 */}
+
+ 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 */}
+
+ 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"
+ />
+
+
+
+
+ {/* Tool Configuration */}
+
+
+ {/* Tool Mode */}
+
+
Tool Access
+
+ {(['all', 'enabled', 'disabled'] as const).map((m) => (
+ setToolMode(m)}
+ className={cn(
+ 'px-3 py-2 rounded-lg text-sm transition-colors',
+ toolMode === m
+ ? 'bg-primary-500 text-white'
+ : 'bg-gray-900 text-gray-400 hover:bg-gray-800'
+ )}
+ >
+ {m === 'all' ? 'All Tools' : m === 'enabled' ? 'Only These' : 'Except These'}
+
+ ))}
+
+
+
+ {/* Tool List */}
+ {toolMode !== 'all' && (
+
+
+ {toolMode === 'enabled' ? 'Enabled Tools' : 'Disabled Tools'}
+
+
setToolList(e.target.value)}
+ placeholder="bash, read_file, write_file"
+ 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"
+ />
+
Comma-separated tool names
+
+ )}
+
+ {/* No Task */}
+
+ setNoTask(e.target.checked)}
+ className="w-4 h-4 rounded border-gray-600 bg-gray-900 text-primary-500 focus:ring-primary-500"
+ />
+
+ Disable nested Task calls
+
+
+
+
+
+ {/* Execution Limits */}
+
+
+
Max Steps
+
+ 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. Leave empty for default.
+
+
+
+ >
+ )}
+
+
+ {/* Footer */}
+
+
+ Cancel
+
+
+
+ {saving ? 'Saving...' : 'Save'}
+
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/components/AgentsPanel.tsx b/packages/ui/src/components/AgentsPanel.tsx
new file mode 100644
index 0000000..5e3cfc2
--- /dev/null
+++ b/packages/ui/src/components/AgentsPanel.tsx
@@ -0,0 +1,604 @@
+/**
+ * AgentsPanel Component
+ *
+ * Agent 预设管理面板:列出所有 Agent、查看详情、创建/编辑/删除
+ */
+
+import { useState, useEffect, useCallback } from 'react';
+import {
+ X,
+ RefreshCw,
+ Bot,
+ Plus,
+ Settings,
+ ChevronDown,
+ ChevronRight,
+ Eye,
+ Copy,
+ Trash2,
+ Edit3,
+ Sparkles,
+ Cpu,
+ Layers,
+} 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 { Skeleton } from './Skeleton';
+import { AgentEditor } from './AgentEditor';
+import { AgentDefaultsEditor } from './AgentDefaultsEditor';
+import {
+ listAgents,
+ getAgent,
+ deleteAgent,
+ type AgentListItem,
+ type AgentDetail,
+} from '../api/client.js';
+
+interface AgentsPanelProps {
+ onClose: () => void;
+ /** 是否启用响应式布局 */
+ responsive?: boolean;
+}
+
+// 模式颜色映射
+function getModeColor(mode: AgentListItem['mode']) {
+ switch (mode) {
+ case 'primary':
+ return 'bg-blue-500/20 text-blue-400';
+ case 'subagent':
+ return 'bg-purple-500/20 text-purple-400';
+ case 'all':
+ return 'bg-green-500/20 text-green-400';
+ default:
+ return 'bg-gray-500/20 text-gray-400';
+ }
+}
+
+// 模式文字
+function getModeText(mode: AgentListItem['mode']) {
+ switch (mode) {
+ case 'primary':
+ return 'Primary';
+ case 'subagent':
+ return 'Subagent';
+ case 'all':
+ return 'Both';
+ default:
+ return mode;
+ }
+}
+
+export function AgentsPanel({ onClose, responsive = false }: AgentsPanelProps) {
+ // 数据状态
+ const [agents, setAgents] = useState([]);
+ const [expandedAgents, setExpandedAgents] = useState>(new Set());
+ const [agentDetails, setAgentDetails] = useState>({});
+
+ // UI 状态
+ const [loading, setLoading] = useState(true);
+ const [refreshing, setRefreshing] = useState(false);
+ const [actionLoading, setActionLoading] = useState(null);
+
+ // 编辑器状态
+ const [editingAgent, setEditingAgent] = useState<{ name: string; isNew: boolean } | null>(null);
+ const [showDefaultsEditor, setShowDefaultsEditor] = useState(false);
+
+ // 加载 Agent 列表
+ const loadAgents = useCallback(async (showToast = false) => {
+ try {
+ const result = await listAgents();
+ if (result.success) {
+ setAgents(result.data);
+ if (showToast) {
+ toast.success('Agents refreshed');
+ }
+ } else {
+ toast.error(result.error || 'Failed to load agents');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Failed to load agents');
+ }
+ }, []);
+
+ // 初始加载
+ useEffect(() => {
+ setLoading(true);
+ loadAgents().finally(() => setLoading(false));
+ }, [loadAgents]);
+
+ // 刷新
+ const handleRefresh = async () => {
+ setRefreshing(true);
+ await loadAgents(true);
+ setRefreshing(false);
+ };
+
+ // 加载 Agent 详情
+ const loadAgentDetail = async (name: string) => {
+ if (agentDetails[name]) return agentDetails[name];
+
+ try {
+ const result = await getAgent(name);
+ if (result.success && result.data) {
+ setAgentDetails((prev) => ({ ...prev, [name]: result.data! }));
+ return result.data;
+ }
+ } catch (err) {
+ toast.error(`Failed to load agent details: ${err instanceof Error ? err.message : 'Unknown error'}`);
+ }
+ return null;
+ };
+
+ // 切换展开
+ const toggleExpanded = async (name: string) => {
+ const newExpanded = new Set(expandedAgents);
+ if (newExpanded.has(name)) {
+ newExpanded.delete(name);
+ } else {
+ newExpanded.add(name);
+ // 懒加载详情
+ await loadAgentDetail(name);
+ }
+ setExpandedAgents(newExpanded);
+ };
+
+ // 删除 Agent
+ const handleDelete = async (name: string) => {
+ if (!confirm(`Are you sure you want to delete agent "${name}"?`)) {
+ return;
+ }
+
+ setActionLoading(name);
+ try {
+ const result = await deleteAgent(name);
+ if (result.success) {
+ toast.success(`Agent "${name}" deleted`);
+ await loadAgents();
+ // 清除详情缓存
+ setAgentDetails((prev) => {
+ const newDetails = { ...prev };
+ delete newDetails[name];
+ return newDetails;
+ });
+ } else {
+ toast.error(result.error || 'Delete failed');
+ }
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : 'Delete failed');
+ } finally {
+ setActionLoading(null);
+ }
+ };
+
+ // 复制 Agent(打开编辑器,以新名称创建)
+ const handleCopy = async (name: string) => {
+ const detail = await loadAgentDetail(name);
+ if (detail) {
+ setEditingAgent({ name: `${name}-copy`, isNew: true });
+ }
+ };
+
+ // 编辑器保存成功回调
+ const handleEditorSave = () => {
+ setEditingAgent(null);
+ loadAgents();
+ // 清除所有详情缓存以获取最新数据
+ setAgentDetails({});
+ };
+
+ // 统计
+ const presetAgents = agents.filter((a) => a.isPreset);
+ const customAgents = agents.filter((a) => !a.isPreset);
+
+ // Loading 骨架屏
+ const LoadingSkeleton = () => (
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+ );
+
+ // Agent 项组件
+ const AgentItem = ({ agent }: { agent: AgentListItem }) => {
+ const isExpanded = expandedAgents.has(agent.name);
+ const isLoading = actionLoading === agent.name;
+ const detail = agentDetails[agent.name];
+
+ return (
+
+ {/* Agent Header */}
+ toggleExpanded(agent.name)}
+ >
+ {/* Expand Icon */}
+
+ {isExpanded ? : }
+
+
+ {/* Icon */}
+ {agent.isPreset ? (
+
+ ) : (
+
+ )}
+
+ {/* Info */}
+
+
+ {agent.name}
+
+ {getModeText(agent.mode)}
+
+ {agent.isCustomized && (
+
+ Customized
+
+ )}
+
+
{agent.description}
+
+
+ {/* Actions */}
+
e.stopPropagation()}>
+ {isLoading ? (
+
+ ) : (
+ <>
+ {/* View/Edit */}
+ {agent.isPreset && !agent.isCustomized ? (
+
toggleExpanded(agent.name)}
+ className="text-gray-400 hover:text-gray-300"
+ title="View"
+ >
+
+
+ ) : (
+
setEditingAgent({ name: agent.name, isNew: false })}
+ className="text-blue-400 hover:text-blue-300"
+ title="Edit"
+ >
+
+
+ )}
+
+ {/* Copy */}
+
handleCopy(agent.name)}
+ className="text-gray-400 hover:text-gray-300"
+ title="Copy"
+ >
+
+
+
+ {/* Delete (only for custom agents) */}
+ {!agent.isPreset && (
+
handleDelete(agent.name)}
+ className="text-red-400 hover:text-red-300"
+ title="Delete"
+ >
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Expanded Content */}
+
+ {isExpanded && (
+
+
+ {detail ? (
+ <>
+ {/* Model Info */}
+ {detail.model && (
+
+
+
+ Model: {' '}
+
+ {detail.model.provider && `${detail.model.provider}/`}
+ {detail.model.model || 'default'}
+
+ {detail.model.temperature !== undefined && (
+
+ temp: {detail.model.temperature}
+
+ )}
+
+
+ )}
+
+ {/* Tools Config */}
+ {detail.tools && (
+
+
+
+ Tools: {' '}
+ {detail.tools.enabled ? (
+
+ Only: {detail.tools.enabled.join(', ')}
+
+ ) : detail.tools.disabled ? (
+
+ Disabled: {detail.tools.disabled.join(', ')}
+
+ ) : (
+ All enabled
+ )}
+ {detail.tools.noTask && (
+ (No nested tasks)
+ )}
+
+
+ )}
+
+ {/* Max Steps */}
+ {detail.maxSteps && (
+
+ Max Steps: {' '}
+ {detail.maxSteps}
+
+ )}
+
+ {/* Prompt Preview */}
+ {detail.prompt && (
+
+
System Prompt:
+
+ {detail.prompt.slice(0, 500)}
+ {detail.prompt.length > 500 && '...'}
+
+
+ )}
+ >
+ ) : (
+
+ )}
+
+
+ )}
+
+
+ );
+ };
+
+ // 如果正在编辑 Agent
+ if (editingAgent) {
+ return (
+ setEditingAgent(null)}
+ onSave={handleEditorSave}
+ responsive={responsive}
+ />
+ );
+ }
+
+ // 如果正在编辑全局默认配置
+ if (showDefaultsEditor) {
+ return (
+ setShowDefaultsEditor(false)}
+ onSave={() => {
+ setShowDefaultsEditor(false);
+ loadAgents();
+ }}
+ responsive={responsive}
+ />
+ );
+ }
+
+ 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 && (
+
+ )}
+
+
+
+ Agent Presets
+
+
+ {agents.length} agents ({presetAgents.length} preset, {customAgents.length} custom)
+
+
+
+
setShowDefaultsEditor(true)}
+ title="Global Defaults"
+ className={cn(responsive && 'min-w-[44px] min-h-[44px]')}
+ >
+
+
+
setEditingAgent({ name: '', isNew: true })}
+ title="New Agent"
+ className={cn(responsive && 'min-w-[44px] min-h-[44px]')}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {/* Agent List */}
+
+ {loading ? (
+
+ ) : agents.length === 0 ? (
+
+
+
No agents available
+
setEditingAgent({ name: '', isNew: true })}
+ >
+
+ Create Agent
+
+
+ ) : (
+
+ {/* Preset Agents */}
+ {presetAgents.length > 0 && (
+
+
+
+ Preset Agents
+
+
+ {presetAgents.map((agent) => (
+
+ ))}
+
+
+ )}
+
+ {/* Custom Agents */}
+
+
+
+ Custom Agents
+
+ {customAgents.length > 0 ? (
+
+ {customAgents.map((agent) => (
+
+ ))}
+
+ ) : (
+
+
No custom agents yet
+
setEditingAgent({ name: '', isNew: true })}
+ >
+
+ Create one
+
+
+ )}
+
+
+ )}
+
+
+ {/* Footer Info */}
+
+ Config stored in{' '}
+ .ai-assist/agents.json
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 5e2d6f8..e9eac6f 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -54,6 +54,15 @@ export {
getSessionCompletedHooks,
updateSessionCompletedHooks,
testHookCommand,
+ // Agents API
+ listAgents,
+ getAgent,
+ createAgent,
+ updateAgent,
+ deleteAgent,
+ listPresetAgents,
+ getAgentDefaults,
+ updateAgentDefaults,
} from './api/client.js';
// Types
@@ -87,6 +96,19 @@ export type {
FileHookConfig,
ShellCommandConfig,
HookTestResult,
+ // Agent types
+ AgentMode,
+ AgentModelConfig,
+ AgentToolConfig,
+ PermissionRule,
+ AgentBashPermission,
+ AgentFilePermission,
+ AgentGitPermission,
+ AgentPermission,
+ AgentListItem,
+ AgentDetail,
+ AgentInput,
+ AgentDefaults,
} from './api/client.js';
// Primitives (shadcn/ui style)
@@ -105,6 +127,9 @@ export { CommandEditor } from './components/CommandEditor.js';
export { MCPPanel } from './components/MCPPanel.js';
export { HooksPanel } from './components/HooksPanel.js';
export { HookEditor } from './components/HookEditor.js';
+export { AgentsPanel } from './components/AgentsPanel.js';
+export { AgentEditor } from './components/AgentEditor.js';
+export { AgentDefaultsEditor } from './components/AgentDefaultsEditor.js';
export { Sidebar } from './components/Sidebar.js';
export { FileBrowser } from './components/FileBrowser.js';
export { ConfigPanel } from './components/ConfigPanel.js';
diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx
index 8bc9364..d07b1c7 100644
--- a/packages/web/src/App.tsx
+++ b/packages/web/src/App.tsx
@@ -12,6 +12,7 @@ import {
CommandPanel,
MCPPanel,
HooksPanel,
+ AgentsPanel,
Toaster,
listSessions,
createSession,
@@ -27,6 +28,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);
// 初始化:加载或创建会话
@@ -114,6 +116,7 @@ export function App() {
onOpenCommands={() => setShowCommands(true)}
onOpenMCP={() => setShowMCP(true)}
onOpenHooks={() => setShowHooks(true)}
+ onOpenAgents={() => setShowAgents(true)}
/>
) : (
@@ -171,6 +174,9 @@ export function App() {
{/* Hooks 面板 */}
{showHooks &&
setShowHooks(false)} responsive />}
+ {/* Agents 面板 */}
+ {showAgents && setShowAgents(false)} responsive />}
+
{/* 移动端底部文件按钮 */}
setShowFileBrowser(true)}
diff --git a/packages/web/src/pages/Chat.tsx b/packages/web/src/pages/Chat.tsx
index 9b9a024..0fc2565 100644
--- a/packages/web/src/pages/Chat.tsx
+++ b/packages/web/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,
@@ -25,6 +25,7 @@ interface ChatPageProps {
onOpenCommands?: () => void;
onOpenMCP?: () => void;
onOpenHooks?: () => void;
+ onOpenAgents?: () => void;
}
export function ChatPage({
@@ -38,6 +39,7 @@ export function ChatPage({
onOpenCommands,
onOpenMCP,
onOpenHooks,
+ onOpenAgents,
}: ChatPageProps) {
const {
messages,
@@ -132,8 +134,21 @@ export function ChatPage({
{/* 工具栏按钮 */}
- {(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks) && (
+ {(onOpenConfig || onToggleFileBrowser || onOpenCommands || onOpenMCP || onOpenHooks || onOpenAgents) && (
+ {/* Agents 按钮 */}
+ {onOpenAgents && (
+
+
+
+ )}
+
{/* Hooks 按钮 */}
{onOpenHooks && (