fix: approved tools (#11025)

* refactor(agent): move permission mode types and constants to config

Move PermissionModeCard type definition to types/agent.ts and relocate permissionModeCards constant from constants/permissionModes.ts to config/agent.ts for better organization and maintainability

* refactor(AgentSettings): simplify state management in ToolingSettings

remove redundant state for selectedMode and derive it from configuration
consolidate permission mode constants import path

* docs(AgentSettings): add jsdoc for computeModeDefaults function

* refactor(AgentSettings): simplify tooling state management with useMemo

remove redundant state for autoToolIds and compute it directly using useMemo

* refactor(AgentSettings): simplify tool approval state management

- Replace useState with useMemo for approvedToolIds to prevent unnecessary state updates
- Remove redundant state transitions and simplify toggle logic
- Ensure consistent tool filtering and merging with defaults

* refactor(AgentSettings): replace useState with useMemo for configuration state

Optimize performance by memoizing agent configuration state to prevent unnecessary re-renders

* perf(AgentSettings): optimize permission_mode computation with useMemo

Prevent unnecessary recalculations of permission_mode by memoizing the value

* refactor(AgentSettings): simplify MCP selection logic and remove unused imports

Remove useEffect for MCP state synchronization and directly use memoized value
Clean up unused imports and simplify toggle handler logic

* refactor: remove unused useAgentClient hook from ToolingSettings
This commit is contained in:
Phantom 2025-10-29 16:21:29 +08:00 committed by GitHub
parent e0a2ed0481
commit 1575e97168
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 114 additions and 142 deletions

View File

@ -16,8 +16,8 @@ import {
import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared'
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { permissionModeCards } from '@renderer/config/agent'
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useApiModels } from '@renderer/hooks/agents/useModels'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'

View File

@ -1,5 +1,6 @@
import ClaudeAvatar from '@renderer/assets/images/models/claude.png'
import { AgentBase, AgentType } from '@renderer/types'
import { PermissionModeCard } from '@renderer/types/agent'
// base agent config. no default config for now.
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
@ -19,3 +20,47 @@ export const getAgentTypeAvatar = (type: AgentType): string => {
return ''
}
}
export const permissionModeCards: PermissionModeCard[] = [
{
mode: 'default',
// t('agent.settings.tooling.permissionMode.default.title')
titleKey: 'agent.settings.tooling.permissionMode.default.title',
titleFallback: 'Default (ask before continuing)',
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
behaviorFallback: 'Read-only tools are pre-approved automatically.'
},
{
mode: 'plan',
// t('agent.settings.tooling.permissionMode.plan.title')
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
titleFallback: 'Planning mode',
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
},
{
mode: 'acceptEdits',
// t('agent.settings.tooling.permissionMode.acceptEdits.title')
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
titleFallback: 'Auto-accept file edits',
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
},
{
mode: 'bypassPermissions',
// t('agent.settings.tooling.permissionMode.bypassPermissions.title')
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
titleFallback: 'Bypass permission checks',
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
descriptionFallback: 'All permission prompts are skipped — use with caution.',
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
behaviorFallback: 'Every tool is pre-approved automatically.',
caution: true
}
]

View File

@ -1,53 +0,0 @@
import { PermissionMode } from '@renderer/types'
export type PermissionModeCard = {
mode: PermissionMode
titleKey: string
titleFallback: string
descriptionKey: string
descriptionFallback: string
behaviorKey: string
behaviorFallback: string
caution?: boolean
unsupported?: boolean
}
export const permissionModeCards: PermissionModeCard[] = [
{
mode: 'default',
titleKey: 'agent.settings.tooling.permissionMode.default.title',
titleFallback: 'Default (ask before continuing)',
descriptionKey: 'agent.settings.tooling.permissionMode.default.description',
descriptionFallback: 'Read-only tools are pre-approved; everything else still needs permission.',
behaviorKey: 'agent.settings.tooling.permissionMode.default.behavior',
behaviorFallback: 'Read-only tools are pre-approved automatically.'
},
{
mode: 'plan',
titleKey: 'agent.settings.tooling.permissionMode.plan.title',
titleFallback: 'Planning mode',
descriptionKey: 'agent.settings.tooling.permissionMode.plan.description',
descriptionFallback: 'Shares the default read-only tool set but presents a plan before execution.',
behaviorKey: 'agent.settings.tooling.permissionMode.plan.behavior',
behaviorFallback: 'Read-only defaults are pre-approved while execution remains disabled.'
},
{
mode: 'acceptEdits',
titleKey: 'agent.settings.tooling.permissionMode.acceptEdits.title',
titleFallback: 'Auto-accept file edits',
descriptionKey: 'agent.settings.tooling.permissionMode.acceptEdits.description',
descriptionFallback: 'File edits and filesystem operations are automatically approved.',
behaviorKey: 'agent.settings.tooling.permissionMode.acceptEdits.behavior',
behaviorFallback: 'Pre-approves trusted filesystem tools so edits run immediately.'
},
{
mode: 'bypassPermissions',
titleKey: 'agent.settings.tooling.permissionMode.bypassPermissions.title',
titleFallback: 'Bypass permission checks',
descriptionKey: 'agent.settings.tooling.permissionMode.bypassPermissions.description',
descriptionFallback: 'All permission prompts are skipped — use with caution.',
behaviorKey: 'agent.settings.tooling.permissionMode.bypassPermissions.behavior',
behaviorFallback: 'Every tool is pre-approved automatically.',
caution: true
}
]

View File

@ -1,6 +1,5 @@
import { Alert, Card, CardBody, CardHeader, Chip, Input, Switch } from '@heroui/react'
import { permissionModeCards } from '@renderer/constants/permissionModes'
import { useAgentClient } from '@renderer/hooks/agents/useAgentClient'
import { permissionModeCards } from '@renderer/config/agent'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
import {
@ -16,9 +15,8 @@ import {
} from '@renderer/types'
import { Modal } from 'antd'
import { ShieldAlert, ShieldCheck, Wrench } from 'lucide-react'
import { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { mutate } from 'swr'
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
@ -36,6 +34,13 @@ type AgentConfigurationState = AgentConfiguration & Record<string, unknown>
const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.parse({})
/**
* Computes the list of tool IDs that should be automatically approved for a given permission mode.
*
* @param mode - The permission mode to compute defaults for.
* @param tools - The full list of available tools.
* @returns An array of tool IDs that are approved by default for the specified mode.
*/
const computeModeDefaults = (mode: PermissionMode, tools: Tool[]): string[] => {
const defaultToolIds = tools.filter((tool) => !tool.requirePermissions).map((tool) => tool.id)
switch (mode) {
@ -65,52 +70,34 @@ const unique = (values: string[]) => Array.from(new Set(values))
export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, update }) => {
const { containerRef, handleScroll } = useScrollPosition('AgentToolingSettings', 100)
const { t } = useTranslation()
const client = useAgentClient()
const { mcpServers: allServers } = useMCPServers()
const [modal, contextHolder] = Modal.useModal()
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
const [selectedMode, setSelectedMode] = useState<PermissionMode>(defaultConfiguration.permission_mode)
const [autoToolIds, setAutoToolIds] = useState<string[]>([])
const [approvedToolIds, setApprovedToolIds] = useState<string[]>([])
const configuration: AgentConfigurationState = useMemo(
() => agentBase?.configuration ?? defaultConfiguration,
[agentBase?.configuration]
)
const selectedMode = useMemo(
() => agentBase?.configuration?.permission_mode ?? defaultConfiguration.permission_mode,
[agentBase?.configuration?.permission_mode]
)
const availableTools = useMemo(() => agentBase?.tools ?? [], [agentBase?.tools])
const autoToolIds = useMemo(() => computeModeDefaults(selectedMode, availableTools), [availableTools, selectedMode])
const approvedToolIds = useMemo(() => {
const allowed = agentBase?.allowed_tools ?? []
const sanitized = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
// Ensure defaults are included even if backend omitted them
const merged = unique([...sanitized, ...autoToolIds])
return merged
}, [agentBase?.allowed_tools, autoToolIds, availableTools])
const selectedMcpIds = useMemo(() => agentBase?.mcps ?? [], [agentBase?.mcps])
const [searchTerm, setSearchTerm] = useState('')
const [isUpdatingMode, setIsUpdatingMode] = useState(false)
const [isUpdatingTools, setIsUpdatingTools] = useState(false)
const [selectedMcpIds, setSelectedMcpIds] = useState<string[]>([])
const [isUpdatingMcp, setIsUpdatingMcp] = useState(false)
const availableTools = useMemo(() => agentBase?.tools ?? [], [agentBase?.tools])
const availableServers = useMemo(() => allServers ?? [], [allServers])
useEffect(() => {
if (!agentBase) {
setConfiguration(defaultConfiguration)
setSelectedMode(defaultConfiguration.permission_mode)
setApprovedToolIds([])
setAutoToolIds([])
setSelectedMcpIds([])
return
}
const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {})
setConfiguration(parsed)
setSelectedMode(parsed.permission_mode)
const defaults = computeModeDefaults(parsed.permission_mode, availableTools)
setAutoToolIds(defaults)
const allowed = agentBase.allowed_tools ?? []
setApprovedToolIds((prev) => {
const sanitized = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
const isSame = sanitized.length === prev.length && sanitized.every((id) => prev.includes(id))
if (isSame) {
return prev
}
// Ensure defaults are included even if backend omitted them
const merged = unique([...sanitized, ...defaults])
return merged
})
setSelectedMcpIds(agentBase.mcps ?? [])
}, [agentBase, availableTools])
const filteredTools = useMemo(() => {
if (!searchTerm.trim()) {
return availableTools
@ -146,10 +133,6 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
configuration: nextConfiguration,
allowed_tools: merged
} satisfies UpdateAgentBaseForm)
setConfiguration(nextConfiguration)
setSelectedMode(nextMode)
setAutoToolIds(defaults)
setApprovedToolIds(merged)
} finally {
setIsUpdatingMode(false)
}
@ -211,33 +194,25 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
)
const handleToggleTool = useCallback(
(toolId: string, isApproved: boolean) => {
async (toolId: string, isApproved: boolean) => {
if (!agentBase || isUpdatingTools) {
return
}
startTransition(() => {
setApprovedToolIds((prev) => {
const exists = prev.includes(toolId)
if (isApproved === exists) {
return prev
}
const next = isApproved ? [...prev, toolId] : prev.filter((id) => id !== toolId)
const sanitized = unique(
next.filter((id) => availableTools.some((tool) => tool.id === id)).concat(autoToolIds)
)
setIsUpdatingTools(true)
void (async () => {
try {
await update({ id: agentBase.id, allowed_tools: sanitized } satisfies UpdateAgentBaseForm)
} finally {
setIsUpdatingTools(false)
}
})()
return sanitized
})
})
const exists = approvedToolIds.includes(toolId)
if (isApproved === exists) {
return
}
setIsUpdatingTools(true)
const next = isApproved ? [...approvedToolIds, toolId] : approvedToolIds.filter((id) => id !== toolId)
const sanitized = unique(next.filter((id) => availableTools.some((tool) => tool.id === id)).concat(autoToolIds))
try {
await update({ id: agentBase.id, allowed_tools: sanitized } satisfies UpdateAgentBaseForm)
} finally {
setIsUpdatingTools(false)
}
},
[agentBase, isUpdatingTools, availableTools, autoToolIds, update]
[agentBase, isUpdatingTools, approvedToolIds, autoToolIds, availableTools, update]
)
const { agentSummary, autoCount, customCount } = useMemo(() => {
@ -257,31 +232,24 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
}, [selectedMode, autoToolIds, userAddedIds, availableTools.length, selectedMcpIds.length])
const handleToggleMcp = useCallback(
(serverId: string, enabled: boolean) => {
async (serverId: string, enabled: boolean) => {
if (!agentBase || isUpdatingMcp) {
return
}
setSelectedMcpIds((prev) => {
const exists = prev.includes(serverId)
if (enabled === exists) {
return prev
}
const next = enabled ? [...prev, serverId] : prev.filter((id) => id !== serverId)
setIsUpdatingMcp(true)
void (async () => {
try {
await update({ id: agentBase.id, mcps: next } satisfies UpdateAgentBaseForm)
const refreshed = await client.getAgent(agentBase.id)
const key = client.agentPaths.withId(agentBase.id)
mutate(key, refreshed, false)
} finally {
setIsUpdatingMcp(false)
}
})()
return next
})
const exists = selectedMcpIds.includes(serverId)
if (enabled === exists) {
return
}
const next = enabled ? [...selectedMcpIds, serverId] : selectedMcpIds.filter((id) => id !== serverId)
setIsUpdatingMcp(true)
try {
await update({ id: agentBase.id, mcps: next } satisfies UpdateAgentBaseForm)
} finally {
setIsUpdatingMcp(false)
}
},
[agentBase, isUpdatingMcp, client, update]
[agentBase, isUpdatingMcp, selectedMcpIds, update]
)
if (!agentBase) {

View File

@ -381,3 +381,15 @@ export type ReplaceSessionRequest = z.infer<typeof ReplaceSessionRequestSchema>
export const CreateSessionMessageRequestSchema = z.object({
content: z.string().min(1, 'Content must be a valid string')
})
export type PermissionModeCard = {
mode: PermissionMode
titleKey: string
titleFallback: string
descriptionKey: string
descriptionFallback: string
behaviorKey: string
behaviorFallback: string
caution?: boolean
unsupported?: boolean
}