Compare commits

...

2 Commits

Author SHA1 Message Date
suyao
3da8b63673 Merge remote-tracking branch 'origin/main' into feat/sub_agents 2025-11-21 14:57:03 +08:00
suyao
1175823ab8 feat: add support for sub-agents in agent and session management
- Added 'sub_agents' field to the BaseService, agents schema, and sessions schema.
- Implemented methods in AgentService and SessionService to handle sub-agent configurations.
- Updated ClaudeCodeService to load and manage sub-agents.
- Enhanced UI components to display and select sub-agents in the agent settings and activity directory.
- Added translations for sub-agent related UI elements in multiple languages.
- Created SubAgentsSettings component for managing sub-agent associations in agent settings.
2025-11-19 15:58:15 +08:00
29 changed files with 703 additions and 24 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE `agents` ADD `sub_agents` text;--> statement-breakpoint
ALTER TABLE `sessions` ADD `sub_agents` text;

View File

@@ -0,0 +1,360 @@
{
"version": "6",
"dialect": "sqlite",
"id": "9aeb5f21-fed7-4dbf-973d-c344681b71c2",
"prevId": "0cf3d79e-69bf-4dba-8df4-996b9b67d2e8",
"tables": {
"agents": {
"name": "agents",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"accessible_paths": {
"name": "accessible_paths",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"instructions": {
"name": "instructions",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"plan_model": {
"name": "plan_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"small_model": {
"name": "small_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"mcps": {
"name": "mcps",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowed_tools": {
"name": "allowed_tools",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sub_agents": {
"name": "sub_agents",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"configuration": {
"name": "configuration",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session_messages": {
"name": "session_messages",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"session_id": {
"name": "session_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"role": {
"name": "role",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agent_session_id": {
"name": "agent_session_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"metadata": {
"name": "metadata",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"migrations": {
"name": "migrations",
"columns": {
"version": {
"name": "version",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"tag": {
"name": "tag",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"executed_at": {
"name": "executed_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"agent_type": {
"name": "agent_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agent_id": {
"name": "agent_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"accessible_paths": {
"name": "accessible_paths",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"instructions": {
"name": "instructions",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"plan_model": {
"name": "plan_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"small_model": {
"name": "small_model",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"mcps": {
"name": "mcps",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowed_tools": {
"name": "allowed_tools",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sub_agents": {
"name": "sub_agents",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"slash_commands": {
"name": "slash_commands",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"configuration": {
"name": "configuration",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1762526423527,
"tag": "0002_wealthy_naoko",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1763500397620,
"tag": "0003_smooth_talkback",
"breakpoints": true
}
]
}

View File

@@ -849,7 +849,7 @@ class FileStorage {
const resolvedPath = path.resolve(dirPath)
const stat = await fs.promises.stat(resolvedPath).catch((error) => {
logger.error(`[IPC - Error] Failed to access directory: ${resolvedPath}`, error as Error)
logger.error(`Failed to access directory: ${resolvedPath}`, error as Error)
throw error
})

View File

@@ -42,6 +42,7 @@ export abstract class BaseService {
'configuration',
'accessible_paths',
'allowed_tools',
'sub_agents',
'slash_commands'
]

View File

@@ -19,6 +19,7 @@ export const agentsTable = sqliteTable('agents', {
mcps: text('mcps'), // JSON array of MCP tool IDs
allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist)
sub_agents: text('sub_agents'), // JSON array of sub-agent IDs
configuration: text('configuration'), // JSON, extensible settings

View File

@@ -22,6 +22,7 @@ export const sessionsTable = sqliteTable('sessions', {
mcps: text('mcps'), // JSON array of MCP tool IDs
allowed_tools: text('allowed_tools'), // JSON array of allowed tool IDs (whitelist)
sub_agents: text('sub_agents'), // JSON array of sub-agent IDs
slash_commands: text('slash_commands'), // JSON array of slash command objects from SDK init
configuration: text('configuration'), // JSON, extensible settings

View File

@@ -117,6 +117,19 @@ export class AgentService extends BaseService {
return agent
}
async getAgentConfigForSDK(id: string): Promise<AgentEntity | null> {
this.ensureInitialized()
const result = await this.database.select().from(agentsTable).where(eq(agentsTable.id, id)).limit(1)
if (!result[0]) {
return null
}
const agent = this.deserializeJsonFields(result[0]) as AgentEntity
return agent
}
async listAgents(options: ListOptions = {}): Promise<{ agents: AgentEntity[]; total: number }> {
this.ensureInitialized() // Build query with pagination

View File

@@ -130,6 +130,7 @@ export class SessionService extends BaseService {
small_model: serializedData.small_model || null,
mcps: serializedData.mcps || null,
allowed_tools: serializedData.allowed_tools || null,
sub_agents: serializedData.sub_agents || null,
configuration: serializedData.configuration || null,
created_at: now,
updated_at: now
@@ -169,6 +170,22 @@ export class SessionService extends BaseService {
session.slash_commands = await this.listSlashCommands(session.agent_type, agentId)
}
// Load installed plugins from cache file
const workdir = session.accessible_paths?.[0]
if (workdir) {
try {
session.plugins = await pluginService.listInstalledFromCache(workdir)
} catch (error) {
logger.warn(`Failed to load installed plugins for session ${id}`, {
workdir,
error: error instanceof Error ? error.message : String(error)
})
session.plugins = []
}
} else {
session.plugins = []
}
return session
}

View File

@@ -2,7 +2,13 @@
import { EventEmitter } from 'node:events'
import { createRequire } from 'node:module'
import type { CanUseTool, McpHttpServerConfig, Options, SDKMessage } from '@anthropic-ai/claude-agent-sdk'
import type {
AgentDefinition,
CanUseTool,
McpHttpServerConfig,
Options,
SDKMessage
} from '@anthropic-ai/claude-agent-sdk'
import { query } from '@anthropic-ai/claude-agent-sdk'
import { loggerService } from '@logger'
import { config as apiConfigService } from '@main/apiServer/config'
@@ -10,7 +16,7 @@ import { validateModelId } from '@main/apiServer/utils'
import getLoginShellEnvironment from '@main/utils/shell-env'
import { app } from 'electron'
import type { GetAgentSessionResponse } from '../..'
import { agentService, type GetAgentSessionResponse } from '../..'
import type { AgentServiceInterface, AgentStream, AgentStreamEvent } from '../../interfaces/AgentStreamInterface'
import { sessionService } from '../SessionService'
import { buildNamespacedToolCallId } from './claude-stream-state'
@@ -157,6 +163,32 @@ class ClaudeCodeService implements AgentServiceInterface {
})
}
const subAgents: Record<string, AgentDefinition> = {}
if (session.sub_agents && session.sub_agents.length > 0) {
for (const subAgentId of session.sub_agents) {
try {
const agentConfig = await agentService.getAgentConfigForSDK(subAgentId)
if (agentConfig) {
subAgents[subAgentId] = {
// TODO: support custom model for sub-agents
model: 'inherit',
description: agentConfig.description ?? '',
prompt: agentConfig.instructions ?? '',
tools: agentConfig.allowed_tools
}
logger.info('Loaded sub-agent', { subAgentId })
} else {
logger.warn('Sub-agent not found', { subAgentId })
}
} catch (error) {
logger.error('Failed to load sub-agent config', {
subAgentId,
error: error instanceof Error ? error.message : String(error)
})
}
}
}
// Build SDK options from parameters
const options: Options = {
abortController,

View File

@@ -156,6 +156,12 @@
"uninstalling": "Uninstalling..."
},
"prompt": "Prompt Settings",
"sub_agents": {
"placeholder": "Select sub agents",
"tab": "Sub Agents",
"title": "Sub Agents",
"tooltip": "Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Connect MCP servers to unlock additional tools you can approve above.",
@@ -641,6 +647,7 @@
"description": "No files available in accessible directories",
"label": "No File Found"
},
"sub_agent": "Sub-Agent",
"title": "Activity Directory"
},
"auto_resize": "Auto resize height",

View File

@@ -156,6 +156,12 @@
"uninstalling": "卸载中..."
},
"prompt": "提示词设置",
"sub_agents": {
"placeholder": "选择子智能体",
"tab": "子智能体",
"title": "子智能体",
"tooltip": "选择可以被此智能体委派任务的其他智能体"
},
"tooling": {
"mcp": {
"description": "连接 MCP 服务器即可解锁更多可在上方预先授权的工具。",
@@ -641,6 +647,7 @@
"description": "可访问目录中没有可用文件",
"label": "未找到文件"
},
"sub_agent": "子代理",
"title": "活动目录"
},
"auto_resize": "自动调整高度",

View File

@@ -156,6 +156,12 @@
"uninstalling": "解除安裝中..."
},
"prompt": "提示設定",
"sub_agents": {
"placeholder": "選擇子助手",
"tab": "子助手",
"title": "子助手",
"tooltip": "選擇可以被此助手委派任務的其他助手"
},
"tooling": {
"mcp": {
"description": "連線 MCP 伺服器即可解鎖更多可在上方預先授權的工具。",
@@ -641,6 +647,7 @@
"description": "可存取的目錄中沒有檔案",
"label": "找不到檔案"
},
"sub_agent": "子代理",
"title": "活動目錄"
},
"auto_resize": "自動調整高度",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Deinstallation läuft..."
},
"prompt": "Prompt-Einstellungen",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Verbinden Sie MCP-Server, um weitere Tools freizuschalten, die oben vorab autorisiert werden können.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Απεγκατάσταση..."
},
"prompt": "Ρυθμίσεις Προτροπής",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Συνδέστε διακομιστές MCP για να ξεκλειδώσετε πρόσθετα εργαλεία που μπορείτε να εγκρίνετε παραπάνω.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Desinstalando..."
},
"prompt": "Configuración de indicaciones",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Conecta servidores MCP para desbloquear herramientas adicionales que puedes aprobar arriba.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Désinstallation en cours..."
},
"prompt": "Paramètres de l'invite",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Connectez des serveurs MCP pour débloquer des outils supplémentaires que vous pouvez approuver ci-dessus.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "アンインストール中..."
},
"prompt": "プロンプト設定",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "MCPサーバーを接続して、上で承認できる追加ツールを解放します。",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Desinstalando..."
},
"prompt": "Configurações de Prompt",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Conecte servidores MCP para desbloquear ferramentas adicionais que você pode aprovar acima.",

View File

@@ -156,6 +156,12 @@
"uninstalling": "Удаление..."
},
"prompt": "Настройки подсказки",
"sub_agents": {
"placeholder": "[to be translated]:Select sub agents",
"tab": "[to be translated]:Sub Agents",
"title": "[to be translated]:Sub Agents",
"tooltip": "[to be translated]:Select other agents that can be delegated tasks by this agent"
},
"tooling": {
"mcp": {
"description": "Подключите серверы MCP, чтобы разблокировать дополнительные инструменты, которые вы можете одобрить выше.",

View File

@@ -103,12 +103,23 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
// Prepare session data for tools
const sessionData = useMemo(() => {
if (!session) return undefined
// Get installed agent plugins from session.plugins
const agentPlugins = (session.plugins ?? [])
.filter((plugin) => plugin.type === 'agent')
.map((plugin) => ({
id: plugin.filename,
name: plugin.metadata.name ?? plugin.filename.replace(/\.md$/i, ''),
description: plugin.metadata.description
}))
return {
agentId,
sessionId,
slashCommands: session.slash_commands,
tools: session.tools,
accessiblePaths: session.accessible_paths ?? []
accessiblePaths: session.accessible_paths ?? [],
subAgents: agentPlugins
}
}, [session, agentId, sessionId])
@@ -158,6 +169,8 @@ interface InnerProps {
sessionId?: string
slashCommands?: Array<{ command: string; description?: string }>
tools?: Array<{ id: string; name: string; type: string; description?: string }>
accessiblePaths?: string[]
subAgents?: Array<{ id: string; name: string; description?: string }>
}
actionsRef: React.MutableRefObject<{
resizeTextArea: () => void

View File

@@ -25,11 +25,12 @@ const activityDirectoryTool = defineTool({
const { quickPanel, quickPanelController, actions, session } = context
const { onTextChange } = actions
// Get accessible paths from session data
// Get accessible paths and sub-agents from session data
const accessiblePaths = session?.accessiblePaths ?? []
const subAgents = session?.subAgents ?? []
// Only render if we have accessible paths
if (accessiblePaths.length === 0) {
// Only render if we have accessible paths or sub-agents
if (accessiblePaths.length === 0 && subAgents.length === 0) {
return null
}
@@ -38,6 +39,7 @@ const activityDirectoryTool = defineTool({
quickPanel={quickPanel}
quickPanelController={quickPanelController}
accessiblePaths={accessiblePaths}
subAgents={subAgents}
setText={onTextChange as React.Dispatch<React.SetStateAction<string>>}
/>
)

View File

@@ -13,10 +13,17 @@ interface Props {
quickPanel: ToolQuickPanelApi
quickPanelController: ToolQuickPanelController
accessiblePaths: string[]
subAgents?: Array<{ id: string; name: string; description?: string }>
setText: React.Dispatch<React.SetStateAction<string>>
}
const ActivityDirectoryButton: FC<Props> = ({ quickPanel, quickPanelController, accessiblePaths, setText }) => {
const ActivityDirectoryButton: FC<Props> = ({
quickPanel,
quickPanelController,
accessiblePaths,
subAgents,
setText
}) => {
const { t } = useTranslation()
const { handleOpenQuickPanel } = useActivityDirectoryPanel(
@@ -24,6 +31,7 @@ const ActivityDirectoryButton: FC<Props> = ({ quickPanel, quickPanelController,
quickPanel,
quickPanelController,
accessiblePaths,
subAgents,
setText
},
'button'

View File

@@ -15,8 +15,9 @@ const ActivityDirectoryQuickPanelManager = ({ context }: ManagerProps) => {
session
} = context
// Get accessible paths from session data
// Get accessible paths and sub-agents from session data
const accessiblePaths = session?.accessiblePaths ?? []
const subAgents = session?.subAgents ?? []
// Always call hooks unconditionally (React rules)
useActivityDirectoryPanel(
@@ -24,6 +25,7 @@ const ActivityDirectoryQuickPanelManager = ({ context }: ManagerProps) => {
quickPanel,
quickPanelController,
accessiblePaths,
subAgents,
setText: onTextChange as React.Dispatch<React.SetStateAction<string>>
},
'manager'

View File

@@ -2,7 +2,7 @@ import { loggerService } from '@logger'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import { QuickPanelReservedSymbol } from '@renderer/components/QuickPanel'
import type { ToolQuickPanelApi, ToolQuickPanelController } from '@renderer/pages/home/Inputbar/types'
import { File, Folder } from 'lucide-react'
import { Bot, File, Folder } from 'lucide-react'
import type React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -25,15 +25,22 @@ export type ActivityDirectoryTriggerInfo = {
symbol?: QuickPanelReservedSymbol
}
interface SubAgentInfo {
id: string
name: string
description?: string
}
interface Params {
quickPanel: ToolQuickPanelApi
quickPanelController: ToolQuickPanelController
accessiblePaths: string[]
subAgents?: SubAgentInfo[]
setText: React.Dispatch<React.SetStateAction<string>>
}
export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'manager' = 'button') => {
const { quickPanel, quickPanelController, accessiblePaths, setText } = params
const { quickPanel, quickPanelController, accessiblePaths, subAgents = [], setText } = params
const { registerTrigger, registerRootMenu } = quickPanel
const { open, close, updateList, isVisible, symbol } = quickPanelController
const { t } = useTranslation()
@@ -238,6 +245,68 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
[close, insertFilePath]
)
/**
* Insert sub-agent name at @ position
*/
const insertSubAgentName = useCallback(
(agentName: string, triggerInfo?: ActivityDirectoryTriggerInfo) => {
setText((currentText) => {
const symbol = triggerInfo?.symbol ?? QuickPanelReservedSymbol.MentionModels
const triggerIndex =
triggerInfo?.position !== undefined
? triggerInfo.position
: symbol === QuickPanelReservedSymbol.Root
? currentText.lastIndexOf('/')
: currentText.lastIndexOf('@')
if (triggerIndex !== -1) {
let endPos = triggerIndex + 1
while (endPos < currentText.length && !/\s/.test(currentText[endPos])) {
endPos++
}
return currentText.slice(0, triggerIndex) + agentName + ' ' + currentText.slice(endPos)
}
// If no trigger found, append at end
return currentText + ' ' + agentName + ' '
})
},
[setText]
)
/**
* Handle sub-agent selection
*/
const onSelectSubAgent = useCallback(
(agentName: string) => {
const trigger = triggerInfoRef.current
insertSubAgentName(agentName, trigger)
close()
},
[close, insertSubAgentName]
)
/**
* Create sub-agent list items for QuickPanel
*/
const createSubAgentItems = useCallback(
(agents: SubAgentInfo[]): QuickPanelListItem[] => {
if (agents.length === 0) {
return []
}
return agents.map((agent) => ({
label: agent.name,
description: agent.description || t('chat.input.activity_directory.sub_agent'),
icon: <Bot size={16} />,
filterText: `${agent.name} ${agent.description || ''} ${agent.id}`,
action: () => onSelectSubAgent(agent.name),
isSelected: false
}))
},
[onSelectSubAgent, t]
)
/**
* Create file list items for QuickPanel from a file list
*/
@@ -291,12 +360,18 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
)
/**
* Create file list items for QuickPanel (for current state)
* Create combined list items for QuickPanel (sub-agents + files)
*/
const fileItems = useMemo<QuickPanelListItem[]>(
() => createFileItems(fileList, isLoading),
[createFileItems, fileList, isLoading]
)
const combinedItems = useMemo<QuickPanelListItem[]>(() => {
const agentItems = createSubAgentItems(subAgents)
const files = createFileItems(fileList, isLoading)
// Combine: sub-agents first, then files
return [...agentItems, ...files]
}, [createSubAgentItems, subAgents, createFileItems, fileList, isLoading])
// Keep fileItems for backward compatibility
const fileItems = combinedItems
/**
* Handle search text change - load files and update list
@@ -311,11 +386,13 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
const hasChanged = updateFileListState(newFiles)
if (hasChanged) {
const newItems = createFileItems(newFiles, false)
updateList(newItems)
// Combine sub-agents and files
const agentItems = createSubAgentItems(subAgents)
const fileItems = createFileItems(newFiles, false)
updateList([...agentItems, ...fileItems])
}
},
[loadFiles, createFileItems, updateList, updateFileListState]
[loadFiles, createFileItems, createSubAgentItems, subAgents, updateList, updateFileListState]
)
/**
@@ -336,8 +413,10 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
const files = await loadFiles()
updateFileListState(files)
// Create items from the loaded files immediately
const items = createFileItems(files, false)
// Create items from sub-agents and loaded files immediately
const agentItems = createSubAgentItems(subAgents)
const fileItems = createFileItems(files, false)
const items = [...agentItems, ...fileItems]
open({
title: t('chat.input.activity_directory.description'),
@@ -377,7 +456,18 @@ export const useActivityDirectoryPanel = (params: Params, role: 'button' | 'mana
onSearchChange: handleSearchChange
})
},
[loadFiles, open, removeTriggerSymbolAndText, setText, t, handleSearchChange, createFileItems, updateFileListState]
[
loadFiles,
open,
removeTriggerSymbolAndText,
setText,
t,
handleSearchChange,
createFileItems,
createSubAgentItems,
subAgents,
updateFileListState
]
)
/**

View File

@@ -68,6 +68,7 @@ export interface ToolContext {
slashCommands?: Array<{ command: string; description?: string }>
tools?: Array<{ id: string; name: string; type: string; description?: string }>
accessiblePaths?: string[]
subAgents?: Array<{ id: string; name: string; description?: string }>
}
}

View File

@@ -11,6 +11,7 @@ import EssentialSettings from './EssentialSettings'
import PluginSettings from './PluginSettings'
import PromptSettings from './PromptSettings'
import { AgentLabel, LeftMenu, Settings, StyledMenu, StyledModal } from './shared'
import SubAgentsSettings from './SubAgentsSettings'
import ToolingSettings from './ToolingSettings'
interface AgentSettingPopupShowParams {
@@ -22,7 +23,7 @@ interface AgentSettingPopupParams extends AgentSettingPopupShowParams {
resolve: () => void
}
type AgentSettingPopupTab = 'essential' | 'prompt' | 'tooling' | 'advanced' | 'plugins' | 'session-mcps'
type AgentSettingPopupTab = 'essential' | 'prompt' | 'tooling' | 'advanced' | 'plugins' | 'sub-agents' | 'session-mcps'
const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => {
const [open, setOpen] = useState(true)
@@ -62,6 +63,10 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
key: 'plugins',
label: t('agent.settings.plugins.tab', 'Plugins')
},
{
key: 'sub-agents',
label: t('agent.settings.sub_agents.tab', 'Sub-agents')
},
{
key: 'advanced',
label: t('agent.settings.advance.title', 'Advanced Settings')
@@ -107,6 +112,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
{menu === 'prompt' && <PromptSettings agentBase={agent} update={updateAgent} />}
{menu === 'tooling' && <ToolingSettings agentBase={agent} update={updateAgent} />}
{menu === 'plugins' && <PluginSettings agentBase={agent} update={updateAgent} />}
{menu === 'sub-agents' && <SubAgentsSettings agentBase={agent} update={updateAgent} />}
{menu === 'advanced' && <AdvancedSettings agentBase={agent} update={updateAgent} />}
</Settings>
</div>

View File

@@ -0,0 +1,57 @@
import { useAgents } from '@renderer/hooks/agents/useAgents'
import type { GetAgentResponse, GetAgentSessionResponse, UpdateAgentFunctionUnion } from '@renderer/types'
import { Form, Select, Spin } from 'antd'
import { useTranslation } from 'react-i18next'
interface SubAgentsSettingsProps {
agentBase: GetAgentResponse | GetAgentSessionResponse | undefined | null
update: UpdateAgentFunctionUnion
}
const SubAgentsSettings: React.FC<SubAgentsSettingsProps> = ({ agentBase, update }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
const { agents, isLoading } = useAgents()
if (!agentBase) return
const handleValuesChange = (changedValues: { sub_agents: string[] }) => {
update({
id: agentBase.id,
...changedValues
})
}
if (isLoading) {
return <Spin />
}
const availableAgents = agents?.filter((agent) => agent.id !== agentBase.id) || []
return (
<Form
form={form}
layout="vertical"
initialValues={{ sub_agents: agentBase.sub_agents || [] }}
onValuesChange={handleValuesChange}
style={{ maxWidth: 600 }}>
<Form.Item
name="sub_agents"
label={t('agent.settings.sub_agents.title')}
tooltip={t('agent.settings.sub_agents.tooltip')}>
<Select
mode="multiple"
placeholder={t('agent.settings.sub_agents.placeholder')}
loading={isLoading}
options={availableAgents.map((agent) => ({
label: agent.name,
value: agent.id
}))}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
/>
</Form.Item>
</Form>
)
}
export default SubAgentsSettings

View File

@@ -82,6 +82,7 @@ export const AgentBaseSchema = z.object({
// Tools
mcps: z.array(z.string()).optional(), // Array of MCP tool IDs
allowed_tools: z.array(z.string()).optional(), // Array of allowed tool IDs (whitelist)
sub_agents: z.array(z.string()).optional(), // Array of sub-agent IDs
slash_commands: z.array(SlashCommandSchema).optional(), // Array of slash commands merged from builtin and SDK
// Configuration
@@ -132,7 +133,7 @@ export const AgentSessionEntitySchema = AgentBaseSchema.extend({
id: z.string(),
agent_id: z.string(), // Primary agent ID for the session
agent_type: AgentTypeSchema,
// sub_agent_ids?: string[] // Array of sub-agent IDs involved in the session
sub_agents: z.array(z.string()).optional(), // Array of sub-agent IDs involved in the session
created_at: z.iso.datetime(),
updated_at: z.iso.datetime()
@@ -205,6 +206,7 @@ export type BaseAgentForm = {
model: string
accessible_paths: string[]
allowed_tools: string[]
sub_agents?: string[]
mcps?: string[]
configuration?: AgentConfiguration
}
@@ -286,6 +288,7 @@ export interface UpdateSessionRequest extends Partial<AgentBase> {}
export const GetAgentSessionResponseSchema = AgentSessionEntitySchema.extend({
tools: z.array(ToolSchema).optional(), // All tools available to the session (including built-in and custom)
sub_agents: z.array(z.string()).optional(), // Array of sub-agent IDs
messages: z.array(AgentSessionMessageEntitySchema).optional(), // Messages in the session
plugins: z
.array(