From 1175823ab82d69d392d5f89a8caf7f1c3124cc3d Mon Sep 17 00:00:00 2001 From: suyao Date: Wed, 19 Nov 2025 15:58:15 +0800 Subject: [PATCH] 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. --- .../database/drizzle/0003_smooth_talkback.sql | 2 + .../database/drizzle/meta/0003_snapshot.json | 360 ++++++++++++++++++ resources/database/drizzle/meta/_journal.json | 7 + src/main/services/FileStorage.ts | 2 +- src/main/services/agents/BaseService.ts | 1 + .../agents/database/schema/agents.schema.ts | 1 + .../agents/database/schema/sessions.schema.ts | 1 + .../services/agents/services/AgentService.ts | 13 + .../agents/services/SessionService.ts | 17 + .../agents/services/claudecode/index.ts | 36 +- src/renderer/src/i18n/locales/en-us.json | 7 + src/renderer/src/i18n/locales/zh-cn.json | 7 + src/renderer/src/i18n/locales/zh-tw.json | 7 + src/renderer/src/i18n/translate/de-de.json | 6 + src/renderer/src/i18n/translate/el-gr.json | 6 + src/renderer/src/i18n/translate/es-es.json | 6 + src/renderer/src/i18n/translate/fr-fr.json | 6 + src/renderer/src/i18n/translate/ja-jp.json | 6 + src/renderer/src/i18n/translate/pt-pt.json | 6 + src/renderer/src/i18n/translate/ru-ru.json | 6 + .../home/Inputbar/AgentSessionInputbar.tsx | 15 +- .../Inputbar/tools/activityDirectoryTool.tsx | 8 +- .../components/ActivityDirectoryButton.tsx | 10 +- .../ActivityDirectoryQuickPanelManager.tsx | 4 +- .../components/useActivityDirectoryPanel.tsx | 116 +++++- src/renderer/src/pages/home/Inputbar/types.ts | 1 + .../AgentSettings/AgentSettingsPopup.tsx | 8 +- .../AgentSettings/SubAgentsSettings.tsx | 57 +++ src/renderer/src/types/agent.ts | 5 +- 29 files changed, 703 insertions(+), 24 deletions(-) create mode 100644 resources/database/drizzle/0003_smooth_talkback.sql create mode 100644 resources/database/drizzle/meta/0003_snapshot.json create mode 100644 src/renderer/src/pages/settings/AgentSettings/SubAgentsSettings.tsx diff --git a/resources/database/drizzle/0003_smooth_talkback.sql b/resources/database/drizzle/0003_smooth_talkback.sql new file mode 100644 index 0000000000..2a608ffc47 --- /dev/null +++ b/resources/database/drizzle/0003_smooth_talkback.sql @@ -0,0 +1,2 @@ +ALTER TABLE `agents` ADD `sub_agents` text;--> statement-breakpoint +ALTER TABLE `sessions` ADD `sub_agents` text; \ No newline at end of file diff --git a/resources/database/drizzle/meta/0003_snapshot.json b/resources/database/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000000..f3d130f612 --- /dev/null +++ b/resources/database/drizzle/meta/0003_snapshot.json @@ -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": {} + } +} diff --git a/resources/database/drizzle/meta/_journal.json b/resources/database/drizzle/meta/_journal.json index ac026637aa..d1c0c258ee 100644 --- a/resources/database/drizzle/meta/_journal.json +++ b/resources/database/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1762526423527, "tag": "0002_wealthy_naoko", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1763500397620, + "tag": "0003_smooth_talkback", + "breakpoints": true } ] } diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 3165fcf27e..0dd8a71e02 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -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 }) diff --git a/src/main/services/agents/BaseService.ts b/src/main/services/agents/BaseService.ts index 1c9b438e4a..e835bfc29c 100644 --- a/src/main/services/agents/BaseService.ts +++ b/src/main/services/agents/BaseService.ts @@ -42,6 +42,7 @@ export abstract class BaseService { 'configuration', 'accessible_paths', 'allowed_tools', + 'sub_agents', 'slash_commands' ] diff --git a/src/main/services/agents/database/schema/agents.schema.ts b/src/main/services/agents/database/schema/agents.schema.ts index be8983c1fc..96b6ff0f22 100644 --- a/src/main/services/agents/database/schema/agents.schema.ts +++ b/src/main/services/agents/database/schema/agents.schema.ts @@ -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 diff --git a/src/main/services/agents/database/schema/sessions.schema.ts b/src/main/services/agents/database/schema/sessions.schema.ts index 4b16a9ec41..2d8798f88e 100644 --- a/src/main/services/agents/database/schema/sessions.schema.ts +++ b/src/main/services/agents/database/schema/sessions.schema.ts @@ -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 diff --git a/src/main/services/agents/services/AgentService.ts b/src/main/services/agents/services/AgentService.ts index 07ed89a0f3..45d1fe3837 100644 --- a/src/main/services/agents/services/AgentService.ts +++ b/src/main/services/agents/services/AgentService.ts @@ -117,6 +117,19 @@ export class AgentService extends BaseService { return agent } + async getAgentConfigForSDK(id: string): Promise { + 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 diff --git a/src/main/services/agents/services/SessionService.ts b/src/main/services/agents/services/SessionService.ts index c9ecf72c32..6f01d5df7d 100644 --- a/src/main/services/agents/services/SessionService.ts +++ b/src/main/services/agents/services/SessionService.ts @@ -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 } diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index a8f3f54fa8..6405ae94ef 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -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 { promptForToolApproval } from './tool-permissions' @@ -153,6 +159,32 @@ class ClaudeCodeService implements AgentServiceInterface { return promptForToolApproval(toolName, input, options) } + const subAgents: Record = {} + 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, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index c1c0dc2620..53be87f124 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 2a1ee09688..d1bed1fdf5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "自动调整高度", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f5a0264875..fd368e644c 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "自動調整高度", diff --git a/src/renderer/src/i18n/translate/de-de.json b/src/renderer/src/i18n/translate/de-de.json index 597259d919..ee2833dbd3 100644 --- a/src/renderer/src/i18n/translate/de-de.json +++ b/src/renderer/src/i18n/translate/de-de.json @@ -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.", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index d7cf3b8cff..18b0453e71 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -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 για να ξεκλειδώσετε πρόσθετα εργαλεία που μπορείτε να εγκρίνετε παραπάνω.", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 7c857e9efc..3a3a3488d2 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -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.", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index a9025e404e..6dc8d8a4ee 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -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.", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 1559b450da..9f77118508 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -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サーバーを接続して、上で承認できる追加ツールを解放します。", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index eb55076143..07d7d20be6 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -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.", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index c9e4fc6fc4..c69ede1748 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -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, чтобы разблокировать дополнительные инструменты, которые вы можете одобрить выше.", diff --git a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx index 722697be78..b013fd6500 100644 --- a/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/AgentSessionInputbar.tsx @@ -103,12 +103,23 @@ const AgentSessionInputbar: FC = ({ 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 diff --git a/src/renderer/src/pages/home/Inputbar/tools/activityDirectoryTool.tsx b/src/renderer/src/pages/home/Inputbar/tools/activityDirectoryTool.tsx index 655de594d2..c9a0a26c6e 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/activityDirectoryTool.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/activityDirectoryTool.tsx @@ -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>} /> ) diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryButton.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryButton.tsx index cda8e7ca8f..c5699dd26f 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryButton.tsx @@ -13,10 +13,17 @@ interface Props { quickPanel: ToolQuickPanelApi quickPanelController: ToolQuickPanelController accessiblePaths: string[] + subAgents?: Array<{ id: string; name: string; description?: string }> setText: React.Dispatch> } -const ActivityDirectoryButton: FC = ({ quickPanel, quickPanelController, accessiblePaths, setText }) => { +const ActivityDirectoryButton: FC = ({ + quickPanel, + quickPanelController, + accessiblePaths, + subAgents, + setText +}) => { const { t } = useTranslation() const { handleOpenQuickPanel } = useActivityDirectoryPanel( @@ -24,6 +31,7 @@ const ActivityDirectoryButton: FC = ({ quickPanel, quickPanelController, quickPanel, quickPanelController, accessiblePaths, + subAgents, setText }, 'button' diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryQuickPanelManager.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryQuickPanelManager.tsx index b5d01326b9..47c32fae1a 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryQuickPanelManager.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/ActivityDirectoryQuickPanelManager.tsx @@ -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> }, 'manager' diff --git a/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx b/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx index b83c00c42d..758b3485b3 100644 --- a/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx +++ b/src/renderer/src/pages/home/Inputbar/tools/components/useActivityDirectoryPanel.tsx @@ -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> } 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: , + 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( - () => createFileItems(fileList, isLoading), - [createFileItems, fileList, isLoading] - ) + const combinedItems = useMemo(() => { + 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 + ] ) /** diff --git a/src/renderer/src/pages/home/Inputbar/types.ts b/src/renderer/src/pages/home/Inputbar/types.ts index bfe5df3a12..de7cebeff4 100644 --- a/src/renderer/src/pages/home/Inputbar/types.ts +++ b/src/renderer/src/pages/home/Inputbar/types.ts @@ -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 }> } } diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentSettingsPopup.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentSettingsPopup.tsx index 84859fbecb..92cd8603d7 100644 --- a/src/renderer/src/pages/settings/AgentSettings/AgentSettingsPopup.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/AgentSettingsPopup.tsx @@ -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 = ({ tab, agentId, resolve }) => { const [open, setOpen] = useState(true) @@ -62,6 +63,10 @@ const AgentSettingPopupContainer: React.FC = ({ 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 = ({ tab, ag {menu === 'prompt' && } {menu === 'tooling' && } {menu === 'plugins' && } + {menu === 'sub-agents' && } {menu === 'advanced' && } diff --git a/src/renderer/src/pages/settings/AgentSettings/SubAgentsSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/SubAgentsSettings.tsx new file mode 100644 index 0000000000..2f8624bb40 --- /dev/null +++ b/src/renderer/src/pages/settings/AgentSettings/SubAgentsSettings.tsx @@ -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 = ({ 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 + } + + const availableAgents = agents?.filter((agent) => agent.id !== agentBase.id) || [] + + return ( +
+ +