Refine agent permissions UI & translations

This commit is contained in:
Vaayne 2025-09-26 17:36:46 +08:00
parent 5c1ac376e6
commit fa394576bb
7 changed files with 111 additions and 124 deletions

View File

@ -27,7 +27,6 @@
"scripts": {
"start": "electron-vite preview",
"dev": "dotenv electron-vite dev",
"dev:main": "dotenv electron-vite dev --watch",
"debug": "electron-vite -- --inspect --sourcemap --remote-debugging-port=9222",
"build": "npm run typecheck && electron-vite build",
"build:check": "yarn lint && yarn test",

View File

@ -1,6 +1,5 @@
import {
Button,
Chip,
cn,
Form,
Input,
@ -11,7 +10,6 @@ import {
ModalHeader,
Select,
SelectedItemProps,
SelectedItems,
SelectItem,
Textarea,
useDisclosure
@ -20,6 +18,7 @@ import { loggerService } from '@logger'
import type { Selection } from '@react-types/shared'
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { getModelLogo } 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'
@ -30,6 +29,7 @@ import {
AgentType,
BaseAgentForm,
isAgentType,
PermissionMode,
Tool,
UpdateAgentForm
} from '@renderer/types'
@ -110,25 +110,41 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
}
}, [agent, isOpen])
const availableTools = useMemo(() => agent?.tools ?? [], [agent?.tools])
const selectedToolKeys = useMemo(() => new Set(form.allowed_tools), [form.allowed_tools])
const selectedPermissionMode = form.configuration?.permission_mode ?? 'default'
useEffect(() => {
if (!availableTools.length) {
const onPermissionModeChange = useCallback((keys: Selection) => {
if (keys === 'all') {
return
}
const [first] = Array.from(keys)
if (!first) {
return
}
setForm((prev) => {
const validTools = prev.allowed_tools.filter((id) => availableTools.some((tool) => tool.id === id))
if (validTools.length === prev.allowed_tools.length) {
const parsedConfiguration = AgentConfigurationSchema.parse(prev.configuration ?? {})
const nextMode = first as PermissionMode
if (parsedConfiguration.permission_mode === nextMode) {
if (!prev.configuration) {
return {
...prev,
configuration: parsedConfiguration
}
}
return prev
}
return {
...prev,
allowed_tools: validTools
configuration: {
...parsedConfiguration,
permission_mode: nextMode
}
}
})
}, [availableTools])
}, [])
// add supported agents type here.
const agentConfig = useMemo(
@ -197,45 +213,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
}))
}, [])
const onAllowedToolsChange = useCallback(
(keys: Selection) => {
setForm((prev) => {
if (keys === 'all') {
return {
...prev,
allowed_tools: availableTools.map((tool) => tool.id)
}
}
const next = Array.from(keys).map(String)
const filtered = availableTools.length
? next.filter((id) => availableTools.some((tool) => tool.id === id))
: next
return {
...prev,
allowed_tools: filtered
}
})
},
[availableTools]
)
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
if (!items.length) {
return null
}
return (
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
{item.data?.name ?? item.textValue ?? item.key}
</Chip>
))}
</div>
)
}, [])
const addAccessiblePath = useCallback(async () => {
try {
const selected = await window.api.file.selectFolder()
@ -433,25 +410,34 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
)}
</Select>
<Select
selectionMode="multiple"
selectedKeys={selectedToolKeys}
onSelectionChange={onAllowedToolsChange}
label={t('agent.session.allowed_tools.label')}
placeholder={t('agent.session.allowed_tools.placeholder')}
description={
availableTools.length
? t('agent.session.allowed_tools.helper')
: t('agent.session.allowed_tools.empty')
}
isDisabled={!availableTools.length}
items={availableTools}
renderValue={renderSelectedTools}>
{(tool) => (
<SelectItem key={tool.id} textValue={tool.name}>
<div className="flex flex-col">
<span className="font-medium text-sm">{tool.name}</span>
{tool.description ? (
<span className="text-foreground-500 text-xs">{tool.description}</span>
isRequired
selectionMode="single"
selectedKeys={[selectedPermissionMode]}
onSelectionChange={onPermissionModeChange}
label={t('agent.settings.tooling.permissionMode.title', 'Permission mode')}
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
description={t(
'agent.settings.tooling.permissionMode.helper',
'Choose how the agent handles tool approvals.'
)}
items={permissionModeCards}>
{(item) => (
<SelectItem key={item.mode} textValue={t(item.titleKey, item.titleFallback)}>
<div className="flex flex-col gap-1">
<span className="font-medium text-sm">{t(item.titleKey, item.titleFallback)}</span>
<span className="text-foreground-500 text-xs">
{t(item.descriptionKey, item.descriptionFallback)}
</span>
<span className="text-foreground-400 text-xs">
{t(item.behaviorKey, item.behaviorFallback)}
</span>
{item.caution ? (
<span className="text-danger-500 text-xs">
{t(
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
'Use with caution — all tools will run without asking for approval.'
)}
</span>
) : null}
</div>
</SelectItem>

View File

@ -0,0 +1,53 @@
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

@ -132,7 +132,7 @@
"plan": {
"behavior": "Read-only defaults are pre-approved while execution remains disabled.",
"description": "Shares the default read-only tool set but presents a plan before execution.",
"title": "Planning mode (coming soon)"
"title": "Planning mode"
}
},
"preapproved": {

View File

@ -132,7 +132,7 @@
"plan": {
"behavior": "默认的只读工具会自动预先授权,但执行仍被禁用。",
"description": "继承默认的只读工具集,并会在执行前先呈现计划。",
"title": "规划模式(即将支持)"
"title": "规划模式"
}
},
"preapproved": {

View File

@ -132,7 +132,7 @@
"plan": {
"behavior": "預設的唯讀工具會自動預先授權,但執行仍被停用。",
"description": "沿用預設的唯讀工具集,並會在執行前先呈現計畫。",
"title": "規劃模式(即將支援)"
"title": "規劃模式"
}
},
"preapproved": {

View File

@ -1,4 +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 { useMCPServers } from '@renderer/hooks/useMCPServers'
import useScrollPosition from '@renderer/hooks/useScrollPosition'
@ -33,60 +34,8 @@ type AgentToolingSettingsProps =
type AgentConfigurationState = AgentConfiguration & Record<string, unknown>
type PermissionModeCard = {
mode: PermissionMode
titleKey: string
titleFallback: string
descriptionKey: string
descriptionFallback: string
behaviorKey: string
behaviorFallback: string
caution?: boolean
unsupported?: boolean
}
const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.parse({})
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
}
]
const computeModeDefaults = (mode: PermissionMode, tools: Tool[]): string[] => {
const defaultToolIds = tools.filter((tool) => !tool.requirePermissions).map((tool) => tool.id)
switch (mode) {