Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new

This commit is contained in:
icarus 2025-09-23 11:15:44 +08:00
commit 164386a337
19 changed files with 771 additions and 118 deletions

View File

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

View File

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

View File

@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ agent, trigger, isOpen: _isOpen, o
form.instructions,
form.accessible_paths,
form.allowed_tools,
form.configuration,
agent,
onClose,
t,

View File

@ -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<Props> = ({
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<Props> = ({
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<Props> = ({
form.instructions,
form.accessible_paths,
form.allowed_tools,
form.mcps,
session,
onClose,
onSessionCreated,

View File

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

View File

@ -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": "智能体类型",

View File

@ -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": "代理類型",

View File

@ -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": "Τύπος Πράκτορα",

View File

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

View File

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

View File

@ -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": "エージェントタイプ",

View File

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

View File

@ -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": "Тип агента",

View File

@ -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<PermissionMode, string> = {
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<PermissionMode, string> = {
default: 'Default (ask before continuing)',
acceptEdits: 'Accept edits automatically',
bypassPermissions: 'Bypass permission checks',
plan: 'Planning mode (requires plan approval)'
}
type AgentConfigurationState = AgentConfiguration & Record<string, unknown>
interface AgentAdvanceSettingsProps {
agent: GetAgentResponse | undefined | null
updateAgent: (form: UpdateAgentForm) => Promise<void> | void
}
const defaultConfiguration = AgentConfigurationSchema.parse({}) as AgentConfigurationState
export const AgentAdvanceSettings: React.FC<AgentAdvanceSettingsProps> = ({ agent, updateAgent }) => {
const { t } = useTranslation()
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
const [maxTurnsInput, setMaxTurnsInput] = useState<string>(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 (
<SettingsContainer>
<SettingsItem>
<SettingsTitle
actions={
<Tooltip content={t('agent.settings.advance.permissionMode.description')} placement="right">
<Info size={16} className="text-foreground-400" />
</Tooltip>
}>
{t('agent.settings.advance.permissionMode.label')}
</SettingsTitle>
<Select
aria-label={t('agent.settings.advance.permissionMode.label')}
selectionMode="single"
selectedKeys={[configuration.permission_mode]}
onSelectionChange={handlePermissionChange}
className="max-w-md"
placeholder={t('agent.settings.advance.permissionMode.placeholder')}>
{permissionOptions.map((option) => (
<SelectItem key={option.key} textValue={option.label}>
{option.label}
</SelectItem>
))}
</Select>
</SettingsItem>
<SettingsItem divider={false}>
<SettingsTitle
actions={
<Tooltip content={t('agent.settings.advance.maxTurns.description')} placement="right">
<Info size={16} className="text-foreground-400" />
</Tooltip>
}>
{t('agent.settings.advance.maxTurns.label')}
</SettingsTitle>
<div className="flex max-w-md flex-col gap-2">
<Input
type="number"
min={1}
value={maxTurnsInput}
onValueChange={setMaxTurnsInput}
onBlur={commitMaxTurns}
onKeyDown={(event) => {
if (event.key === 'Enter') {
commitMaxTurns()
}
}}
aria-label={t('agent.settings.advance.maxTurns.label')}
/>
<span className="text-foreground-500 text-xs">
{t('agent.settings.advance.maxTurns.helper')}
</span>
</div>
</SettingsItem>
</SettingsContainer>
)
}
export default AgentAdvanceSettings

View File

@ -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<AgentEssentialSettingsProps> = ({ agent, update
const { t } = useTranslation()
const [name, setName] = useState<string>((agent?.name ?? '').trim())
const { models } = useApiModels({ providerType: 'anthropic' })
const availableTools = useMemo(() => agent?.tools ?? [], [agent?.tools])
const [allowedToolIds, setAllowedToolIds] = useState<string[]>([])
const selectedToolKeys = useMemo<Selection>(() => new Set<string>(allowedToolIds), [allowedToolIds])
const updateName = (name: string) => {
if (!agent) return
@ -47,14 +42,6 @@ const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ 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<AgentEssentialSettingsProps> = ({ 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<AgentEssentialSettingsProps> = ({ 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<AgentEssentialSettingsProps> = ({ agent, update
placeholder={t('common.placeholders.select.model')}
/>
</SettingsItem>
<SettingsItem>
<SettingsTitle>{t('agent.session.allowed_tools.label')}</SettingsTitle>
<AllowedToolsSelect
items={availableTools}
selectedKeys={selectedToolKeys}
onSelectionChange={onAllowedToolsChange}
onClose={onAllowedToolsSelected}
/>
</SettingsItem>
<SettingsItem>
<SettingsTitle
actions={

View File

@ -0,0 +1,130 @@
import { Card, CardBody, CardHeader, Switch, Tooltip } from '@heroui/react'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { GetAgentResponse, UpdateAgentForm } from '@renderer/types'
import { Info } from 'lucide-react'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
interface AgentMCPSettingsProps {
agent: GetAgentResponse | undefined | null
updateAgent: (form: UpdateAgentForm) => Promise<void> | void
}
export const AgentMCPSettings: React.FC<AgentMCPSettingsProps> = ({ agent, updateAgent }) => {
const { t } = useTranslation()
const { mcpServers: allMcpServers } = useMCPServers()
const [selectedIds, setSelectedIds] = useState<string[]>([])
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 <span className="text-foreground-400 text-xs">{meta}</span>
}, [])
if (!agent) {
return null
}
return (
<SettingsContainer>
<SettingsItem divider={false} className="flex-1">
<div className="flex h-full flex-col gap-4">
<div className="flex items-center justify-between">
<SettingsTitle>
{t('assistants.settings.mcp.title')}
<Tooltip
placement="right"
content={t('assistants.settings.mcp.description', 'Select MCP servers to use with this agent')}>
<Info size={16} className="text-foreground-400" />
</Tooltip>
</SettingsTitle>
{availableServers.length > 0 ? (
<span className="text-foreground-500 text-xs">
{enabledCount} / {availableServers.length} {t('settings.mcp.active')}
</span>
) : null}
</div>
{availableServers.length > 0 ? (
<div className="flex flex-1 flex-col gap-3 overflow-auto pr-1">
{availableServers.map((server) => {
const isSelected = selectedIds.includes(server.id)
return (
<Card key={server.id} shadow="none" className="border border-default-200">
<CardHeader className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="truncate font-medium text-sm">{server.name}</span>
{server.description ? (
<span className="line-clamp-2 text-foreground-500 text-xs">{server.description}</span>
) : null}
</div>
<Switch
aria-label={t('assistants.settings.mcp.toggle', {
defaultValue: `Toggle ${server.name}`
})}
isSelected={isSelected}
isDisabled={!server.isActive}
size="sm"
onValueChange={(value) => handleToggle(server.id, value)}
/>
</CardHeader>
<CardBody className="gap-1 py-0 pb-3">
{renderServerMeta(server.baseUrl)}
{renderServerMeta(server.provider)}
</CardBody>
</Card>
)
})}
</div>
) : (
<div className="flex flex-1 items-center justify-center rounded-medium border border-default-200 border-dashed px-4 py-10 text-foreground-500 text-sm">
{t('assistants.settings.mcp.noServersAvailable', 'No MCP servers available')}
</div>
)}
</div>
</SettingsItem>
</SettingsContainer>
)
}
export default AgentMCPSettings

View File

@ -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> | 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<AgentToolSettingsProps> = ({ agent, updateAgent }) => {
const { t } = useTranslation()
const [approvedIds, setApprovedIds] = useState<string[]>([])
const availableTools = useMemo<Tool[]>(() => 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 (
<SettingsContainer>
<SettingsItem divider={false} className="flex-1">
<div className="flex h-full flex-col gap-4">
<div className="flex items-center justify-between">
<SettingsTitle>
{t('agent.settings.tools.title', 'Pre-approved tools')}
<Tooltip
placement="right"
content={t(
'agent.settings.tools.description',
'Choose which tools can run without manual approval.'
)}>
<Info size={16} className="text-foreground-400" />
</Tooltip>
</SettingsTitle>
{availableTools.length > 0 ? (
<span className="text-foreground-500 text-xs">
{approvedCount} / {availableTools.length} {t('agent.settings.tools.approved', 'approved')}
</span>
) : null}
</div>
<Alert
color="warning"
title={t(
'agent.settings.tools.caution',
'Pre-approved tools bypass human review. Enable only trusted tools.'
)}
/>
{availableTools.length > 0 ? (
<div className="flex flex-1 flex-col gap-3 overflow-auto pr-1">
{availableTools.map((tool) => {
const isApproved = approvedIds.includes(tool.id)
return (
<Card key={tool.id} shadow="none" className="border border-default-200">
<CardHeader className="flex items-start justify-between gap-3">
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="truncate font-medium text-sm">{tool.name}</span>
{tool.description ? (
<span className="line-clamp-2 text-foreground-500 text-xs">{tool.description}</span>
) : null}
</div>
<Switch
aria-label={t('agent.settings.tools.toggle', {
defaultValue: `Toggle ${tool.name}`
})}
isSelected={isApproved}
size="sm"
onValueChange={(value) => handleToggle(tool.id, value)}
/>
</CardHeader>
{tool.requirePermissions ? (
<CardBody className="py-0 pb-3">
<span className="text-foreground-400 text-xs">
{t(
'agent.settings.tools.requiresPermission',
'Requires permission when not pre-approved.'
)}
</span>
</CardBody>
) : null}
</Card>
)
})}
</div>
) : (
<div className="flex flex-1 items-center justify-center rounded-medium border border-default-200 border-dashed px-4 py-10 text-foreground-500 text-sm">
{t('agent.session.allowed_tools.empty')}
</div>
)}
</div>
</SettingsItem>
</SettingsContainer>
)
}
export default AgentToolSettings

View File

@ -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<AgentSettingPopupParams> = ({ tab, agentId, resolve }) => {
const [open, setOpen] = useState(true)
@ -51,6 +54,18 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ 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<AgentSettingPopupParams> = ({ tab, ag
<Settings>
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
{menu === 'prompt' && <AgentPromptSettings agent={agent} update={updateAgent} />}
{menu === 'tools' && <AgentToolSettings agent={agent} updateAgent={updateAgent} />}
{menu === 'mcps' && <AgentMCPSettings agent={agent} updateAgent={updateAgent} />}
{menu === 'advance' && <AgentAdvanceSettings agent={agent} updateAgent={updateAgent} />}
</Settings>
</div>
)

View File

@ -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<BaseAgentForm, 'id'> & { id?: never }