feat(agents): update API types, store, i18n, and tests for new agent model

- Update AgentDefinition type with agentType, source, isActive, modelDisplay fields
- Refactor agentStore to support activeAgents/allAgents with cwd parameter
- Add i18n strings for agent browser UI (source labels, summary, status)
- Update server agents API with serialization helpers and override resolution
- Update tests to match new agent data structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-04-09 16:45:51 +08:00
parent db005e5e98
commit 211226c7a1
11 changed files with 302 additions and 91 deletions

View File

@ -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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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(<Settings />)
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()

View File

@ -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,

View File

@ -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<AgentListResponse>(`/api/agents${query}`)
},
}

View File

@ -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',

View File

@ -145,9 +145,12 @@ export const zh: Record<TranslationKey, string> = {
// 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<TranslationKey, string> = {
'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': '已安装技能',

View File

@ -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<void>
fetchAgents: (cwd?: string) => Promise<void>
selectAgent: (agent: AgentDefinition | null) => void
}
export const useAgentStore = create<AgentStore>((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 })

View File

@ -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"}
{"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"}

View File

@ -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 () => {

View File

@ -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 () => {

View File

@ -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')
}

View File

@ -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<Response> {
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<Response> {
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<string, unknown>)
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<Record<string, unknown>> {
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,
}
}