fix: all mcp tools exposed to main agent (#5252)

This commit is contained in:
Soulter
2026-02-20 15:40:13 +08:00
committed by GitHub
parent 52d1979937
commit ed4cacfffb
11 changed files with 578 additions and 241 deletions
@@ -4,6 +4,7 @@ import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { ref, computed } from 'vue'
import ConfigItemRenderer from './ConfigItemRenderer.vue'
import TemplateListEditor from './TemplateListEditor.vue'
import PersonaQuickPreview from './PersonaQuickPreview.vue'
import { useI18n, useModuleI18n } from '@/i18n/composables'
@@ -274,6 +275,16 @@ function getSpecialSubtype(value) {
</div>
</v-col>
</v-row>
<!-- Default Persona Quick Preview 全宽显示区域 -->
<v-row
v-if="!itemMeta?.invisible && itemMeta?._special === 'select_persona' && itemKey === 'provider_settings.default_personality'"
class="persona-preview-row"
>
<v-col cols="12" class="persona-preview-display">
<PersonaQuickPreview :model-value="createSelectorModel(itemKey).value" />
</v-col>
</v-row>
</template>
<v-divider class="config-divider"
v-if="shouldShowItem(itemMeta, itemKey) && hasVisibleItemsAfter(metadata[metadataKey].items, index)"></v-divider>
@@ -433,6 +444,15 @@ function getSpecialSubtype(value) {
padding: 0 8px;
}
.persona-preview-row {
margin: 16px;
margin-top: 0;
}
.persona-preview-display {
padding: 0 8px;
}
.selected-plugins-full-width {
background-color: rgba(var(--v-theme-primary), 0.05);
border: 1px solid rgba(var(--v-theme-primary), 0.1);
@@ -0,0 +1,287 @@
<template>
<div class="persona-preview-card">
<div class="preview-header">
<small>{{ tm('personaQuickPreview.title') }}</small>
</div>
<div v-if="loading" class="preview-loading">
<v-progress-circular indeterminate size="18" width="2" color="primary" class="mr-2" />
<small class="text-grey">{{ tm('personaQuickPreview.loading') }}</small>
</div>
<div v-else-if="!modelValue" class="preview-empty">
<small class="text-grey">{{ tm('personaQuickPreview.noPersonaSelected') }}</small>
</div>
<div v-else-if="!personaData" class="preview-empty">
<small class="text-grey">{{ tm('personaQuickPreview.personaNotFound') }}</small>
</div>
<div v-else class="preview-content">
<div class="section-title">{{ tm('personaQuickPreview.systemPromptLabel') }}</div>
<pre class="prompt-content">{{ personaData.system_prompt || '' }}</pre>
<div class="section-title mt-3">{{ tm('personaQuickPreview.toolsLabel') }}</div>
<div class="chip-wrap tools-wrap">
<v-chip
v-if="personaData.tools === null"
size="small"
color="success"
variant="tonal"
label
>
{{ tm('personaQuickPreview.allToolsWithCount', { count: allToolsCount }) }}
</v-chip>
<div v-for="tool in resolvedTools" v-else :key="tool.name" class="tool-item">
<v-chip
size="small"
color="primary"
variant="outlined"
label
>
{{ tool.name }}
</v-chip>
<small v-if="tool.origin || tool.origin_name" class="text-grey tool-meta">
<span v-if="tool.origin">{{ tm('personaQuickPreview.originLabel') }}: {{ tool.origin }}</span>
<span v-if="tool.origin_name"> | {{ tm('personaQuickPreview.originNameLabel') }}: {{ tool.origin_name }}</span>
</small>
</div>
<small v-if="personaData.tools !== null && normalizedTools.length === 0" class="text-grey">
{{ tm('personaQuickPreview.noTools') }}
</small>
</div>
<div class="section-title mt-3">{{ tm('personaQuickPreview.skillsLabel') }}</div>
<div class="chip-wrap">
<v-chip
v-if="personaData.skills === null"
size="small"
color="success"
variant="tonal"
label
>
{{ tm('personaQuickPreview.allSkillsWithCount', { count: allSkillsCount }) }}
</v-chip>
<v-chip
v-for="skillName in normalizedSkills"
v-else
:key="skillName"
size="small"
color="primary"
variant="outlined"
label
>
{{ skillName }}
</v-chip>
<small v-if="personaData.skills !== null && normalizedSkills.length === 0" class="text-grey">
{{ tm('personaQuickPreview.noSkills') }}
</small>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import axios from 'axios'
import { useModuleI18n } from '@/i18n/composables'
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const { tm } = useModuleI18n('core.shared')
const loading = ref(false)
const personaData = ref(null)
const toolMetaMap = ref({})
const availableSkills = ref([])
const defaultPersonaData = {
persona_id: 'default',
system_prompt: 'You are a helpful and friendly assistant.',
tools: null,
skills: null
}
const normalizedTools = computed(() => (Array.isArray(personaData.value?.tools) ? personaData.value.tools : []))
const normalizedSkills = computed(() => (Array.isArray(personaData.value?.skills) ? personaData.value.skills : []))
const allToolsCount = computed(() => Object.keys(toolMetaMap.value).length)
const allSkillsCount = computed(() => availableSkills.value.length)
const resolvedTools = computed(() =>
normalizedTools.value.map((toolName) => {
const meta = toolMetaMap.value[toolName] || {}
return {
name: toolName,
origin: meta.origin || '',
origin_name: meta.origin_name || ''
}
})
)
async function loadToolsMeta() {
try {
const response = await axios.get('/api/tools/list')
if (response.data?.status === 'ok') {
const tools = response.data?.data || []
const nextMap = {}
for (const tool of tools) {
if (!tool?.name) {
continue
}
nextMap[tool.name] = {
origin: tool.origin || '',
origin_name: tool.origin_name || ''
}
}
toolMetaMap.value = nextMap
}
} catch (error) {
console.error('Failed to load tools metadata:', error)
toolMetaMap.value = {}
}
}
async function loadSkillsMeta() {
try {
const response = await axios.get('/api/skills')
if (response.data?.status === 'ok') {
const payload = response.data?.data || []
if (Array.isArray(payload)) {
availableSkills.value = payload.filter((skill) => skill.active !== false)
} else {
const skills = payload.skills || []
availableSkills.value = skills.filter((skill) => skill.active !== false)
}
} else {
availableSkills.value = []
}
} catch (error) {
console.error('Failed to load skills metadata:', error)
availableSkills.value = []
}
}
async function loadPersonaPreview(personaId) {
if (!personaId) {
personaData.value = null
return
}
if (personaId === 'default') {
personaData.value = defaultPersonaData
return
}
loading.value = true
try {
const response = await axios.get('/api/persona/list')
if (response.data?.status === 'ok') {
const personas = response.data?.data || []
personaData.value = personas.find((item) => item.persona_id === personaId) || null
} else {
personaData.value = null
}
} catch (error) {
console.error('Failed to load persona preview:', error)
personaData.value = null
} finally {
loading.value = false
}
}
function handlePersonaSaved() {
if (props.modelValue) {
loadPersonaPreview(props.modelValue)
}
}
watch(
() => props.modelValue,
(newValue) => {
loadPersonaPreview(newValue)
},
{ immediate: true }
)
loadToolsMeta()
loadSkillsMeta()
onMounted(() => {
window.addEventListener('astrbot:persona-saved', handlePersonaSaved)
})
onBeforeUnmount(() => {
window.removeEventListener('astrbot:persona-saved', handlePersonaSaved)
})
</script>
<style scoped>
.persona-preview-card {
background-color: rgba(var(--v-theme-primary), 0.05);
border: 1px solid rgba(var(--v-theme-primary), 0.1);
border-radius: 8px;
padding: 12px;
}
.preview-header {
margin-bottom: 8px;
}
.preview-loading,
.preview-empty {
display: flex;
align-items: center;
min-height: 24px;
}
.section-title {
font-size: 0.75rem;
color: rgb(var(--v-theme-primaryText));
opacity: 0.85;
}
.prompt-content {
margin-top: 6px;
max-height: 180px;
overflow: auto;
font-size: 0.78rem;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
background: rgba(0, 0, 0, 0.03);
border-radius: 6px;
padding: 8px;
}
.chip-wrap {
display: grid;
gap: 6px;
margin-top: 6px;
}
.tools-wrap {
max-height: 160px;
overflow: auto;
}
.tool-item {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.tool-meta {
font-size: 0.74rem;
}
@media (max-width: 600px) {
.tools-wrap {
max-height: 120px;
}
}
</style>
@@ -188,10 +188,16 @@ function openEditPersona(persona: Persona) {
// 人格保存成功(创建或编辑)
async function handlePersonaSaved(message: string) {
console.log('人格保存成功:', message)
const savedPersonaId = editingPersona.value?.persona_id || ''
showPersonaDialog.value = false
editingPersona.value = null
// 刷新当前文件夹的人格列表
await loadPersonasInFolder(currentFolderId.value)
window.dispatchEvent(
new CustomEvent('astrbot:persona-saved', {
detail: { persona_id: savedPersonaId }
})
)
}
// 错误处理
@@ -62,6 +62,23 @@
"rootFolder": "All Personas",
"emptyFolder": "This folder is empty"
},
"personaQuickPreview": {
"title": "Quick Persona Preview",
"loading": "Loading...",
"noPersonaSelected": "No persona selected",
"personaNotFound": "Persona details not found",
"systemPromptLabel": "System Prompt",
"toolsLabel": "Tools",
"skillsLabel": "Skills",
"originLabel": "Origin",
"originNameLabel": "Origin Name",
"allTools": "All tools available",
"allToolsWithCount": "All tools available ({count})",
"noTools": "No tools configured",
"allSkills": "All Skills available",
"allSkillsWithCount": "All Skills available ({count})",
"noSkills": "No Skills configured"
},
"t2iTemplateEditor": {
"buttonText": "Customize T2I Template",
"dialogTitle": "Customize Text-to-Image HTML Template",
@@ -19,7 +19,8 @@
"enabled": "When on: the main LLM keeps its own tools and mounts transfer_to_* delegate tools. With deduplication, tools overlapping with SubAgents are removed from the main tool set."
},
"section": {
"title": "SubAgents"
"title": "SubAgents",
"globalSettings": "Global Settings"
},
"cards": {
"statusEnabled": "Enabled",
@@ -62,6 +62,23 @@
"rootFolder": "全部人格",
"emptyFolder": "此文件夹为空"
},
"personaQuickPreview": {
"title": "快速预览",
"loading": "加载中...",
"noPersonaSelected": "未选择人格",
"personaNotFound": "未找到该人格的详情",
"systemPromptLabel": "系统提示词",
"toolsLabel": "工具",
"skillsLabel": "技能(Skills",
"originLabel": "来源",
"originNameLabel": "来源名称",
"allTools": "全部工具可用",
"allToolsWithCount": "全部工具可用({count}",
"noTools": "未配置工具",
"allSkills": "全部 Skills 可用",
"allSkillsWithCount": "全部 Skills 可用({count}",
"noSkills": "未配置 Skills"
},
"t2iTemplateEditor": {
"buttonText": "自定义 T2I 模板",
"dialogTitle": "自定义文转图 HTML 模板",
@@ -24,7 +24,7 @@
"presetDialogsHelp": "添加一些预设的对话来帮助机器人更好地理解角色设定。",
"userMessage": "用户消息",
"assistantMessage": "AI 回答",
"tools": "工具选择",
"tools": "工具 / MCP 工具选择",
"toolsHelp": "为这个人格选择可用的外部工具。外部工具给了 AI 接触外部环境的能力,如搜索、计算、获取信息等。",
"toolsSelection": "工具选择操作",
"selectAllTools": "选择所有工具",
@@ -19,7 +19,8 @@
"enabled": "启动:主 LLM 会保留自身工具并挂载 transfer_to_* 委派工具。若开启“去重重复工具”,与 SubAgent 指定的工具重叠部分会从主 LLM 工具集中移除。"
},
"section": {
"title": "SubAgents"
"title": "SubAgents 配置",
"globalSettings": "全局设置"
},
"cards": {
"statusEnabled": "启用",
@@ -28,7 +29,8 @@
"transferPrefix": "transfer_to_{name}",
"switchLabel": "启用",
"previewTitle": "预览:主 LLM 将看到的 handoff 工具",
"personaChip": "Persona: {id}"
"personaChip": "Persona: {id}",
"noDescription": "暂无描述"
},
"form": {
"nameLabel": "Agent 名称(用于 transfer_to_{name}",
+1 -1
View File
@@ -1121,7 +1121,7 @@ export default {
.text-truncate {
display: inline-block;
max-width: 100px;
/* max-width: 100px; */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+223 -227
View File
@@ -1,155 +1,249 @@
<template>
<div class="subagent-page">
<div class="d-flex align-center justify-space-between mb-4">
<div class="d-flex align-center justify-space-between mb-6">
<div>
<div class="d-flex align-center" style="gap: 8px;">
<div class="d-flex align-center gap-2 mb-1">
<h2 class="text-h5 font-weight-bold">{{ tm('page.title') }}</h2>
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label>{{ tm('page.beta') }}</v-chip>
<v-chip size="x-small" color="orange-darken-2" variant="tonal" label class="font-weight-bold">
{{ tm('page.beta') }}
</v-chip>
</div>
<div class="text-body-2 text-medium-emphasis">
{{ tm('page.subtitle') }}
</div>
</div>
<div class="d-flex align-center" style="gap: 8px;">
<v-btn variant="tonal" color="primary" :loading="loading" @click="reload">{{ tm('actions.refresh') }}</v-btn>
<v-btn variant="flat" color="primary" :loading="saving" @click="save">{{ tm('actions.save') }}</v-btn>
<div class="d-flex align-center gap-2">
<v-btn
variant="text"
color="primary"
prepend-icon="mdi-refresh"
:loading="loading"
@click="reload"
>
{{ tm('actions.refresh') }}
</v-btn>
<v-btn
variant="flat"
color="primary"
prepend-icon="mdi-content-save"
:loading="saving"
@click="save"
>
{{ tm('actions.save') }}
</v-btn>
</div>
</div>
<v-card class="rounded-lg" variant="flat">
<!-- Global Settings Card -->
<v-card class="rounded-lg mb-6 border-thin" variant="flat" border>
<v-card-text>
<v-row>
<div class="d-flex align-center justify-space-between">
<div>
<div class="text-subtitle-1 font-weight-bold mb-1">{{ tm('section.globalSettings') || 'Global Settings' }}</div>
<div class="text-caption text-medium-emphasis">
{{ mainStateDescription }}
</div>
</div>
</div>
<v-divider class="my-4" />
<v-row dense>
<v-col cols="12" md="6">
<v-switch
v-model="cfg.main_enable"
:label="tm('switches.enable')"
inset
color="primary"
hide-details
inset
density="comfortable"
/>
>
<template #label>
<div class="d-flex flex-column">
<span class="text-body-2 font-weight-medium">{{ tm('switches.enable') }}</span>
<span class="text-caption text-medium-emphasis">Enable sub-agent functionality</span>
</div>
</template>
</v-switch>
</v-col>
<v-col cols="12" md="6">
<v-switch
v-model="cfg.remove_main_duplicate_tools"
:disabled="!cfg.main_enable"
:label="tm('switches.dedupe')"
inset
color="primary"
hide-details
inset
density="comfortable"
/>
>
<template #label>
<div class="d-flex flex-column">
<span class="text-body-2 font-weight-medium">{{ tm('switches.dedupe') }}</span>
<span class="text-caption text-medium-emphasis">Remove duplicate tools from main agent</span>
</div>
</template>
</v-switch>
</v-col>
</v-row>
<div class="text-caption text-medium-emphasis mt-1">
{{ mainStateDescription }}
</div>
<div class="d-flex align-center justify-space-between mt-6 mb-2">
<div class="text-subtitle-1 font-weight-bold">{{ tm('section.title') }}</div>
<v-btn size="small" variant="tonal" color="primary" @click="addAgent">
{{ tm('actions.add') }}
</v-btn>
</div>
<v-expansion-panels variant="accordion" multiple>
<v-expansion-panel v-for="(agent, idx) in cfg.agents" :key="agent.__key">
<v-expansion-panel-title>
<div class="subagent-panel-title">
<div class="subagent-title-left">
<v-chip :color="agent.enabled ? 'success' : 'grey'" size="small" variant="tonal">
{{ agent.enabled ? tm('cards.statusEnabled') : tm('cards.statusDisabled') }}
</v-chip>
<div class="subagent-title-text">
<div class="subagent-title-name">{{ agent.name || tm('cards.unnamed') }}</div>
<div class="subagent-title-sub">
{{ tm('cards.transferPrefix', { name: agent.name || '...' }) }}
</div>
</div>
</div>
<div class="subagent-title-right">
<v-switch
v-model="agent.enabled"
inset
color="primary"
hide-details
class="subagent-enabled-inline"
@click.stop
>
<template #label>{{ tm('cards.switchLabel') }}</template>
</v-switch>
<v-btn size="small" variant="text" color="error" @click.stop="removeAgent(idx)">
{{ tm('actions.delete') }}
</v-btn>
</div>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-row class="subagent-grid">
<v-col cols="12" md="5">
<v-text-field
v-model="agent.name"
:label="tm('form.nameLabel')"
variant="outlined"
density="comfortable"
:hint="tm('form.nameHint')"
persistent-hint
/>
</v-col>
<v-col cols="12" md="7" class="subagent-actions">
<ProviderSelector
v-model="agent.provider_id"
provider-type="chat_completion"
:label="tm('form.providerLabel')"
:hint="tm('form.providerHint')"
persistent-hint
clearable
class="subagent-provider"
/>
</v-col>
<v-col cols="12" md="6">
<v-autocomplete
v-model="agent.persona_id"
:items="personaOptions"
item-title="title"
item-value="value"
:label="tm('form.personaLabel')"
variant="outlined"
density="comfortable"
clearable
:loading="personaLoading"
:disabled="personaLoading"
:hint="tm('form.personaHint')"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="agent.public_description"
:label="tm('form.descriptionLabel')"
variant="outlined"
density="comfortable"
:hint="tm('form.descriptionHint')"
persistent-hint
/>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000">
<!-- Agents List Section -->
<div class="d-flex align-center justify-space-between mb-4">
<div class="d-flex align-center gap-2">
<v-icon icon="mdi-robot" color="primary" size="small" />
<div class="text-h6 font-weight-bold">{{ tm('section.title') }}</div>
<v-chip size="small" variant="tonal" color="primary" class="ml-2">
{{ cfg.agents.length }}
</v-chip>
</div>
<v-btn
prepend-icon="mdi-plus"
color="primary"
@click="addAgent"
>
{{ tm('actions.add') }}
</v-btn>
</div>
<v-expansion-panels variant="popout" class="subagent-panels">
<v-expansion-panel
v-for="(agent, idx) in cfg.agents"
:key="agent.__key"
elevation="0"
class="border-thin mb-2 rounded-lg"
:class="{ 'border-primary': agent.enabled }"
>
<v-expansion-panel-title class="py-3">
<div class="d-flex align-center w-100 gap-4">
<!-- Status Indicator -->
<v-badge
dot
:color="agent.enabled ? 'success' : 'grey'"
inline
class="mr-2"
/>
<!-- Agent Info -->
<div class="d-flex flex-column flex-grow-1" style="min-width: 0;">
<div class="d-flex align-center gap-2">
<span class="text-subtitle-1 font-weight-bold text-truncate">
{{ agent.name || tm('cards.unnamed') }}
</span>
</div>
<div class="text-caption text-medium-emphasis text-truncate">
{{ agent.public_description || tm('cards.noDescription') }}
</div>
</div>
<!-- Controls (stop propagation on clicks) -->
<div class="d-flex align-center gap-2" @click.stop>
<v-switch
v-model="agent.enabled"
color="success"
hide-details
inset
density="compact"
class="mr-2"
/>
<v-btn
icon="mdi-delete-outline"
variant="text"
color="error"
density="comfortable"
@click="removeAgent(idx)"
/>
</div>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-divider class="mb-4" />
<v-row>
<!-- Left Column: Form -->
<v-col cols="12" md="6">
<div class="d-flex flex-column gap-4">
<v-text-field
v-model="agent.name"
:label="tm('form.nameLabel')"
:rules="[v => !!v || 'Name is required', v => /^[a-z][a-z0-9_]*$/.test(v) || 'Lowercase letters, numbers, underscore only']"
variant="outlined"
density="comfortable"
hide-details="auto"
prepend-inner-icon="mdi-account"
/>
<div class="d-flex flex-column gap-1">
<div class="text-caption text-medium-emphasis ml-1">{{ tm('form.providerLabel') }}</div>
<v-card variant="outlined" class="pa-0 border-thin rounded bg-transparent" style="border-color: rgba(var(--v-border-color), var(--v-border-opacity));">
<div class="pa-3">
<ProviderSelector
v-model="agent.provider_id"
provider-type="chat_completion"
variant="outlined"
density="comfortable"
clearable
/>
</div>
</v-card>
</div>
<div class="d-flex flex-column gap-1">
<div class="text-caption text-medium-emphasis ml-1">{{ tm('form.personaLabel') }}</div>
<v-card variant="outlined" class="pa-0 border-thin rounded bg-transparent" style="border-color: rgba(var(--v-border-color), var(--v-border-opacity));">
<div class="pa-3">
<PersonaSelector
v-model="agent.persona_id"
/>
</div>
</v-card>
</div>
<v-textarea
v-model="agent.public_description"
:label="tm('form.descriptionLabel')"
variant="outlined"
density="comfortable"
auto-grow
hide-details="auto"
prepend-inner-icon="mdi-text"
/>
</div>
</v-col>
<!-- Right Column: Preview -->
<v-col cols="12" md="6">
<div class="h-100">
<div class="text-caption font-weight-bold text-medium-emphasis mb-2 ml-1">
PERSONA PREVIEW
</div>
<PersonaQuickPreview
:model-value="agent.persona_id"
class="h-100"
/>
</div>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<!-- Empty State -->
<div v-if="cfg.agents.length === 0" class="d-flex flex-column align-center justify-center py-12 text-medium-emphasis">
<v-icon icon="mdi-robot-off" size="64" class="mb-4 opacity-50" />
<div class="text-h6">No Agents Configured</div>
<div class="text-body-2 mb-4">Add a new sub-agent to get started</div>
<v-btn color="primary" variant="tonal" @click="addAgent">
Create First Agent
</v-btn>
</div>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000" location="top">
{{ snackbar.message }}
<template #actions>
<v-btn variant="text" @click="snackbar.show = false">Close</v-btn>
</template>
</v-snackbar>
</div>
</template>
@@ -158,9 +252,12 @@
import { computed, onMounted, ref } from 'vue'
import axios from 'axios'
import ProviderSelector from '@/components/shared/ProviderSelector.vue'
import PersonaSelector from '@/components/shared/PersonaSelector.vue'
import PersonaQuickPreview from '@/components/shared/PersonaQuickPreview.vue'
import { useModuleI18n } from '@/i18n/composables'
type SubAgentItem = {
__key: string
name: string
persona_id: string
@@ -196,9 +293,6 @@ const cfg = ref<SubAgentConfig>({
agents: []
})
const personaOptions = ref<{ title: string; value: string }[]>([])
const personaLoading = ref(false)
const mainStateDescription = computed(() =>
cfg.value.main_enable ? tm('description.enabled') : tm('description.disabled')
)
@@ -244,24 +338,6 @@ async function loadConfig() {
}
}
async function loadPersonas() {
personaLoading.value = true
try {
const res = await axios.get('/api/persona/list')
if (res.data.status === 'ok') {
const list = Array.isArray(res.data.data) ? res.data.data : []
personaOptions.value = list.map((p: any) => ({
title: p.persona_id,
value: p.persona_id
}))
}
} catch (e: any) {
toast(e?.response?.data?.message || tm('messages.loadPersonaFailed'), 'error')
} finally {
personaLoading.value = false
}
}
function addAgent() {
cfg.value.agents.push({
__key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
@@ -333,7 +409,7 @@ async function save() {
}
async function reload() {
await Promise.all([loadConfig(), loadPersonas()])
await Promise.all([loadConfig()])
}
onMounted(() => {
@@ -343,101 +419,21 @@ onMounted(() => {
<style scoped>
.subagent-page {
padding: 20px;
padding-top: 8px;
padding-bottom: 40px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.subagent-panel-title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
.subagent-panels :deep(.v-expansion-panel-text__wrapper) {
padding: 16px;
padding-bottom: 42px;
}
.subagent-title-left {
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.subagent-title-text {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.subagent-title-name {
font-weight: 600;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 520px;
}
.subagent-title-sub {
font-size: 12px;
opacity: 0.72;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 520px;
}
.subagent-title-right {
display: flex;
align-items: center;
.gap-2 {
gap: 8px;
}
.subagent-actions {
display: flex;
align-items: flex-start;
gap: 14px;
}
.subagent-provider {
flex: 1;
min-width: 260px;
}
.subagent-enabled-inline {
margin-right: 2px;
}
/* Keep the switch compact inside the expansion-panel title row. */
.subagent-enabled-inline :deep(.v-input__details) {
display: none;
}
.subagent-enabled-inline :deep(.v-selection-control) {
min-height: 32px;
}
</style>
<style>
/*
Vuetify renders selected chips inside the input control and will grow the
field height as chips wrap. For subagent tool assignment this quickly becomes
unwieldy, so we cap the chip area height and allow scrolling.
Note: this must be a non-scoped style so it can reach Vuetify's internal
elements.
*/
.subagent-tools .v-field__input {
max-height: 160px;
overflow-y: auto;
align-content: flex-start;
}
/* Small breathing room so the scrollbar doesn't overlap chip close icons. */
.subagent-tools .v-field__input {
padding-right: 6px;
.gap-4 {
gap: 16px;
}
</style>