diff --git a/CLAUDE.md b/CLAUDE.md index 3a47b0de5f..4119019336 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ This file provides guidance to AI coding assistants when working with code in th - **Install**: `yarn install` - **Development**: `yarn dev` - Runs Electron app in development mode - **Debug**: `yarn debug` - Starts with debugging enabled, use chrome://inspect -- **Build Check**: `yarn build:check` - REQUIRED before commits (lint + test + typecheck) +- **Build Check**: `yarn build:check` - REQUIRED before commits (lint + test + typecheck), if having i18n sort issues, run `yarn i18n:sync` first to sync template. - **Test**: `yarn test` - Run all tests (Vitest) - **Single Test**: `yarn test:main` or `yarn test:renderer` - **Lint**: `yarn lint` - Fix linting issues and run typecheck diff --git a/src/main/apiServer/services/mcp.ts b/src/main/apiServer/services/mcp.ts index d6acae745e..4cb19477b8 100644 --- a/src/main/apiServer/services/mcp.ts +++ b/src/main/apiServer/services/mcp.ts @@ -113,14 +113,14 @@ class MCPApiService extends EventEmitter { const client = await mcpService.initClient(server) const tools = await client.listTools() - logger.info(`Server with id ${id} info:`, { tools: JSON.stringify(tools) }) + logger.silly(`Server with id ${id} info:`, { tools: JSON.stringify(tools.tools) }) return { id: server.id, name: server.name, type: server.type, description: server.description, - tools + tools: tools.tools } } catch (error: any) { logger.error(`Failed to get server info with id ${id}:`, error) diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index 6536c6103e..ebce1b05d2 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -25,6 +25,7 @@ import { useApiModels } from '@renderer/hooks/agents/useModels' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' import { AddAgentForm, + AgentConfigurationSchema, AgentEntity, AgentType, BaseAgentForm, @@ -57,7 +58,9 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({ instructions: existing?.instructions, model: existing?.model ?? 'claude-4-sonnet', accessible_paths: existing?.accessible_paths ? [...existing.accessible_paths] : [], - allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [] + allowed_tools: existing?.allowed_tools ? [...existing.allowed_tools] : [], + mcps: existing?.mcps ? [...existing.mcps] : [], + configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {}) }) interface BaseProps { @@ -320,7 +323,8 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o instructions: form.instructions, model: form.model, accessible_paths: [...form.accessible_paths], - allowed_tools: [...form.allowed_tools] + allowed_tools: [...form.allowed_tools], + configuration: form.configuration ? { ...form.configuration } : undefined } satisfies UpdateAgentForm updateAgent(updatePayload) @@ -333,7 +337,8 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o instructions: form.instructions, model: form.model, accessible_paths: [...form.accessible_paths], - allowed_tools: [...form.allowed_tools] + allowed_tools: [...form.allowed_tools], + configuration: form.configuration ? { ...form.configuration } : undefined } satisfies AddAgentForm addAgent(newAgent) logger.debug('Added agent', newAgent) @@ -352,6 +357,7 @@ export const AgentModal: React.FC = ({ agent, trigger, isOpen: _isOpen, o form.instructions, form.accessible_paths, form.allowed_tools, + form.configuration, agent, onClose, t, diff --git a/src/renderer/src/components/Popups/agent/SessionModal.tsx b/src/renderer/src/components/Popups/agent/SessionModal.tsx index 8fc869b172..efa037a72e 100644 --- a/src/renderer/src/components/Popups/agent/SessionModal.tsx +++ b/src/renderer/src/components/Popups/agent/SessionModal.tsx @@ -58,7 +58,8 @@ const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): ? [...existing.allowed_tools] : agent?.allowed_tools ? [...agent.allowed_tools] - : [] + : [], + mcps: existing?.mcps ? [...existing.mcps] : agent?.mcps ? [...agent.mcps] : [] }) interface BaseProps { @@ -251,7 +252,8 @@ export const SessionModal: React.FC = ({ instructions: form.instructions, model: form.model, accessible_paths: [...form.accessible_paths], - allowed_tools: [...(form.allowed_tools ?? [])] + allowed_tools: [...(form.allowed_tools ?? [])], + mcps: [...(form.mcps ?? [])] } satisfies UpdateSessionForm updateSession(updatePayload) @@ -263,7 +265,8 @@ export const SessionModal: React.FC = ({ instructions: form.instructions, model: form.model, accessible_paths: [...form.accessible_paths], - allowed_tools: [...(form.allowed_tools ?? [])] + allowed_tools: [...(form.allowed_tools ?? [])], + mcps: [...(form.mcps ?? [])] } satisfies CreateSessionForm const createdSession = await createSession(newSession) if (createdSession) { @@ -285,6 +288,7 @@ export const SessionModal: React.FC = ({ form.instructions, form.accessible_paths, form.allowed_tools, + form.mcps, session, onClose, onSessionCreated, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b28d3bc351..ab6b2332fd 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -46,9 +46,9 @@ }, "allowed_tools": { "empty": "No tools available for this agent.", - "helper": "Choose which tools are pre-approved. Unselected tools require approval before use.", - "label": "Allowed tools", - "placeholder": "Select allowed tools" + "helper": "Pre-approved tools run without manual approval. Unselected tools require approval before use.", + "label": "Pre-approved tools", + "placeholder": "Select pre-approved tools" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "Define how many request/response cycles the agent may complete automatically.", + "helper": "Higher values enable longer autonomous runs; lower values keep sessions short.", + "label": "Conversation turn limit" + }, + "permissionMode": { + "description": "Control how the agent handles actions that require approval.", + "label": "Permission mode", + "options": { + "acceptEdits": "Accept edits automatically", + "bypassPermissions": "Bypass permission checks", + "default": "Default (ask before continuing)", + "plan": "Planning mode (requires plan approval)" + }, + "placeholder": "Choose a permission behavior" + }, + "title": "Advanced Settings" + }, "essential": "Essential Settings", - "prompt": "Prompt Settings" + "mcps": "MCP Servers", + "prompt": "Prompt Settings", + "tools": { + "approved": "approved", + "caution": "Pre-approved tools bypass human review. Enable only trusted tools.", + "description": "Choose which tools can run without manual approval.", + "requiresPermission": "Requires permission when not pre-approved.", + "tab": "Pre-approved tools", + "title": "Pre-approved tools", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Agent Type", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index e6ec135a0e..e798d9000d 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "当前智能体暂无可用工具。", "helper": "选择预先授权的工具,未选中的工具在使用时需要手动审批。", - "label": "允许的工具", - "placeholder": "选择允许使用的工具" + "label": "预先授权工具", + "placeholder": "选择预先授权的工具" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "设定代理自动执行的请求/回复轮次数。", + "helper": "数值越高可自主运行越久;数值越低更易控制。", + "label": "会话轮次数上限" + }, + "permissionMode": { + "description": "控制代理在需要授权时的处理方式。", + "label": "权限模式", + "options": { + "acceptEdits": "自动接受编辑", + "bypassPermissions": "跳过权限检查", + "default": "默认(继续前询问)", + "plan": "规划模式(需审批计划)" + }, + "placeholder": "选择权限模式" + }, + "title": "高级设置" + }, "essential": "基础设置", - "prompt": "提示词设置" + "mcps": "MCP 服务器", + "prompt": "提示词设置", + "tools": { + "approved": "已授权", + "caution": "预先授权的工具会跳过人工审核,请仅启用可信的工具。", + "description": "选择哪些工具可以在无需人工审批的情况下执行。", + "requiresPermission": "未预先授权时需要人工审批。", + "tab": "预先授权工具", + "title": "预先授权工具", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "智能体类型", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 269ef98b39..6abfcd0c62 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "目前此代理沒有可用的工具。", "helper": "選擇預先授權的工具,未選取的工具在使用時需要手動審批。", - "label": "允許的工具", - "placeholder": "選擇允許使用的工具" + "label": "預先授權工具", + "placeholder": "選擇預先授權的工具" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "設定代理自動執行的請求/回覆輪次数。", + "helper": "數值越高可自動運行越久;數值越低更容易掌控。", + "label": "會話輪次上限" + }, + "permissionMode": { + "description": "控制代理在需要授權時的處理方式。", + "label": "權限模式", + "options": { + "acceptEdits": "自動接受編輯", + "bypassPermissions": "略過權限檢查", + "default": "預設(繼續前先詢問)", + "plan": "規劃模式(需核准計畫)" + }, + "placeholder": "選擇權限模式" + }, + "title": "進階設定" + }, "essential": "必要設定", - "prompt": "提示設定" + "mcps": "MCP 伺服器", + "prompt": "提示設定", + "tools": { + "approved": "已授權", + "caution": "預先授權的工具會略過人工審查,請僅啟用可信任的工具。", + "description": "選擇哪些工具可在無需人工核准的情況下執行。", + "requiresPermission": "未預先授權時需要人工核准。", + "tab": "預先授權工具", + "title": "預先授權工具", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "代理類型", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 5fd6d5a575..f73eb9c3dd 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "Δεν υπάρχουν διαθέσιμα εργαλεία για αυτόν τον agent.", "helper": "Επιλέξτε ποια εργαλεία είναι προεγκεκριμένα. Τα μη επιλεγμένα θα χρειαστούν έγκριση πριν από τη χρήση.", - "label": "Επιτρεπόμενα εργαλεία", - "placeholder": "Επιλέξτε επιτρεπόμενα εργαλεία" + "label": "Προεγκεκριμένα εργαλεία", + "placeholder": "Επιλέξτε προεγκεκριμένα εργαλεία" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "Ορίστε τον αριθμό γύρων αιτήματος/απάντησης που θα εκτελούνται αυτόματα από τον διαμεσολαβητ.", + "helper": "Όσο υψηλότερη είναι η τιμή, τόσο περισσότερο μπορεί να λειτουργεί αυτόνομα· όσο χαμηλότερη είναι, τόσο πιο εύκολα ελέγχεται.", + "label": "Όριο αριθμού γύρων συνεδρίας" + }, + "permissionMode": { + "description": "Ο τρόπος με τον οποίο ο πληρεξούσιος χειρίζεται την κατάσταση όταν απαιτείται εξουσιοδότηση.", + "label": "Λειτουργία δικαιωμάτων", + "options": { + "acceptEdits": "Αυτόματη αποδοχή επεξεργασίας", + "bypassPermissions": "παράλειψη ελέγχου δικαιωμάτων", + "default": "Προεπιλογή (να ερωτηθεί πριν από τη συνέχεια)", + "plan": "Λειτουργία σχεδιασμού (απαιτείται έγκριση σχεδίου)" + }, + "placeholder": "Επιλέξτε λειτουργία δικαιωμάτων" + }, + "title": "Ρυθμίσεις για προχωρημένους" + }, "essential": "Βασικές Ρυθμίσεις", - "prompt": "Ρυθμίσεις Προτροπής" + "mcps": "Διακομιστής MCP", + "prompt": "Ρυθμίσεις Προτροπής", + "tools": { + "approved": "εγκεκριμένο", + "caution": "Εργαλεία προεγκεκριμένα παρακάμπτουν την ανθρώπινη αξιολόγηση. Ενεργοποιήστε μόνο έμπιστα εργαλεία.", + "description": "Επιλέξτε ποια εργαλεία μπορούν να εκτελούνται χωρίς χειροκίνητη έγκριση.", + "requiresPermission": "Απαιτείται άδεια όταν δεν έχει προεγκριθεί.", + "tab": "Προεγκεκριμένα εργαλεία", + "title": "Προεγκεκριμένα εργαλεία", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Τύπος Πράκτορα", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 416921fbbb..ddf2b5b597 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "No hay herramientas disponibles para este agente.", "helper": "Elige qué herramientas quedan preaprobadas. Las no seleccionadas requerirán aprobación manual antes de usarse.", - "label": "Herramientas permitidas", - "placeholder": "Selecciona las herramientas permitidas" + "label": "Herramientas preaprobadas", + "placeholder": "Seleccionar herramientas preaprobadas" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "Establece el número de rondas de solicitud/respuesta que el agente ejecutará automáticamente.", + "helper": "Cuanto mayor es el valor, más tiempo puede funcionar de forma autónoma; cuanto menor es el valor, más fácil es de controlar.", + "label": "Límite máximo de turnos de conversación" + }, + "permissionMode": { + "description": "Cómo el agente de control maneja las situaciones que requieren autorización.", + "label": "modo de permisos", + "options": { + "acceptEdits": "Aceptar ediciones automáticamente", + "bypassPermissions": "Omitir verificación de permisos", + "default": "Predeterminado (preguntar antes de continuar)", + "plan": "Modo de planificación (requiere aprobación del plan)" + }, + "placeholder": "Seleccionar modo de permisos" + }, + "title": "Configuración avanzada" + }, "essential": "Configuraciones esenciales", - "prompt": "Configuración de indicaciones" + "mcps": "Servidor MCP", + "prompt": "Configuración de indicaciones", + "tools": { + "approved": "aprobado", + "caution": "Herramientas preaprobadas omiten la revisión humana. Habilita solo herramientas de confianza.", + "description": "Elige qué herramientas pueden ejecutarse sin aprobación manual.", + "requiresPermission": "Requiere permiso cuando no está preaprobado.", + "tab": "Herramientas preaprobadas", + "title": "Herramientas preaprobadas", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Tipo de Agente", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 38502d5f13..ead69c1bb9 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "Aucun outil disponible pour cet agent.", "helper": "Choisissez les outils préapprouvés. Les outils non sélectionnés nécessiteront une approbation avant utilisation.", - "label": "Outils autorisés", - "placeholder": "Sélectionner les outils autorisés" + "label": "Outils pré-approuvés", + "placeholder": "Sélectionner des outils pré-approuvés" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "Définir le nombre de cycles de requête/réponse exécutés automatiquement par l'agent.", + "helper": "Une valeur plus élevée permet une autonomie prolongée ; une valeur plus faible facilite le contrôle.", + "label": "Limite maximale de tours de conversation" + }, + "permissionMode": { + "description": "Contrôle la manière dont l'agent gère les demandes d'autorisation.", + "label": "mode d'autorisation", + "options": { + "acceptEdits": "Accepter automatiquement les modifications", + "bypassPermissions": "sauter la vérification des autorisations", + "default": "Par défaut (demander avant de continuer)", + "plan": "Mode de planification (plan soumis à approbation)" + }, + "placeholder": "Choisir le mode d'autorisation" + }, + "title": "Paramètres avancés" + }, "essential": "Paramètres essentiels", - "prompt": "Paramètres de l'invite" + "mcps": "Serveur MCP", + "prompt": "Paramètres de l'invite", + "tools": { + "approved": "approuvé", + "caution": "Outils pré-approuvés contournent la révision humaine. Activez uniquement les outils de confiance.", + "description": "Choisissez quels outils peuvent s'exécuter sans approbation manuelle.", + "requiresPermission": "Nécessite une autorisation lorsqu'elle n'est pas préapprouvée.", + "tab": "Outils pré-approuvés", + "title": "Outils pré-approuvés", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Type d'agent", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index 1c8743b218..8401752b77 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "このエージェントが利用できるツールはありません。", "helper": "事前承認済みのツールを選択します。未選択のツールは使用時に承認が必要になります。", - "label": "許可されたツール", - "placeholder": "許可するツールを選択" + "label": "事前承認済みツール", + "placeholder": "事前承認するツールを選択" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "プロキシが自動的に実行するリクエスト/レスポンスのラウンド数を設定します。", + "helper": "数値が高いほど自律動作の時間が長くなり、数値が低いほど制御しやすくなります。", + "label": "会話ラウンド数の上限" + }, + "permissionMode": { + "description": "制御エージェントが認可を必要とする場合の処理方法。", + "label": "権限モード", + "options": { + "acceptEdits": "自動的に編集を受け入れる", + "bypassPermissions": "権限チェックをスキップ", + "default": "デフォルト(続行する前に確認)", + "plan": "計画モード(承認が必要な計画)" + }, + "placeholder": "権限モードを選択" + }, + "title": "高級設定" + }, "essential": "必須設定", - "prompt": "プロンプト設定" + "mcps": "MCPサーバー", + "prompt": "プロンプト設定", + "tools": { + "approved": "承認済み", + "caution": "事前承認したツールは人によるレビューをスキップします。信頼できるツールのみ有効にしてください。", + "description": "人による承認なしで実行できるツールを選択します。", + "requiresPermission": "事前承認されていない場合は承認が必要です。", + "tab": "事前承認済みツール", + "title": "事前承認済みツール", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "エージェントタイプ", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 3106c19c8e..7e5f36b287 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "Não há ferramentas disponíveis para este agente.", "helper": "Escolha quais ferramentas ficam pré-autorizadas. As não selecionadas exigirão aprovação antes do uso.", - "label": "Ferramentas permitidas", - "placeholder": "Selecione as ferramentas permitidas" + "label": "Ferramentas pré-aprovadas", + "placeholder": "Selecionar ferramentas pré-aprovadas" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "Define o número de ciclos de solicitação/resposta executados automaticamente pelo agente.", + "helper": "Quanto maior o valor, mais tempo pode funcionar de forma autônoma; quanto menor o valor, mais fácil de controlar.", + "label": "Limite máximo de turnos de conversa" + }, + "permissionMode": { + "description": "Controla como o agente lida com situações que exigem autorização.", + "label": "Modo de permissão", + "options": { + "acceptEdits": "Aceitar edições automaticamente", + "bypassPermissions": "忽略检查 de permissão", + "default": "Padrão (perguntar antes de continuar)", + "plan": "Modo de planejamento (plano sujeito a aprovação)" + }, + "placeholder": "Selecione o modo de permissão" + }, + "title": "Configurações avançadas" + }, "essential": "Configurações Essenciais", - "prompt": "Configurações de Prompt" + "mcps": "Servidor MCP", + "prompt": "Configurações de Prompt", + "tools": { + "approved": "aprovado", + "caution": "Ferramentas pré-aprovadas ignoram a revisão humana. Ative apenas ferramentas confiáveis.", + "description": "Escolha quais ferramentas podem ser executadas sem aprovação manual.", + "requiresPermission": "Requer permissão quando não pré-aprovado.", + "tab": "Ferramentas pré-aprovadas", + "title": "Ferramentas pré-aprovadas", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Tipo de Agente", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 78a9ffab50..02b11d9284 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -47,8 +47,8 @@ "allowed_tools": { "empty": "Для этого агента нет доступных инструментов.", "helper": "Выберите инструменты с предварительным допуском. Неотмеченные инструменты потребуют подтверждения перед использованием.", - "label": "Разрешённые инструменты", - "placeholder": "Выберите разрешённые инструменты" + "label": "Предварительно одобренные инструменты", + "placeholder": "Выберите предварительно одобренные инструменты" }, "create": { "error": { @@ -80,8 +80,37 @@ } }, "settings": { + "advance": { + "maxTurns": { + "description": "Установить количество циклов запрос/ответ, выполняемых автоматически через прокси.", + "helper": "Чем выше значение, тем дольше может работать автономно; чем ниже значение, тем легче контролировать.", + "label": "Максимальное количество раундов в сеансе" + }, + "permissionMode": { + "description": "Как агент управления обрабатывает ситуации, требующие авторизации.", + "label": "Режим разрешений", + "options": { + "acceptEdits": "Автоматически принимать правки", + "bypassPermissions": "Пропустить проверку разрешений", + "default": "По умолчанию (спросить перед продолжением)", + "plan": "Режим планирования (требуется утверждение плана)" + }, + "placeholder": "Выбрать режим разрешений" + }, + "title": "Расширенные настройки" + }, "essential": "Основные настройки", - "prompt": "Настройки подсказки" + "mcps": "MCP сервер", + "prompt": "Настройки подсказки", + "tools": { + "approved": "одобрено", + "caution": "Предварительно одобренные инструменты обходят проверку человеком. Включайте только доверенные инструменты.", + "description": "Выберите, какие инструменты могут запускаться без ручного подтверждения.", + "requiresPermission": "Требуется разрешение, если не предварительно одобрено.", + "tab": "Предварительно одобренные инструменты", + "title": "Предварительно одобренные инструменты", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Тип агента", diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentAdvanceSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentAdvanceSettings.tsx new file mode 100644 index 0000000000..0e0c4a5583 --- /dev/null +++ b/src/renderer/src/pages/settings/AgentSettings/AgentAdvanceSettings.tsx @@ -0,0 +1,162 @@ +import { Input, Select, SelectItem, Tooltip } from '@heroui/react' +import type { Selection } from '@react-types/shared' +import { + AgentConfiguration, + AgentConfigurationSchema, + GetAgentResponse, + PermissionMode, + PermissionModeSchema, + UpdateAgentForm +} from '@renderer/types' +import { Info } from 'lucide-react' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { SettingsContainer, SettingsItem, SettingsTitle } from './shared' + +const permissionModeKeyMap: Record = { + default: 'agent.settings.advance.permissionMode.options.default', + acceptEdits: 'agent.settings.advance.permissionMode.options.acceptEdits', + bypassPermissions: 'agent.settings.advance.permissionMode.options.bypassPermissions', + plan: 'agent.settings.advance.permissionMode.options.plan' +} + +const permissionModeFallback: Record = { + default: 'Default (ask before continuing)', + acceptEdits: 'Accept edits automatically', + bypassPermissions: 'Bypass permission checks', + plan: 'Planning mode (requires plan approval)' +} + +type AgentConfigurationState = AgentConfiguration & Record + +interface AgentAdvanceSettingsProps { + agent: GetAgentResponse | undefined | null + updateAgent: (form: UpdateAgentForm) => Promise | void +} + +const defaultConfiguration = AgentConfigurationSchema.parse({}) as AgentConfigurationState + +export const AgentAdvanceSettings: React.FC = ({ agent, updateAgent }) => { + const { t } = useTranslation() + const [configuration, setConfiguration] = useState(defaultConfiguration) + const [maxTurnsInput, setMaxTurnsInput] = useState(String(defaultConfiguration.max_turns)) + + useEffect(() => { + if (!agent) { + setConfiguration(defaultConfiguration) + setMaxTurnsInput(String(defaultConfiguration.max_turns)) + return + } + const parsed = AgentConfigurationSchema.parse(agent.configuration ?? {}) as AgentConfigurationState + setConfiguration(parsed) + setMaxTurnsInput(String(parsed.max_turns)) + }, [agent]) + + const permissionOptions = useMemo( + () => + PermissionModeSchema.options.map((mode) => ({ + key: mode, + label: t(permissionModeKeyMap[mode], permissionModeFallback[mode]) + })) satisfies { key: PermissionMode; label: string }[], + [t] + ) + + const handlePermissionChange = useCallback( + (keys: Selection) => { + if (!agent || keys === 'all') return + const [first] = Array.from(keys) + if (!first) return + const nextMode = first as PermissionMode + setConfiguration((prev) => { + if (prev.permission_mode === nextMode) { + return prev + } + const next = { ...prev, permission_mode: nextMode } as AgentConfigurationState + updateAgent({ id: agent.id, configuration: next } satisfies UpdateAgentForm) + return next + }) + }, + [agent, updateAgent] + ) + + const commitMaxTurns = useCallback(() => { + if (!agent) return + const parsedValue = Number.parseInt(maxTurnsInput, 10) + if (!Number.isFinite(parsedValue)) { + setMaxTurnsInput(String(configuration.max_turns)) + return + } + const sanitized = Math.max(1, parsedValue) + if (sanitized === configuration.max_turns) { + setMaxTurnsInput(String(configuration.max_turns)) + return + } + const next = { ...configuration, max_turns: sanitized } as AgentConfigurationState + setConfiguration(next) + setMaxTurnsInput(String(sanitized)) + updateAgent({ id: agent.id, configuration: next } satisfies UpdateAgentForm) + }, [agent, configuration, maxTurnsInput, updateAgent]) + + if (!agent) { + return null + } + + return ( + + + + + + }> + {t('agent.settings.advance.permissionMode.label')} + + + + + + + + }> + {t('agent.settings.advance.maxTurns.label')} + +
+ { + if (event.key === 'Enter') { + commitMaxTurns() + } + }} + aria-label={t('agent.settings.advance.maxTurns.label')} + /> + + {t('agent.settings.advance.maxTurns.helper')} + +
+
+
+ ) +} + +export default AgentAdvanceSettings diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx index 7df39a7a93..283857fa04 100644 --- a/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx @@ -1,7 +1,5 @@ import { Button, Tooltip } from '@heroui/react' import { loggerService } from '@logger' -import type { Selection } from '@react-types/shared' -import { AllowedToolsSelect } from '@renderer/components/agent' import { ApiModelLabel } from '@renderer/components/ApiModelLabel' import { useApiModels } from '@renderer/hooks/agents/useModels' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' @@ -9,7 +7,7 @@ import { GetAgentResponse, UpdateAgentForm } from '@renderer/types' import { Input, Select } from 'antd' import { DefaultOptionType } from 'antd/es/select' import { Plus } from 'lucide-react' -import { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { FC, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared' @@ -25,9 +23,6 @@ const AgentEssentialSettings: FC = ({ agent, update const { t } = useTranslation() const [name, setName] = useState((agent?.name ?? '').trim()) const { models } = useApiModels({ providerType: 'anthropic' }) - const availableTools = useMemo(() => agent?.tools ?? [], [agent?.tools]) - const [allowedToolIds, setAllowedToolIds] = useState([]) - const selectedToolKeys = useMemo(() => new Set(allowedToolIds), [allowedToolIds]) const updateName = (name: string) => { if (!agent) return @@ -47,14 +42,6 @@ const AgentEssentialSettings: FC = ({ agent, update [agent, update] ) - const updateAllowedTools = useCallback( - (allowed_tools: UpdateAgentForm['allowed_tools']) => { - if (!agent) return - update({ id: agent.id, allowed_tools }) - }, - [agent, update] - ) - const modelOptions = useMemo(() => { return models.map((model) => ({ value: model.id, @@ -62,27 +49,6 @@ const AgentEssentialSettings: FC = ({ agent, update })) satisfies DefaultOptionType[] }, [models]) - useEffect(() => { - if (!agent) { - setAllowedToolIds((prev) => (prev.length === 0 ? prev : [])) - return - } - - const allowed = agent.allowed_tools ?? [] - const filtered = availableTools.length - ? allowed.filter((id) => availableTools.some((tool) => tool.id === id)) - : allowed - - setAllowedToolIds((prev) => { - const prevSet = new Set(prev) - const isSame = filtered.length === prevSet.size && filtered.every((id) => prevSet.has(id)) - if (isSame) { - return prev - } - return filtered - }) - }, [agent, availableTools]) - const addAccessiblePath = useCallback(async () => { if (!agent) return @@ -117,40 +83,6 @@ const AgentEssentialSettings: FC = ({ agent, update [agent, t, updateAccessiblePaths] ) - const onAllowedToolsChange = useCallback( - (keys: Selection) => { - if (!agent) return - - const nextIds = keys === 'all' ? availableTools.map((tool) => tool.id) : Array.from(keys).map(String) - const filtered = availableTools.length - ? nextIds.filter((id) => availableTools.some((tool) => tool.id === id)) - : nextIds - - setAllowedToolIds((prev) => { - const prevSet = new Set(prev) - const isSame = filtered.length === prevSet.size && filtered.every((id) => prevSet.has(id)) - if (isSame) { - return prev - } - return filtered - }) - }, - [agent, availableTools] - ) - - const onAllowedToolsSelected = useCallback(() => { - if (!agent) return - const previous = agent.allowed_tools ?? [] - const previousSet = new Set(previous) - const isSameSelection = - allowedToolIds.length === previousSet.size && allowedToolIds.every((id) => previousSet.has(id)) - - if (isSameSelection) { - return - } - updateAllowedTools(allowedToolIds) - }, [agent, allowedToolIds, updateAllowedTools]) - if (!agent) return null return ( @@ -185,15 +117,6 @@ const AgentEssentialSettings: FC = ({ agent, update placeholder={t('common.placeholders.select.model')} /> - - {t('agent.session.allowed_tools.label')} - - Promise | void +} + +export const AgentMCPSettings: React.FC = ({ agent, updateAgent }) => { + const { t } = useTranslation() + const { mcpServers: allMcpServers } = useMCPServers() + const [selectedIds, setSelectedIds] = useState([]) + + const availableServers = useMemo(() => allMcpServers ?? [], [allMcpServers]) + + useEffect(() => { + if (!agent) { + setSelectedIds([]) + return + } + const mcps = agent.mcps ?? [] + const validIds = mcps.filter((id) => availableServers.some((server) => server.id === id)) + setSelectedIds((prev) => { + if (prev.length === validIds.length && prev.every((id) => validIds.includes(id))) { + return prev + } + return validIds + }) + }, [agent, availableServers]) + + const handleToggle = useCallback( + (serverId: string, isEnabled: boolean) => { + if (!agent) return + + setSelectedIds((prev) => { + const exists = prev.includes(serverId) + if (isEnabled === exists) { + return prev + } + const next = isEnabled ? [...prev, serverId] : prev.filter((id) => id !== serverId) + updateAgent({ id: agent.id, mcps: next }) + return next + }) + }, + [agent, updateAgent] + ) + + const enabledCount = useMemo(() => { + const validSelected = selectedIds.filter((id) => availableServers.some((server) => server.id === id)) + return validSelected.length + }, [selectedIds, availableServers]) + + const renderServerMeta = useCallback((meta?: ReactNode) => { + if (!meta) return null + return {meta} + }, []) + + if (!agent) { + return null + } + + return ( + + +
+
+ + {t('assistants.settings.mcp.title')} + + + + + {availableServers.length > 0 ? ( + + {enabledCount} / {availableServers.length} {t('settings.mcp.active')} + + ) : null} +
+ + {availableServers.length > 0 ? ( +
+ {availableServers.map((server) => { + const isSelected = selectedIds.includes(server.id) + return ( + + +
+ {server.name} + {server.description ? ( + {server.description} + ) : null} +
+ handleToggle(server.id, value)} + /> +
+ + {renderServerMeta(server.baseUrl)} + {renderServerMeta(server.provider)} + +
+ ) + })} +
+ ) : ( +
+ {t('assistants.settings.mcp.noServersAvailable', 'No MCP servers available')} +
+ )} +
+
+
+ ) +} + +export default AgentMCPSettings diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentToolSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentToolSettings.tsx new file mode 100644 index 0000000000..fdd4f0cce0 --- /dev/null +++ b/src/renderer/src/pages/settings/AgentSettings/AgentToolSettings.tsx @@ -0,0 +1,149 @@ +import { Alert, Card, CardBody, CardHeader, Switch, Tooltip } from '@heroui/react' +import { GetAgentResponse, Tool, UpdateAgentForm } from '@renderer/types' +import { Info } from 'lucide-react' +import { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import { SettingsContainer, SettingsItem, SettingsTitle } from './shared' + +interface AgentToolSettingsProps { + agent: GetAgentResponse | undefined | null + updateAgent: (form: UpdateAgentForm) => Promise | void +} + +const isSameSelection = (next: string[], previous: string[]) => { + if (next.length !== previous.length) { + return false + } + const previousSet = new Set(previous) + return next.every((id) => previousSet.has(id)) +} + +export const AgentToolSettings: FC = ({ agent, updateAgent }) => { + const { t } = useTranslation() + const [approvedIds, setApprovedIds] = useState([]) + + const availableTools = useMemo(() => agent?.tools ?? [], [agent?.tools]) + + useEffect(() => { + if (!agent) { + setApprovedIds((prev) => (prev.length === 0 ? prev : [])) + return + } + const allowed = agent.allowed_tools ?? [] + const validIds = allowed.filter((id) => availableTools.some((tool) => tool.id === id)) + setApprovedIds((prev) => { + if (isSameSelection(prev, validIds)) { + return prev + } + return validIds + }) + }, [agent, availableTools]) + + const handleToggle = useCallback( + (toolId: string, isApproved: boolean) => { + if (!agent) return + + setApprovedIds((prev) => { + const exists = prev.includes(toolId) + if (isApproved === exists) { + return prev + } + const next = isApproved ? [...prev, toolId] : prev.filter((id) => id !== toolId) + const previous = agent.allowed_tools ?? [] + if (!isSameSelection(next, previous)) { + updateAgent({ id: agent.id, allowed_tools: next }) + } + return next + }) + }, + [agent, updateAgent] + ) + + const approvedCount = useMemo(() => { + return approvedIds.filter((id) => availableTools.some((tool) => tool.id === id)).length + }, [approvedIds, availableTools]) + + if (!agent) { + return null + } + + return ( + + +
+
+ + {t('agent.settings.tools.title', 'Pre-approved tools')} + + + + + {availableTools.length > 0 ? ( + + {approvedCount} / {availableTools.length} {t('agent.settings.tools.approved', 'approved')} + + ) : null} +
+ + + + {availableTools.length > 0 ? ( +
+ {availableTools.map((tool) => { + const isApproved = approvedIds.includes(tool.id) + return ( + + +
+ {tool.name} + {tool.description ? ( + {tool.description} + ) : null} +
+ handleToggle(tool.id, value)} + /> +
+ {tool.requirePermissions ? ( + + + {t( + 'agent.settings.tools.requiresPermission', + 'Requires permission when not pre-approved.' + )} + + + ) : null} +
+ ) + })} +
+ ) : ( +
+ {t('agent.session.allowed_tools.empty')} +
+ )} +
+
+
+ ) +} + +export default AgentToolSettings diff --git a/src/renderer/src/pages/settings/AgentSettings/index.tsx b/src/renderer/src/pages/settings/AgentSettings/index.tsx index 525eb94675..a23b14a7e0 100644 --- a/src/renderer/src/pages/settings/AgentSettings/index.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/index.tsx @@ -7,8 +7,11 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import AgentAdvanceSettings from './AgentAdvanceSettings' import AgentEssentialSettings from './AgentEssentialSettings' +import AgentMCPSettings from './AgentMCPSettings' import AgentPromptSettings from './AgentPromptSettings' +import AgentToolSettings from './AgentToolSettings' import { AgentLabel } from './shared' interface AgentSettingPopupShowParams { @@ -20,7 +23,7 @@ interface AgentSettingPopupParams extends AgentSettingPopupShowParams { resolve: () => void } -type AgentSettingPopupTab = 'essential' | 'prompt' +type AgentSettingPopupTab = 'essential' | 'prompt' | 'tools' | 'mcps' | 'advance' | 'session-mcps' const AgentSettingPopupContainer: React.FC = ({ tab, agentId, resolve }) => { const [open, setOpen] = useState(true) @@ -51,6 +54,18 @@ const AgentSettingPopupContainer: React.FC = ({ tab, ag { key: 'prompt', label: t('agent.settings.prompt') + }, + { + key: 'tools', + label: t('agent.settings.tools.tab', 'Pre-approved tools') + }, + { + key: 'mcps', + label: t('agent.settings.mcps', 'MCP Servers') + }, + { + key: 'advance', + label: t('agent.settings.advance.title', 'Advanced Settings') } ] as const satisfies { key: AgentSettingPopupTab; label: string }[] ).filter(Boolean) @@ -81,6 +96,9 @@ const AgentSettingPopupContainer: React.FC = ({ tab, ag {menu === 'essential' && } {menu === 'prompt' && } + {menu === 'tools' && } + {menu === 'mcps' && } + {menu === 'advance' && } ) diff --git a/src/renderer/src/types/agent.ts b/src/renderer/src/types/agent.ts index bf414c785e..9a7172fb23 100644 --- a/src/renderer/src/types/agent.ts +++ b/src/renderer/src/types/agent.ts @@ -156,8 +156,6 @@ export interface AgentMessagePersistExchangeResult { // Not implemented fields: // - plan_model: Optional model for planning/thinking tasks // - small_model: Optional lightweight model for quick responses -// - mcps: Optional array of MCP (Model Control Protocol) tool IDs -// - allowed_tools: Optional array of permitted tool IDs // - configuration: Optional agent settings (temperature, top_p, etc.) // ------------------ Form models ------------------ export type BaseAgentForm = { @@ -170,6 +168,8 @@ export type BaseAgentForm = { model: string accessible_paths: string[] allowed_tools: string[] + mcps?: string[] + configuration?: AgentConfiguration } export type AddAgentForm = Omit & { id?: never }