mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
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:
parent
db005e5e98
commit
211226c7a1
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}`)
|
||||
},
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': '已安装技能',
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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"}
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user