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/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index f78d8abd3c..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": { @@ -101,7 +101,16 @@ }, "essential": "Essential Settings", "mcps": "MCP Servers", - "prompt": "Prompt Settings" + "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 76781468e1..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": { @@ -101,7 +101,16 @@ }, "essential": "基础设置", "mcps": "MCP 服务器", - "prompt": "提示词设置" + "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 bbcef66ad7..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": { @@ -101,7 +101,16 @@ }, "essential": "必要設定", "mcps": "MCP 伺服器", - "prompt": "提示設定" + "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 51e2e7df2c..982956f49c 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": "[to be translated]:Pre-approved tools", + "placeholder": "[to be translated]:Select pre-approved tools" }, "create": { "error": { @@ -101,7 +101,16 @@ }, "essential": "Βασικές Ρυθμίσεις", "mcps": "[to be translated]:MCP 服务器", - "prompt": "Ρυθμίσεις Προτροπής" + "prompt": "Ρυθμίσεις Προτροπής", + "tools": { + "approved": "[to be translated]:approved", + "caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", + "description": "[to be translated]:Choose which tools can run without manual approval.", + "requiresPermission": "[to be translated]:Requires permission when not pre-approved.", + "tab": "[to be translated]:Pre-approved tools", + "title": "[to be translated]:Pre-approved tools", + "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 91c125a5e6..f0f1ec3b5e 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": "[to be translated]:Pre-approved tools", + "placeholder": "[to be translated]:Select pre-approved tools" }, "create": { "error": { @@ -101,7 +101,16 @@ }, "essential": "Configuraciones esenciales", "mcps": "[to be translated]:MCP 服务器", - "prompt": "Configuración de indicaciones" + "prompt": "Configuración de indicaciones", + "tools": { + "approved": "[to be translated]:approved", + "caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", + "description": "[to be translated]:Choose which tools can run without manual approval.", + "requiresPermission": "[to be translated]:Requires permission when not pre-approved.", + "tab": "[to be translated]:Pre-approved tools", + "title": "[to be translated]:Pre-approved tools", + "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 119344967d..7fa0ed81ca 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": "[to be translated]:Pre-approved tools", + "placeholder": "[to be translated]:Select pre-approved tools" }, "create": { "error": { @@ -101,7 +101,16 @@ }, "essential": "Paramètres essentiels", "mcps": "[to be translated]:MCP 服务器", - "prompt": "Paramètres de l'invite" + "prompt": "Paramètres de l'invite", + "tools": { + "approved": "[to be translated]:approved", + "caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", + "description": "[to be translated]:Choose which tools can run without manual approval.", + "requiresPermission": "[to be translated]:Requires permission when not pre-approved.", + "tab": "[to be translated]:Pre-approved tools", + "title": "[to be translated]:Pre-approved tools", + "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 4322ff6cc2..375326d047 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": { @@ -101,7 +101,16 @@ }, "essential": "必須設定", "mcps": "[to be translated]:MCP 服务器", - "prompt": "プロンプト設定" + "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 55670d7578..57fd837c40 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": "[to be translated]:Pre-approved tools", + "placeholder": "[to be translated]:Select pre-approved tools" }, "create": { "error": { @@ -101,7 +101,16 @@ }, "essential": "Configurações Essenciais", "mcps": "[to be translated]:MCP 服务器", - "prompt": "Configurações de Prompt" + "prompt": "Configurações de Prompt", + "tools": { + "approved": "[to be translated]:approved", + "caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", + "description": "[to be translated]:Choose which tools can run without manual approval.", + "requiresPermission": "[to be translated]:Requires permission when not pre-approved.", + "tab": "[to be translated]:Pre-approved tools", + "title": "[to be translated]:Pre-approved tools", + "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 9604315c62..175c54b585 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": "[to be translated]:Pre-approved tools", + "placeholder": "[to be translated]:Select pre-approved tools" }, "create": { "error": { @@ -101,7 +101,16 @@ }, "essential": "Основные настройки", "mcps": "[to be translated]:MCP 服务器", - "prompt": "Настройки подсказки" + "prompt": "Настройки подсказки", + "tools": { + "approved": "[to be translated]:approved", + "caution": "[to be translated]:Pre-approved tools bypass human review. Enable only trusted tools.", + "description": "[to be translated]:Choose which tools can run without manual approval.", + "requiresPermission": "[to be translated]:Requires permission when not pre-approved.", + "tab": "[to be translated]:Pre-approved tools", + "title": "[to be translated]:Pre-approved tools", + "toggle": "{{defaultValue}}" + } }, "type": { "label": "Тип агента", diff --git a/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx b/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx index f6cdb5704b..4373f0205f 100644 --- a/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/AgentEssentialSettings.tsx @@ -1,14 +1,13 @@ -import { Button, Chip, Select as HeroSelect, SelectedItems, SelectItem, Tooltip } from '@heroui/react' +import { Button, Tooltip } from '@heroui/react' import { loggerService } from '@logger' -import type { Selection } from '@react-types/shared' import { ApiModelLabel } from '@renderer/components/ApiModelLabel' import { useApiModels } from '@renderer/hooks/agents/useModels' import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent' -import { GetAgentResponse, Tool, UpdateAgentForm } from '@renderer/types' +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' @@ -24,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 @@ -46,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, @@ -61,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 @@ -116,52 +83,6 @@ const AgentEssentialSettings: FC = ({ agent, update [agent, t, updateAccessiblePaths] ) - const renderSelectedTools = useCallback((items: SelectedItems) => { - if (!items.length) { - return null - } - - return ( -
- {items.map((item) => ( - - {item.data?.name ?? item.textValue ?? item.key} - - ))} -
- ) - }, []) - - 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 - }) - - const previous = agent.allowed_tools ?? [] - const previousSet = new Set(previous) - const isSameSelection = filtered.length === previousSet.size && filtered.every((id) => previousSet.has(id)) - - if (isSameSelection) { - return - } - - updateAllowedTools(filtered) - }, - [agent, availableTools, updateAllowedTools] - ) if (!agent) return null @@ -197,31 +118,6 @@ const AgentEssentialSettings: FC = ({ agent, update placeholder={t('common.placeholders.select.model')} /> - - {t('agent.session.allowed_tools.label')} - - {(tool) => ( - -
- {tool.name} - {tool.description ? {tool.description} : null} -
-
- )} -
-
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 a117573825..a23b14a7e0 100644 --- a/src/renderer/src/pages/settings/AgentSettings/index.tsx +++ b/src/renderer/src/pages/settings/AgentSettings/index.tsx @@ -11,6 +11,7 @@ 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 { @@ -22,7 +23,7 @@ interface AgentSettingPopupParams extends AgentSettingPopupShowParams { resolve: () => void } -type AgentSettingPopupTab = 'essential' | 'prompt' | 'mcps' | 'advance' | 'session-mcps' +type AgentSettingPopupTab = 'essential' | 'prompt' | 'tools' | 'mcps' | 'advance' | 'session-mcps' const AgentSettingPopupContainer: React.FC = ({ tab, agentId, resolve }) => { const [open, setOpen] = useState(true) @@ -54,6 +55,10 @@ 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') @@ -91,6 +96,7 @@ const AgentSettingPopupContainer: React.FC = ({ tab, ag {menu === 'essential' && } {menu === 'prompt' && } + {menu === 'tools' && } {menu === 'mcps' && } {menu === 'advance' && }