feat(web): 专家/子代理 capability 字段改为勾选式编辑
- 专家编辑模态框:4 个 textarea 替换为 CheckboxList,技能/工具按来源分组展示 - 新增子代理编辑模态框:非 builtin 子代理显示编辑按钮,支持描述 + 4 个 capability 勾选字段 - 进入 experts/subagents 标签页时自动拉取技能与工具列表 - allowed_* 为空时传 undefined(后端 None=不限),避免空数组触发白名单空集语义
This commit is contained in:
parent
3ea0c19262
commit
9381ed5dd4
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
Settings, Save, X, Plus, Trash2, AlertTriangle, Loader2, Wifi,
|
||||
CheckCircle, RefreshCw, UserCheck, Pencil,
|
||||
CheckCircle, RefreshCw, UserCheck, Pencil, Bot,
|
||||
} from 'lucide-react'
|
||||
|
||||
// ── Extracted modules ─────────────────────────────────
|
||||
@ -10,16 +10,19 @@ import type {
|
||||
ProviderConfig, ModelConfig, AgentConfig,
|
||||
McpServerConfig, McpStatusResponse,
|
||||
SkillListResponse,
|
||||
SubagentListResponse,
|
||||
SubagentListResponse, SubagentItem,
|
||||
ToolsListResponse,
|
||||
ExpertItem, ExpertListResponse,
|
||||
CapabilityPolicy,
|
||||
KnownSource,
|
||||
SchedulerConfig, ChannelConfig,
|
||||
} from './types'
|
||||
import { TABS, inputCls, selectCls, TIMEZONE_OPTIONS } from './constants'
|
||||
import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader } from './ui'
|
||||
import { Field, Toggle, TagEditor, SectionCard, SourceEditor, MapEntryHeader, CheckboxList } from './ui'
|
||||
import { getAppConfig, updateAppConfig, restartGateway, checkHealth } from '../../api/config'
|
||||
import { listSkills, toggleSkill } from '../../api/skills'
|
||||
import { listSubagents, toggleSubagent } from '../../api/subagents'
|
||||
import { listTools } from '../../api/tools'
|
||||
import { listSubagents, toggleSubagent, updateSubagent } from '../../api/subagents'
|
||||
import { listExperts, toggleExpert, createExpert, updateExpert, deleteExpert } from '../../api/experts'
|
||||
import { getMcpStatus } from '../../api/mcp'
|
||||
export { getSelectedExpert, selectExpert } from '../../api/experts'
|
||||
@ -47,6 +50,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
const [mcpStatus, setMcpStatus] = useState<McpStatusResponse | null>(null)
|
||||
const [skillList, setSkillList] = useState<SkillListResponse | null>(null)
|
||||
const [skillListLoading, setSkillListLoading] = useState(false)
|
||||
const [toolList, setToolList] = useState<ToolsListResponse | null>(null)
|
||||
const [toolListLoading, setToolListLoading] = useState(false)
|
||||
const [subagentList, setSubagentList] = useState<SubagentListResponse | null>(null)
|
||||
const [subagentListLoading, setSubagentListLoading] = useState(false)
|
||||
const [expertList, setExpertList] = useState<ExpertListResponse | null>(null)
|
||||
@ -58,9 +63,23 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
nameField: string
|
||||
description: string
|
||||
body: string
|
||||
allowedSkills: string[]
|
||||
deniedSkills: string[]
|
||||
allowedTools: string[]
|
||||
deniedTools: string[]
|
||||
} | null>(null)
|
||||
const [editingExpertError, setEditingExpertError] = useState('')
|
||||
const [savingExpert, setSavingExpert] = useState(false)
|
||||
const [editingSubagent, setEditingSubagent] = useState<{
|
||||
name: string
|
||||
description: string
|
||||
allowedSkills: string[]
|
||||
deniedSkills: string[]
|
||||
allowedTools: string[]
|
||||
deniedTools: string[]
|
||||
} | null>(null)
|
||||
const [editingSubagentError, setEditingSubagentError] = useState('')
|
||||
const [savingSubagent, setSavingSubagent] = useState(false)
|
||||
|
||||
const fetchMcpStatus = useCallback(async () => {
|
||||
const data = await getMcpStatus()
|
||||
@ -74,6 +93,13 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
setSkillListLoading(false)
|
||||
}, [])
|
||||
|
||||
const fetchToolList = useCallback(async () => {
|
||||
setToolListLoading(true)
|
||||
const data = await listTools()
|
||||
if (data) setToolList(data)
|
||||
setToolListLoading(false)
|
||||
}, [])
|
||||
|
||||
const toggleSkillCb = useCallback(async (name: string, scope: string, enabled: boolean) => {
|
||||
return toggleSkill(name, scope, enabled)
|
||||
}, [])
|
||||
@ -89,6 +115,10 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
return toggleSubagent(name, scope, enabled)
|
||||
}, [])
|
||||
|
||||
const updateSubagentCb = useCallback(async (payload: { name: string; description?: string; body?: string; capability?: CapabilityPolicy }) => {
|
||||
return updateSubagent(payload)
|
||||
}, [])
|
||||
|
||||
const fetchExpertList = useCallback(async () => {
|
||||
setExpertListLoading(true)
|
||||
const data = await listExperts()
|
||||
@ -100,11 +130,11 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
return toggleExpert(name, scope, enabled)
|
||||
}, [])
|
||||
|
||||
const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string }) => {
|
||||
const createExpertCb = useCallback(async (payload: { name: string; description: string; body: string; scope: string; capability?: CapabilityPolicy }) => {
|
||||
return createExpert(payload)
|
||||
}, [])
|
||||
|
||||
const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string }) => {
|
||||
const updateExpertCb = useCallback(async (payload: { name: string; scope: string; description?: string; body?: string; capability?: CapabilityPolicy }) => {
|
||||
return updateExpert(payload)
|
||||
}, [])
|
||||
|
||||
@ -141,6 +171,14 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
if (activeTab === 'subagents') fetchSubagentList()
|
||||
}, [activeTab, fetchSubagentList])
|
||||
|
||||
// Fetch skills + tools when experts/subagents tab is selected (for capability CheckboxList)
|
||||
useEffect(() => {
|
||||
if (activeTab === 'experts' || activeTab === 'subagents') {
|
||||
if (!skillList) fetchSkillList()
|
||||
if (!toolList) fetchToolList()
|
||||
}
|
||||
}, [activeTab, fetchSkillList, fetchToolList, skillList, toolList])
|
||||
|
||||
// Fetch expert list when experts tab is selected
|
||||
useEffect(() => {
|
||||
if (activeTab === 'experts') fetchExpertList()
|
||||
@ -568,6 +606,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
/>
|
||||
</SectionCard>
|
||||
{renderDiscoveredSubagents()}
|
||||
{renderSubagentModal()}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -636,6 +675,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
)
|
||||
}
|
||||
|
||||
const handleEditSubagent = (subagent: SubagentItem) => {
|
||||
setEditingSubagentError('')
|
||||
setEditingSubagent({
|
||||
name: subagent.name,
|
||||
description: subagent.description,
|
||||
allowedSkills: subagent.capability?.allowed_skills ?? [],
|
||||
deniedSkills: subagent.capability?.denied_skills ?? [],
|
||||
allowedTools: subagent.capability?.allowed_tools ?? [],
|
||||
deniedTools: subagent.capability?.denied_tools ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionCard title="已发现子代理" subtitle="即时生效">
|
||||
{subagentListLoading && subagents.length === 0 ? (
|
||||
@ -648,6 +699,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<div className="space-y-1">
|
||||
{subagents.map(subagent => {
|
||||
const isEnabled = subagent.disabled_in_scopes.length === 0
|
||||
const isBuiltin = subagent.source === 'builtin'
|
||||
return (
|
||||
<div key={subagent.name} className="flex items-center gap-3 py-2 px-2 rounded-lg hover:bg-[var(--bg-hover)] transition-colors">
|
||||
<div className="flex-1 min-w-0">
|
||||
@ -656,9 +708,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-tertiary)] text-[var(--text-muted)] uppercase tracking-wider">{subagent.source}</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-muted)] truncate mt-0.5">{subagent.description}</p>
|
||||
{renderToolTags('允许', subagent.allowed_tools, 'allow')}
|
||||
{renderToolTags('禁用', subagent.denied_tools, 'deny')}
|
||||
{renderToolTags('允许', subagent.capability?.allowed_tools, 'allow')}
|
||||
{renderToolTags('禁用', subagent.capability?.denied_tools, 'deny')}
|
||||
</div>
|
||||
{!isBuiltin && (
|
||||
<button
|
||||
onClick={() => handleEditSubagent(subagent)}
|
||||
className="p-1 rounded text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
title="编辑"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<Toggle checked={isEnabled} onChange={() => handleToggle(subagent.name, isEnabled)} />
|
||||
</div>
|
||||
)
|
||||
@ -691,7 +752,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingExpertError('')
|
||||
setEditingExpert({ mode: 'create', scope: 'project', nameField: '', description: '', body: '' })
|
||||
setEditingExpert({ mode: 'create', scope: 'project', nameField: '', description: '', body: '', allowedSkills: [], deniedSkills: [], allowedTools: [], deniedTools: [] })
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl border border-dashed border-[var(--border-color)] text-[var(--text-muted)] hover:text-[var(--accent-cyan)] hover:border-[var(--accent-cyan)]/30 transition-colors text-sm w-full justify-center"
|
||||
>
|
||||
@ -750,6 +811,10 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
nameField: expert.name,
|
||||
description: expert.description,
|
||||
body: expert.body ?? '',
|
||||
allowedSkills: expert.capability?.allowed_skills ?? [],
|
||||
deniedSkills: expert.capability?.denied_skills ?? [],
|
||||
allowedTools: expert.capability?.allowed_tools ?? [],
|
||||
deniedTools: expert.capability?.denied_tools ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
@ -817,6 +882,12 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
)
|
||||
}
|
||||
|
||||
// 技能/工具勾选选项(专家与子代理编辑模态框共用)
|
||||
const skillOptions = (skillList?.skills ?? []).map(s => ({ key: s.name, label: s.name, description: s.description, group: s.source }))
|
||||
const toolOptions = (toolList?.tools ?? []).map(t => ({ key: t.name, label: t.name, description: t.description, group: t.source }))
|
||||
const skillEmptyHint = skillListLoading ? '加载中...' : '未发现任何技能,请先在技能页配置来源目录'
|
||||
const toolEmptyHint = toolListLoading ? '加载中...' : '未发现任何工具'
|
||||
|
||||
const renderExpertModal = () => {
|
||||
if (!editingExpert) return null
|
||||
const isEdit = editingExpert.mode === 'edit'
|
||||
@ -827,19 +898,24 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
setSavingExpert(true)
|
||||
setEditingExpertError('')
|
||||
try {
|
||||
// allowed_skills/allowed_tools 为空时必须传 undefined(后端 None=不限),
|
||||
// 否则空数组会被反序列化为 Some(vec![]) 触发白名单空集语义(全禁)。
|
||||
// denied_skills/denied_tools 为 Vec<String>,空数组即"不禁",可直接传。
|
||||
const capability: CapabilityPolicy = {
|
||||
allowed_skills: editingExpert.allowedSkills.length > 0 ? editingExpert.allowedSkills : undefined,
|
||||
denied_skills: editingExpert.deniedSkills,
|
||||
allowed_tools: editingExpert.allowedTools.length > 0 ? editingExpert.allowedTools : undefined,
|
||||
denied_tools: editingExpert.deniedTools,
|
||||
}
|
||||
const payload = {
|
||||
name: editingExpert.nameField,
|
||||
description: editingExpert.description,
|
||||
body: editingExpert.body,
|
||||
capability,
|
||||
}
|
||||
const resp = isEdit
|
||||
? await updateExpertCb({
|
||||
name: editingExpert.nameField,
|
||||
scope: 'project',
|
||||
description: editingExpert.description,
|
||||
body: editingExpert.body,
|
||||
})
|
||||
: await createExpertCb({
|
||||
name: editingExpert.nameField,
|
||||
description: editingExpert.description,
|
||||
body: editingExpert.body,
|
||||
scope: 'project',
|
||||
})
|
||||
? await updateExpertCb({ ...payload, scope: 'project' })
|
||||
: await createExpertCb({ ...payload, scope: 'project' })
|
||||
const data = await resp.json().catch(() => ({}))
|
||||
if (!resp.ok) {
|
||||
setEditingExpertError(data.error || data.message || '保存失败')
|
||||
@ -859,7 +935,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out]">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] overflow-y-auto">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<UserCheck className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">
|
||||
@ -890,9 +966,51 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
value={editingExpert.body}
|
||||
onChange={e => setEditingExpert(prev => prev ? { ...prev, body: e.target.value } : prev)}
|
||||
placeholder="你是一名专业翻译..."
|
||||
className={inputCls + ' min-h-[360px] resize-y font-mono text-xs'}
|
||||
className={inputCls + ' min-h-[200px] resize-y font-mono text-xs'}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 border-t border-[var(--border-color)]">
|
||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingExpert.allowedSkills}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedSkills: v } : prev)}
|
||||
extraSelected={editingExpert.allowedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的技能(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingExpert.deniedSkills}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedSkills: v } : prev)}
|
||||
extraSelected={editingExpert.deniedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingExpert.allowedTools}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, allowedTools: v } : prev)}
|
||||
extraSelected={editingExpert.allowedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的工具" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingExpert.deniedTools}
|
||||
onChange={v => setEditingExpert(prev => prev ? { ...prev, deniedTools: v } : prev)}
|
||||
extraSelected={editingExpert.deniedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{editingExpertError && (
|
||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
{editingExpertError}
|
||||
@ -920,6 +1038,135 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
|
||||
)
|
||||
}
|
||||
|
||||
const renderSubagentModal = () => {
|
||||
if (!editingSubagent) return null
|
||||
const canSave = editingSubagent.description.trim().length > 0
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!canSave) return
|
||||
setSavingSubagent(true)
|
||||
setEditingSubagentError('')
|
||||
try {
|
||||
// 与专家一致:allowed_* 为空时传 undefined(None=不限),denied_* 空数组即"不禁"
|
||||
const capability: CapabilityPolicy = {
|
||||
allowed_skills: editingSubagent.allowedSkills.length > 0 ? editingSubagent.allowedSkills : undefined,
|
||||
denied_skills: editingSubagent.deniedSkills,
|
||||
allowed_tools: editingSubagent.allowedTools.length > 0 ? editingSubagent.allowedTools : undefined,
|
||||
denied_tools: editingSubagent.deniedTools,
|
||||
}
|
||||
const resp = await updateSubagentCb({
|
||||
name: editingSubagent.name,
|
||||
description: editingSubagent.description,
|
||||
capability,
|
||||
})
|
||||
const data = await resp.json().catch(() => ({}))
|
||||
if (!resp.ok) {
|
||||
setEditingSubagentError(data.error || data.message || '保存失败')
|
||||
setSavingSubagent(false)
|
||||
return
|
||||
}
|
||||
setToast('子代理已更新')
|
||||
setTimeout(() => setToast(''), 3000)
|
||||
setEditingSubagent(null)
|
||||
fetchSubagentList()
|
||||
} catch (e: unknown) {
|
||||
setEditingSubagentError(e instanceof Error ? e.message : '网络错误')
|
||||
} finally {
|
||||
setSavingSubagent(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 w-[90%] max-w-3xl mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out] max-h-[90%] overflow-y-auto">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Bot className="h-5 w-5 text-[var(--accent-cyan)]" />
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">编辑子代理</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Field label="名称">
|
||||
<input
|
||||
value={editingSubagent.name}
|
||||
disabled
|
||||
className={inputCls + ' opacity-60 cursor-not-allowed'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="描述" hint="子代理的简短描述,用于主智能体选择">
|
||||
<input
|
||||
value={editingSubagent.description}
|
||||
onChange={e => setEditingSubagent(prev => prev ? { ...prev, description: e.target.value } : prev)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-2 border-t border-[var(--border-color)]">
|
||||
<Field label="允许的技能(白名单)" hint="留空表示不限。仅勾选的 SKILL.md 技能可见">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingSubagent.allowedSkills}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedSkills: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的技能(黑名单)" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={skillOptions}
|
||||
selected={editingSubagent.deniedSkills}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedSkills: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedSkills}
|
||||
emptyHint={skillEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="允许的工具(白名单)" hint="留空表示不限。覆盖内置 + MCP 工具">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingSubagent.allowedTools}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, allowedTools: v } : prev)}
|
||||
extraSelected={editingSubagent.allowedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="禁用的工具" hint="在白名单之后应用">
|
||||
<CheckboxList
|
||||
options={toolOptions}
|
||||
selected={editingSubagent.deniedTools}
|
||||
onChange={v => setEditingSubagent(prev => prev ? { ...prev, deniedTools: v } : prev)}
|
||||
extraSelected={editingSubagent.deniedTools}
|
||||
emptyHint={toolEmptyHint}
|
||||
groupBy={o => o.group ?? '其他'}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{editingSubagentError && (
|
||||
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
|
||||
{editingSubagentError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3 justify-end mt-5">
|
||||
<button
|
||||
onClick={() => setEditingSubagent(null)}
|
||||
className="px-4 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!canSave || savingSubagent}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30 hover:bg-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/50 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{savingSubagent ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
||||
{savingSubagent ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderMcp = () => {
|
||||
const entries = Object.entries(config.mcpServers)
|
||||
const statusFor = (key: string) => mcpStatus?.servers?.find(s => s.key === key)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user