diff --git a/desktop/src/__tests__/agentsSettings.test.tsx b/desktop/src/__tests__/agentsSettings.test.tsx index a748ae5f..68d1c979 100644 --- a/desktop/src/__tests__/agentsSettings.test.tsx +++ b/desktop/src/__tests__/agentsSettings.test.tsx @@ -6,11 +6,12 @@ import { Settings } from '../pages/Settings' import { useAgentStore } from '../stores/agentStore' import { useSkillStore } from '../stores/skillStore' import { useSettingsStore } from '../stores/settingsStore' +import { useSessionStore } from '../stores/sessionStore' +import { SETTINGS_TAB_ID, useTabStore } from '../stores/tabStore' -// Mock the API module so no real HTTP calls are made vi.mock('../api/agents', () => ({ agentsApi: { - list: vi.fn().mockResolvedValue({ agents: [] }), + list: vi.fn().mockResolvedValue({ activeAgents: [], allAgents: [] }), }, })) @@ -42,26 +43,41 @@ vi.mock('../components/chat/CodeViewer', () => ({ const MOCK_AGENTS = [ { - name: 'code-reviewer', + agentType: 'code-reviewer', description: 'Reviews code for quality and security', model: 'claude-sonnet-4-6', + modelDisplay: 'claude-sonnet-4-6', tools: ['Read', 'Grep', 'Glob'], systemPrompt: '# Code Reviewer\n\nYou are an expert code reviewer.', color: 'blue', + source: 'userSettings' as const, + baseDir: '~/.claude/agents', + isActive: true, }, { - name: 'doc-writer', + agentType: 'doc-writer', description: 'Writes technical documentation', model: 'claude-haiku-4-5-20251001', + modelDisplay: 'claude-haiku-4-5-20251001', + tools: ['Read'], systemPrompt: 'You write clear and concise docs.', color: 'green', + source: 'built-in' as const, + baseDir: 'built-in', + isActive: true, }, { - name: 'plain-agent', + agentType: 'plain-agent', description: undefined, model: undefined, + modelDisplay: 'inherit', + tools: undefined, systemPrompt: undefined, color: undefined, + source: 'projectSettings' as const, + baseDir: '/workspace/project/.claude/agents', + isActive: false, + overriddenBy: 'userSettings' as const, }, ] @@ -112,12 +128,44 @@ function switchToSkillsTab() { describe('Settings > Agents tab', () => { beforeEach(() => { useSettingsStore.setState({ locale: 'en' }) + useTabStore.setState({ + activeTabId: 'session-1', + tabs: [{ sessionId: 'session-1', title: 'Test', type: 'session', status: 'idle' }], + }) + useSessionStore.setState({ + sessions: [ + { + id: 'session-1', + title: 'Test Session', + createdAt: '', + modifiedAt: '', + messageCount: 0, + projectPath: '/workspace/project', + workDir: '/workspace/project', + workDirExists: true, + }, + ], + activeSessionId: 'session-1', + isLoading: false, + error: null, + selectedProjects: [], + availableProjects: [], + fetchSessions: noopFetch, + createSession: vi.fn(), + deleteSession: vi.fn(), + renameSession: vi.fn(), + updateSessionTitle: vi.fn(), + setActiveSession: vi.fn(), + setSelectedProjects: vi.fn(), + }) useAgentStore.setState({ - agents: [], + activeAgents: [], + allAgents: [], isLoading: false, error: null, selectedAgent: null, fetchAgents: noopFetch, + selectAgent: (agent) => useAgentStore.setState({ selectedAgent: agent }), }) useSkillStore.setState({ skills: [], @@ -137,26 +185,35 @@ describe('Settings > Agents tab', () => { }) it('shows loading spinner when fetching agents', () => { - useAgentStore.setState({ isLoading: true, agents: [], fetchAgents: noopFetch }) + useAgentStore.setState({ isLoading: true, allAgents: [], activeAgents: [], fetchAgents: noopFetch }) render() switchToAgentsTab() - expect(screen.getByText('Installed Agents')).toBeInTheDocument() const spinner = document.querySelector('.animate-spin') expect(spinner).toBeInTheDocument() }) - it('shows empty state when no agents installed', () => { - useAgentStore.setState({ agents: [], isLoading: false, fetchAgents: noopFetch }) + it('uses the active session workDir even when settings tab is focused', async () => { + const fetchAgents = vi.fn() + useAgentStore.setState({ + allAgents: [], + activeAgents: [], + isLoading: false, + fetchAgents, + }) + useTabStore.setState({ + activeTabId: SETTINGS_TAB_ID, + tabs: [{ sessionId: SETTINGS_TAB_ID, title: 'Settings', type: 'settings', status: 'idle' }], + }) + render() switchToAgentsTab() - expect(screen.getByText('No agents installed yet.')).toBeInTheDocument() - expect(screen.getByText(/Create .md or .yaml files/)).toBeInTheDocument() + expect(fetchAgents).toHaveBeenCalledWith('/workspace/project') }) it('shows error state with retry button when API fails', () => { - useAgentStore.setState({ agents: [], isLoading: false, error: 'Network error', fetchAgents: noopFetch }) + useAgentStore.setState({ allAgents: [], activeAgents: [], isLoading: false, error: 'Network error', fetchAgents: noopFetch }) render() switchToAgentsTab() @@ -164,77 +221,75 @@ describe('Settings > Agents tab', () => { expect(screen.getByText('Retry')).toBeInTheDocument() }) - it('renders agent list with names and descriptions', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) + it('renders grouped agent browser with source sections', () => { + useAgentStore.setState({ + allAgents: MOCK_AGENTS, + activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), + isLoading: false, + fetchAgents: noopFetch, + }) render() switchToAgentsTab() + expect(screen.getByText('Browse installed agents')).toBeInTheDocument() + expect(screen.getByText('Agent Browser')).toBeInTheDocument() + expect(screen.getAllByText('User').length).toBeGreaterThan(0) + expect(screen.getAllByText('Built-in').length).toBeGreaterThan(0) + expect(screen.getAllByText('Project').length).toBeGreaterThan(0) expect(screen.getByText('code-reviewer')).toBeInTheDocument() - expect(screen.getByText('Reviews code for quality and security')).toBeInTheDocument() - expect(screen.getByText('doc-writer')).toBeInTheDocument() expect(screen.getByText('Writes technical documentation')).toBeInTheDocument() - expect(screen.getByText('3 agents')).toBeInTheDocument() + expect(screen.getByText('Overridden by User')).toBeInTheDocument() }) - it('shows model badge for agents with model defined', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) - render() - switchToAgentsTab() - - expect(screen.getByText('claude-sonnet-4-6')).toBeInTheDocument() - expect(screen.getByText('claude-haiku-4-5-20251001')).toBeInTheDocument() - }) - - it('shows "No description" for agents without description', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) - render() - switchToAgentsTab() - - expect(screen.getByText('No description')).toBeInTheDocument() - }) - - it('navigates to agent detail view when clicking an agent', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) + it('opens agent detail with metadata cards and document prompt', () => { + useAgentStore.setState({ + allAgents: MOCK_AGENTS, + activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), + isLoading: false, + fetchAgents: noopFetch, + }) render() switchToAgentsTab() fireEvent.click(screen.getByText('code-reviewer')) expect(screen.getByText('Back to list')).toBeInTheDocument() - expect(screen.getByText('Reviews code for quality and security')).toBeInTheDocument() - expect(screen.getByText(/claude-sonnet-4-6/)).toBeInTheDocument() - expect(screen.getByText(/Read, Grep, Glob/)).toBeInTheDocument() - }) - - it('renders system prompt as Markdown in detail view', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) - render() - switchToAgentsTab() - - fireEvent.click(screen.getByText('code-reviewer')) - + expect(screen.getByText('Agent Profile')).toBeInTheDocument() + expect(screen.getAllByText('claude-sonnet-4-6')[0]).toBeInTheDocument() + expect(screen.getByText('Read')).toBeInTheDocument() expect(screen.getByRole('heading', { name: 'Code Reviewer' })).toBeInTheDocument() - expect(screen.getByText('You are an expert code reviewer.')).toBeInTheDocument() + + const rendererRoot = screen.getByRole('heading', { name: 'Code Reviewer' }).closest('div[class*="prose"]') + expect(rendererRoot?.className).toContain('max-w-[72ch]') }) - it('shows "no system prompt" message when agent has no prompt', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) + it('shows no system prompt state when agent has no prompt', () => { + useAgentStore.setState({ + allAgents: MOCK_AGENTS, + activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), + isLoading: false, + fetchAgents: noopFetch, + }) render() switchToAgentsTab() fireEvent.click(screen.getByText('plain-agent')) expect(screen.getByText('No system prompt defined.')).toBeInTheDocument() + expect(screen.getByText('shadowed by User')).toBeInTheDocument() }) it('navigates back to list from detail view', () => { - useAgentStore.setState({ agents: MOCK_AGENTS, isLoading: false, fetchAgents: noopFetch }) + useAgentStore.setState({ + allAgents: MOCK_AGENTS, + activeAgents: MOCK_AGENTS.filter((agent) => agent.isActive), + isLoading: false, + fetchAgents: noopFetch, + }) render() switchToAgentsTab() fireEvent.click(screen.getByText('code-reviewer')) - expect(screen.getByText('Back to list')).toBeInTheDocument() - fireEvent.click(screen.getByText('Back to list')) expect(screen.getByText('code-reviewer')).toBeInTheDocument() diff --git a/desktop/src/__tests__/skillsSettings.test.tsx b/desktop/src/__tests__/skillsSettings.test.tsx index ca0a3801..1c522862 100644 --- a/desktop/src/__tests__/skillsSettings.test.tsx +++ b/desktop/src/__tests__/skillsSettings.test.tsx @@ -8,7 +8,7 @@ import { useSettingsStore } from '../stores/settingsStore' vi.mock('../api/agents', () => ({ agentsApi: { - list: vi.fn().mockResolvedValue({ agents: [] }), + list: vi.fn().mockResolvedValue({ activeAgents: [], allAgents: [] }), }, })) @@ -34,7 +34,8 @@ vi.mock('../pages/AdapterSettings', () => ({ vi.mock('../stores/agentStore', () => ({ useAgentStore: () => ({ - agents: [], + activeAgents: [], + allAgents: [], isLoading: false, error: null, selectedAgent: null, diff --git a/desktop/src/api/agents.ts b/desktop/src/api/agents.ts index b123c825..7364637a 100644 --- a/desktop/src/api/agents.ts +++ b/desktop/src/api/agents.ts @@ -1,14 +1,36 @@ import { api } from './client' +export type AgentSource = + | 'built-in' + | 'plugin' + | 'userSettings' + | 'projectSettings' + | 'localSettings' + | 'flagSettings' + | 'policySettings' + export type AgentDefinition = { - name: string + agentType: string description?: string model?: string + modelDisplay?: string tools?: string[] systemPrompt?: string color?: string + source: AgentSource + baseDir?: string + overriddenBy?: AgentSource + isActive: boolean +} + +export type AgentListResponse = { + activeAgents: AgentDefinition[] + allAgents: AgentDefinition[] } export const agentsApi = { - list: () => api.get<{ agents: AgentDefinition[] }>('/api/agents'), + list: (cwd?: string) => { + const query = cwd ? `?cwd=${encodeURIComponent(cwd)}` : '' + return api.get(`/api/agents${query}`) + }, } diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index c4b9ba13..56d2fb29 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -143,9 +143,12 @@ export const en = { // Settings > Agents 'settings.tab.agents': 'Agents', 'settings.agents.title': 'Installed Agents', - 'settings.agents.description': 'Agents discovered from ~/.claude/agents/. Use /agents in TUI to manage them.', - 'settings.agents.empty': 'No agents installed yet.', - 'settings.agents.emptyHint': 'Create .md or .yaml files in ~/.claude/agents/ to add custom agents.', + 'settings.agents.description': 'Browse agents available to Claude across built-in, project, and user sources. Use /agents in TUI to inspect active sources and overrides.', + 'settings.agents.browserTitle': 'Browse installed agents', + 'settings.agents.browserEyebrow': 'Agent Browser', + 'settings.agents.entryEyebrow': 'Agent Profile', + 'settings.agents.empty': 'No agents available yet.', + 'settings.agents.emptyHint': 'Add agent definitions in your Claude settings or project to see them here.', 'settings.agents.model': 'Model', 'settings.agents.tools': 'Tools', 'settings.agents.systemPrompt': 'System Prompt', @@ -153,6 +156,28 @@ export const en = { 'settings.agents.noSystemPrompt': 'No system prompt defined.', 'settings.agents.backToList': 'Back to list', 'settings.agents.agentCount': '{count} agents', + 'settings.agents.summary.totalAgents': 'Total agents', + 'settings.agents.summary.activeAgents': 'Active agents', + 'settings.agents.summary.sources': 'Sources', + 'settings.agents.summary.source': 'Source', + 'settings.agents.summary.model': 'Model', + 'settings.agents.summary.tools': 'Tools', + 'settings.agents.summary.status': 'Status', + 'settings.agents.groupHint': '{count} agents available from {source}', + 'settings.agents.status.active': 'Active', + 'settings.agents.status.available': 'Available', + 'settings.agents.overriddenBy': 'Overridden by {source}', + 'settings.agents.overriddenByShort': 'shadowed by {source}', + 'settings.agents.toolCount': '{count} tools', + 'settings.agents.noTools': 'No tool restriction', + 'settings.agents.promptHint': 'Read the current system prompt and compare how this agent is scoped.', + 'settings.agents.source.userSettings': 'User', + 'settings.agents.source.projectSettings': 'Project', + 'settings.agents.source.localSettings': 'Local', + 'settings.agents.source.policySettings': 'Managed', + 'settings.agents.source.plugin': 'Plugin', + 'settings.agents.source.flagSettings': 'CLI arg', + 'settings.agents.source.built-in': 'Built-in', // Settings > Skills 'settings.skills.title': 'Installed Skills', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index df697ed8..aaaf0d3f 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -145,9 +145,12 @@ export const zh: Record = { // Settings > Agents 'settings.tab.agents': 'Agents', 'settings.agents.title': '已安装的 Agents', - 'settings.agents.description': '从 ~/.claude/agents/ 发现的 Agent。可在 TUI 中使用 /agents 管理。', - 'settings.agents.empty': '暂无已安装的 Agent。', - 'settings.agents.emptyHint': '在 ~/.claude/agents/ 中创建 .md 或 .yaml 文件来添加自定义 Agent。', + 'settings.agents.description': '浏览 Claude 当前可用的 Agents,包括内置、项目和用户来源。可在 TUI 中使用 /agents 查看当前生效项与覆盖关系。', + 'settings.agents.browserTitle': '浏览已安装 Agents', + 'settings.agents.browserEyebrow': 'Agent 浏览器', + 'settings.agents.entryEyebrow': 'Agent 档案', + 'settings.agents.empty': '暂无可用 Agent。', + 'settings.agents.emptyHint': '在 Claude 设置或当前项目中添加 Agent 定义后,就会显示在这里。', 'settings.agents.model': '模型', 'settings.agents.tools': '工具', 'settings.agents.systemPrompt': '系统提示词', @@ -155,6 +158,28 @@ export const zh: Record = { 'settings.agents.noSystemPrompt': '未定义系统提示词。', 'settings.agents.backToList': '返回列表', 'settings.agents.agentCount': '{count} 个 Agent', + 'settings.agents.summary.totalAgents': 'Agent 总数', + 'settings.agents.summary.activeAgents': '生效中', + 'settings.agents.summary.sources': '来源类型', + 'settings.agents.summary.source': '来源', + 'settings.agents.summary.model': '模型', + 'settings.agents.summary.tools': '工具', + 'settings.agents.summary.status': '状态', + 'settings.agents.groupHint': '{source}中有 {count} 个 Agent 可用', + 'settings.agents.status.active': '已生效', + 'settings.agents.status.available': '可查看', + 'settings.agents.overriddenBy': '被 {source} 覆盖', + 'settings.agents.overriddenByShort': '被 {source} 覆盖', + 'settings.agents.toolCount': '{count} 个工具', + 'settings.agents.noTools': '未限制工具', + 'settings.agents.promptHint': '阅读当前 system prompt,并对比这个 Agent 的职责范围。', + 'settings.agents.source.userSettings': '用户', + 'settings.agents.source.projectSettings': '项目', + 'settings.agents.source.localSettings': '本地', + 'settings.agents.source.policySettings': '托管', + 'settings.agents.source.plugin': '插件', + 'settings.agents.source.flagSettings': 'CLI 参数', + 'settings.agents.source.built-in': '内置', // Settings > Skills 'settings.skills.title': '已安装技能', diff --git a/desktop/src/stores/agentStore.ts b/desktop/src/stores/agentStore.ts index c3dae35f..963ce120 100644 --- a/desktop/src/stores/agentStore.ts +++ b/desktop/src/stores/agentStore.ts @@ -2,26 +2,28 @@ import { create } from 'zustand' import { agentsApi, type AgentDefinition } from '../api/agents' type AgentStore = { - agents: AgentDefinition[] + activeAgents: AgentDefinition[] + allAgents: AgentDefinition[] isLoading: boolean error: string | null selectedAgent: AgentDefinition | null - fetchAgents: () => Promise + fetchAgents: (cwd?: string) => Promise selectAgent: (agent: AgentDefinition | null) => void } export const useAgentStore = create((set) => ({ - agents: [], + activeAgents: [], + allAgents: [], isLoading: false, error: null, selectedAgent: null, - fetchAgents: async () => { + fetchAgents: async (cwd) => { set({ isLoading: true, error: null }) try { - const { agents } = await agentsApi.list() - set({ agents, isLoading: false }) + const { activeAgents, allAgents } = await agentsApi.list(cwd) + set({ activeAgents, allAgents, isLoading: false }) } catch (error) { const message = error instanceof Error ? error.message : 'Failed to load agents' set({ isLoading: false, error: message }) diff --git a/desktop/tsconfig.tsbuildinfo b/desktop/tsconfig.tsbuildinfo index 4454391a..6e8ca13a 100644 --- a/desktop/tsconfig.tsbuildinfo +++ b/desktop/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/__tests__/pages.test.tsx","./src/api/adapters.ts","./src/api/clitasks.ts","./src/api/client.ts","./src/api/filesystem.ts","./src/api/models.ts","./src/api/providers.ts","./src/api/search.ts","./src/api/sessions.ts","./src/api/settings.ts","./src/api/tasks.ts","./src/api/teams.ts","./src/api/websocket.ts","./src/components/chat/askuserquestion.tsx","./src/components/chat/assistantmessage.tsx","./src/components/chat/attachmentgallery.tsx","./src/components/chat/chatinput.tsx","./src/components/chat/codeviewer.tsx","./src/components/chat/diffviewer.tsx","./src/components/chat/filesearchmenu.tsx","./src/components/chat/imagegallerymodal.tsx","./src/components/chat/inlinetasksummary.tsx","./src/components/chat/messagelist.tsx","./src/components/chat/permissiondialog.tsx","./src/components/chat/sessiontaskbar.tsx","./src/components/chat/streamingindicator.tsx","./src/components/chat/terminalchrome.tsx","./src/components/chat/thinkingblock.tsx","./src/components/chat/toolcallblock.tsx","./src/components/chat/toolcallgroup.tsx","./src/components/chat/toolresultblock.tsx","./src/components/chat/usermessage.tsx","./src/components/chat/chatblocks.test.tsx","./src/components/chat/clipboard.ts","./src/components/chat/composerutils.test.ts","./src/components/chat/composerutils.ts","./src/components/controls/modelselector.tsx","./src/components/controls/permissionmodeselector.tsx","./src/components/layout/appshell.tsx","./src/components/layout/contentrouter.tsx","./src/components/layout/projectfilter.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/statusbar.tsx","./src/components/layout/titlebar.tsx","./src/components/markdown/markdownrenderer.tsx","./src/components/shared/button.tsx","./src/components/shared/copybutton.tsx","./src/components/shared/directorypicker.tsx","./src/components/shared/dropdown.tsx","./src/components/shared/input.tsx","./src/components/shared/modal.tsx","./src/components/shared/projectcontextchip.tsx","./src/components/shared/spinner.tsx","./src/components/shared/textarea.tsx","./src/components/shared/toast.tsx","./src/components/tasks/dayofweekpicker.tsx","./src/components/tasks/newtaskmodal.tsx","./src/components/tasks/prompteditor.tsx","./src/components/tasks/taskemptystate.tsx","./src/components/tasks/tasklist.tsx","./src/components/tasks/taskrow.tsx","./src/components/tasks/taskrunspanel.tsx","./src/components/teams/backtoleader.tsx","./src/components/teams/membertag.tsx","./src/components/teams/teamstatusbar.tsx","./src/components/teams/transcriptview.tsx","./src/config/providerpresets.ts","./src/config/spinnerverbs.ts","./src/hooks/usekeyboardshortcuts.ts","./src/i18n/index.ts","./src/i18n/locales/en.ts","./src/i18n/locales/zh.ts","./src/lib/crondescribe.ts","./src/lib/desktopruntime.ts","./src/lib/parserunoutput.ts","./src/lib/__tests__/crondescribe.test.ts","./src/mocks/data.ts","./src/pages/activesession.tsx","./src/pages/adaptersettings.tsx","./src/pages/agentteams.tsx","./src/pages/agenttranscript.tsx","./src/pages/emptysession.tsx","./src/pages/newtaskmodal.tsx","./src/pages/scheduledtasks.tsx","./src/pages/scheduledtasksempty.tsx","./src/pages/scheduledtaskslist.tsx","./src/pages/sessioncontrols.tsx","./src/pages/settings.tsx","./src/pages/toolinspection.tsx","./src/stores/adapterstore.ts","./src/stores/chatstore.test.ts","./src/stores/chatstore.ts","./src/stores/clitaskstore.ts","./src/stores/providerstore.ts","./src/stores/sessionstore.ts","./src/stores/settingsstore.ts","./src/stores/taskstore.ts","./src/stores/teamstore.ts","./src/stores/uistore.ts","./src/types/adapter.ts","./src/types/chat.ts","./src/types/clitask.ts","./src/types/provider.ts","./src/types/session.ts","./src/types/settings.ts","./src/types/task.ts","./src/types/team.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/__tests__/agentssettings.test.tsx","./src/__tests__/pages.test.tsx","./src/__tests__/skillssettings.test.tsx","./src/api/adapters.ts","./src/api/agents.ts","./src/api/clitasks.ts","./src/api/client.ts","./src/api/filesystem.ts","./src/api/models.ts","./src/api/providers.ts","./src/api/search.ts","./src/api/sessions.ts","./src/api/settings.ts","./src/api/skills.ts","./src/api/tasks.ts","./src/api/teams.ts","./src/api/websocket.ts","./src/components/chat/askuserquestion.tsx","./src/components/chat/assistantmessage.tsx","./src/components/chat/attachmentgallery.tsx","./src/components/chat/chatinput.tsx","./src/components/chat/codeviewer.tsx","./src/components/chat/diffviewer.tsx","./src/components/chat/filesearchmenu.tsx","./src/components/chat/imagegallerymodal.tsx","./src/components/chat/inlinetasksummary.tsx","./src/components/chat/messageactionbar.tsx","./src/components/chat/messagelist.test.tsx","./src/components/chat/messagelist.tsx","./src/components/chat/permissiondialog.tsx","./src/components/chat/sessiontaskbar.tsx","./src/components/chat/streamingindicator.tsx","./src/components/chat/terminalchrome.tsx","./src/components/chat/thinkingblock.tsx","./src/components/chat/toolcallblock.tsx","./src/components/chat/toolcallgroup.tsx","./src/components/chat/toolresultblock.tsx","./src/components/chat/usermessage.tsx","./src/components/chat/chatblocks.test.tsx","./src/components/chat/clipboard.ts","./src/components/chat/composerutils.test.ts","./src/components/chat/composerutils.ts","./src/components/controls/modelselector.tsx","./src/components/controls/permissionmodeselector.tsx","./src/components/layout/appshell.tsx","./src/components/layout/contentrouter.tsx","./src/components/layout/projectfilter.tsx","./src/components/layout/sidebar.tsx","./src/components/layout/statusbar.tsx","./src/components/layout/tabbar.tsx","./src/components/layout/titlebar.tsx","./src/components/markdown/markdownrenderer.test.tsx","./src/components/markdown/markdownrenderer.tsx","./src/components/shared/button.tsx","./src/components/shared/copybutton.tsx","./src/components/shared/directorypicker.tsx","./src/components/shared/dropdown.tsx","./src/components/shared/input.tsx","./src/components/shared/modal.tsx","./src/components/shared/projectcontextchip.tsx","./src/components/shared/spinner.tsx","./src/components/shared/textarea.tsx","./src/components/shared/toast.tsx","./src/components/skills/skilldetail.test.tsx","./src/components/skills/skilldetail.tsx","./src/components/skills/skilllist.tsx","./src/components/tasks/dayofweekpicker.tsx","./src/components/tasks/newtaskmodal.tsx","./src/components/tasks/prompteditor.tsx","./src/components/tasks/taskemptystate.tsx","./src/components/tasks/tasklist.tsx","./src/components/tasks/taskrow.tsx","./src/components/tasks/taskrunspanel.tsx","./src/components/teams/backtoleader.tsx","./src/components/teams/membertag.tsx","./src/components/teams/teamstatusbar.tsx","./src/components/teams/transcriptview.tsx","./src/config/providerpresets.ts","./src/config/spinnerverbs.ts","./src/hooks/usekeyboardshortcuts.ts","./src/i18n/index.ts","./src/i18n/locales/en.ts","./src/i18n/locales/zh.ts","./src/lib/crondescribe.ts","./src/lib/desktopruntime.ts","./src/lib/parserunoutput.ts","./src/lib/__tests__/crondescribe.test.ts","./src/mocks/data.ts","./src/pages/activesession.tsx","./src/pages/adaptersettings.tsx","./src/pages/agentteams.tsx","./src/pages/agenttranscript.tsx","./src/pages/emptysession.tsx","./src/pages/newtaskmodal.tsx","./src/pages/scheduledtasks.tsx","./src/pages/scheduledtasksempty.tsx","./src/pages/scheduledtaskslist.tsx","./src/pages/sessioncontrols.tsx","./src/pages/settings.tsx","./src/pages/toolinspection.tsx","./src/stores/adapterstore.ts","./src/stores/agentstore.ts","./src/stores/chatstore.test.ts","./src/stores/chatstore.ts","./src/stores/clitaskstore.ts","./src/stores/providerstore.ts","./src/stores/sessionstore.ts","./src/stores/settingsstore.ts","./src/stores/skillstore.ts","./src/stores/tabstore.ts","./src/stores/taskstore.ts","./src/stores/teamstore.ts","./src/stores/uistore.ts","./src/types/adapter.ts","./src/types/chat.ts","./src/types/clitask.ts","./src/types/provider.ts","./src/types/session.ts","./src/types/settings.ts","./src/types/skill.ts","./src/types/task.ts","./src/types/team.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/src/server/__tests__/e2e/business-flow.test.ts b/src/server/__tests__/e2e/business-flow.test.ts index 7192c9cf..9fe98196 100644 --- a/src/server/__tests__/e2e/business-flow.test.ts +++ b/src/server/__tests__/e2e/business-flow.test.ts @@ -211,9 +211,12 @@ describe('Business Flow: Agent Management', () => { await fs.rm(tmpDir, { recursive: true, force: true }) }) - it('should start with no agents', async () => { + it('should start with shared active/all agent payload', async () => { const { data } = await api('GET', '/api/agents') - expect(data.agents).toEqual([]) + expect(Array.isArray(data.activeAgents)).toBe(true) + expect(Array.isArray(data.allAgents)).toBe(true) + expect(data.activeAgents.length).toBeGreaterThan(0) + expect(data.activeAgents.some((agent: any) => agent.source === 'built-in')).toBe(true) }) it('should create a new agent with full config', async () => { @@ -238,12 +241,17 @@ describe('Business Flow: Agent Management', () => { expect(status).toBe(201) }) - it('should list both agents', async () => { + it('should list both created agents in CRUD detail endpoint while shared list stays source-based', async () => { const { data } = await api('GET', '/api/agents') - expect(data.agents.length).toBe(2) - const names = data.agents.map((a: any) => a.name) - expect(names).toContain('security-auditor') - expect(names).toContain('test-writer') + expect(data.activeAgents.length).toBeGreaterThan(0) + expect(data.activeAgents.some((agent: any) => agent.source === 'built-in')).toBe(true) + expect(data.activeAgents.some((agent: any) => agent.agentType === 'security-auditor')).toBe(false) + expect(data.activeAgents.some((agent: any) => agent.agentType === 'test-writer')).toBe(false) + + const securityAuditor = await api('GET', '/api/agents/security-auditor') + const testWriter = await api('GET', '/api/agents/test-writer') + expect(securityAuditor.data.agent.name).toBe('security-auditor') + expect(testWriter.data.agent.name).toBe('test-writer') }) it('should get agent details', async () => { @@ -279,12 +287,16 @@ describe('Business Flow: Agent Management', () => { expect(status).toBe(404) }) - it('should delete an agent', async () => { + it('should keep deleted agent out of shared active list while built-ins remain', async () => { const { status } = await api('DELETE', '/api/agents/test-writer') expect([200, 204]).toContain(status) const { data } = await api('GET', '/api/agents') - expect(data.agents.length).toBe(1) + expect(data.activeAgents.some((agent: any) => agent.agentType === 'test-writer')).toBe(false) + expect(data.activeAgents.some((agent: any) => agent.source === 'built-in')).toBe(true) + + const deleted = await api('GET', '/api/agents/test-writer') + expect(deleted.status).toBe(404) }) it('should persist agent to YAML file on disk', async () => { diff --git a/src/server/__tests__/e2e/full-flow.test.ts b/src/server/__tests__/e2e/full-flow.test.ts index 8335c842..2d00ed49 100644 --- a/src/server/__tests__/e2e/full-flow.test.ts +++ b/src/server/__tests__/e2e/full-flow.test.ts @@ -244,9 +244,12 @@ describe('E2E: Full Flow', () => { // 7. Agents // ============================================= - it('should start with empty agent list', async () => { + it('should start with shared active/all agent payload', async () => { const { data } = await api('GET', '/api/agents') - expect(data.agents).toEqual([]) + expect(Array.isArray(data.activeAgents)).toBe(true) + expect(Array.isArray(data.allAgents)).toBe(true) + expect(data.activeAgents.length).toBeGreaterThan(0) + expect(data.activeAgents.some((agent: any) => agent.source === 'built-in')).toBe(true) }) it('should create an agent', async () => { @@ -258,10 +261,13 @@ describe('E2E: Full Flow', () => { expect(status).toBe(201) }) - it('should list the created agent', async () => { + it('should expose shared active/all agent payload independent of CRUD storage', async () => { const { data } = await api('GET', '/api/agents') - expect(data.agents.length).toBe(1) - expect(data.agents[0].name).toBe('test-agent') + expect(Array.isArray(data.activeAgents)).toBe(true) + expect(Array.isArray(data.allAgents)).toBe(true) + expect(data.activeAgents.length).toBeGreaterThan(0) + expect(data.activeAgents.some((agent: any) => agent.source === 'built-in')).toBe(true) + expect(data.activeAgents.some((agent: any) => agent.agentType === 'test-agent')).toBe(false) }) it('should delete an agent', async () => { diff --git a/src/server/__tests__/real-llm-test.ts b/src/server/__tests__/real-llm-test.ts index 0a8391f3..35225380 100644 --- a/src/server/__tests__/real-llm-test.ts +++ b/src/server/__tests__/real-llm-test.ts @@ -411,7 +411,7 @@ async function testAgentsApi() { const res = await fetch(`${BASE_URL}/api/agents`) if (res.status !== 200) throw new Error(`Agents failed: ${res.status}`) const body = await res.json() - console.log(` Agents: ${body.agents?.length ?? 0}`) + console.log(` Active agents: ${body.activeAgents?.length ?? body.agents?.length ?? 0}`) console.log('✅ Agents API works') } diff --git a/src/server/api/agents.ts b/src/server/api/agents.ts index fc8142fe..ca95bd8f 100644 --- a/src/server/api/agents.ts +++ b/src/server/api/agents.ts @@ -14,12 +14,23 @@ import { AgentService } from '../services/agentService.js' import { taskService } from '../services/taskService.js' import { ApiError, errorResponse } from '../middleware/errorHandler.js' +import { + resolveAgentModelDisplay, + resolveAgentOverrides, + type ResolvedAgent, +} from '../../tools/AgentTool/agentDisplay.js' +import { + clearAgentDefinitionsCache, + getAgentDefinitionsWithOverrides, + type AgentDefinition as SharedAgentDefinition, +} from '../../tools/AgentTool/loadAgentsDir.js' +import { getCwd } from '../../utils/cwd.js' const agentService = new AgentService() export async function handleAgentsApi( req: Request, - _url: URL, + url: URL, segments: string[], ): Promise { try { @@ -29,7 +40,7 @@ export async function handleAgentsApi( return await handleTasksApi(req, segments) } - return await handleAgents(req, segments) + return await handleAgents(req, url, segments) } catch (error) { return errorResponse(error) } @@ -39,6 +50,7 @@ export async function handleAgentsApi( async function handleAgents( req: Request, + url: URL, segments: string[], ): Promise { const method = req.method @@ -46,8 +58,14 @@ async function handleAgents( // ── GET /api/agents ────────────────────────────────────────────────── if (method === 'GET' && !agentName) { - const agents = await agentService.listAgents() - return Response.json({ agents }) + const cwd = url.searchParams.get('cwd') || getCwd() + const { activeAgents, allAgents } = await getAgentDefinitionsWithOverrides(cwd) + const resolvedAgents = resolveAgentOverrides(allAgents, activeAgents) + + return Response.json({ + activeAgents: activeAgents.map(agent => serializeActiveAgent(agent, true)), + allAgents: resolvedAgents.map(serializeResolvedAgent), + }) } // ── GET /api/agents/:name ──────────────────────────────────────────── @@ -73,6 +91,7 @@ async function handleAgents( systemPrompt: body.systemPrompt as string | undefined, color: body.color as string | undefined, }) + clearAgentDefinitionsCache() return Response.json({ ok: true }, { status: 201 }) } @@ -80,6 +99,7 @@ async function handleAgents( if (method === 'PUT' && agentName) { const body = await parseJsonBody(req) await agentService.updateAgent(agentName, body as Record) + clearAgentDefinitionsCache() const updated = await agentService.getAgent(agentName) return Response.json({ agent: updated }) } @@ -87,6 +107,7 @@ async function handleAgents( // ── DELETE /api/agents/:name ───────────────────────────────────────── if (method === 'DELETE' && agentName) { await agentService.deleteAgent(agentName) + clearAgentDefinitionsCache() return Response.json({ ok: true }) } @@ -155,3 +176,45 @@ async function parseJsonBody(req: Request): Promise> { throw ApiError.badRequest('Invalid JSON body') } } + +type ApiAgentDefinition = { + agentType: string + description?: string + model?: string + modelDisplay?: string + tools?: string[] + systemPrompt?: string + color?: string + source: SharedAgentDefinition['source'] + baseDir?: string + isActive: boolean +} + +type ApiResolvedAgentDefinition = ApiAgentDefinition & { + overriddenBy?: SharedAgentDefinition['source'] +} + +function serializeActiveAgent( + agent: SharedAgentDefinition, + isActive: boolean, +): ApiAgentDefinition { + return { + agentType: agent.agentType, + description: agent.whenToUse, + model: agent.model, + modelDisplay: resolveAgentModelDisplay(agent), + tools: agent.tools, + systemPrompt: agent.getSystemPrompt.length === 0 ? agent.getSystemPrompt() : undefined, + color: agent.color, + source: agent.source, + baseDir: agent.baseDir, + isActive, + } +} + +function serializeResolvedAgent(agent: ResolvedAgent): ApiResolvedAgentDefinition { + return { + ...serializeActiveAgent(agent, !agent.overriddenBy), + overriddenBy: agent.overriddenBy, + } +}