mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 18:50:56 +08:00
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:
parent
e0a2ed0481
commit
1575e97168
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user