- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
1067 lines
28 KiB
Vue
1067 lines
28 KiB
Vue
<script setup lang="ts">
|
|
import type {
|
|
SkillMcpToolManifest,
|
|
SkillToolBinding,
|
|
SkillToolOption,
|
|
SkillToolType,
|
|
} from './types';
|
|
|
|
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
|
|
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
|
|
|
import {
|
|
ArrowDown,
|
|
ArrowRight,
|
|
Connection,
|
|
Link,
|
|
Refresh,
|
|
Search,
|
|
Share,
|
|
} from '@element-plus/icons-vue';
|
|
import {
|
|
ElButton,
|
|
ElCheckbox,
|
|
ElEmpty,
|
|
ElInput,
|
|
ElMessage,
|
|
ElSwitch,
|
|
ElTag,
|
|
} from 'element-plus';
|
|
|
|
import {
|
|
getSkillMcpTools,
|
|
getSkillToolOptions,
|
|
updateSkillToolBindings,
|
|
} from './api';
|
|
import { resolveSkillApiErrorMessage } from './skill-api-error';
|
|
|
|
interface McpState {
|
|
data?: SkillMcpToolManifest;
|
|
error?: string;
|
|
expanded?: boolean;
|
|
loading?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
bindings?: SkillToolBinding[];
|
|
hasToolUpdate?: boolean;
|
|
modelValue: boolean;
|
|
skillId: number | string;
|
|
}>(),
|
|
{ bindings: () => [], hasToolUpdate: false },
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
saved: [bindings: SkillToolBinding[]];
|
|
'update:modelValue': [value: boolean];
|
|
}>();
|
|
|
|
const typeOptions: Array<{
|
|
icon: typeof Share;
|
|
label: string;
|
|
value: SkillToolType;
|
|
}> = [
|
|
{ icon: Share, label: '工作流', value: 'WORKFLOW' },
|
|
{ icon: Connection, label: '插件工具', value: 'PLUGIN' },
|
|
{ icon: Link, label: 'MCP', value: 'MCP' },
|
|
];
|
|
const activeType = ref<SkillToolType>('WORKFLOW');
|
|
const keyword = ref('');
|
|
const loading = ref(false);
|
|
const saving = ref(false);
|
|
const loadError = ref('');
|
|
const actionError = ref('');
|
|
const options = ref<SkillToolOption[]>([]);
|
|
const selected = ref<SkillToolBinding[]>([]);
|
|
const mcpStates = reactive<Record<string, McpState>>({});
|
|
const confirmedMcpManifests = reactive<Record<string, string>>({});
|
|
let optionRequest = 0;
|
|
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
const dialogVisible = computed({
|
|
get: () => props.modelValue,
|
|
set: (value) => {
|
|
if (!value && saving.value) return;
|
|
emit('update:modelValue', value);
|
|
},
|
|
});
|
|
const selectedCount = computed(() => selected.value.length);
|
|
const totalToolCount = computed(() =>
|
|
selected.value.reduce(
|
|
(total, binding) => total + bindingToolCount(binding),
|
|
0,
|
|
),
|
|
);
|
|
const hitlCount = computed(
|
|
() => selected.value.filter((binding) => effectiveHitl(binding)).length,
|
|
);
|
|
const invalidBindings = computed(() =>
|
|
selected.value.filter(
|
|
(binding) => binding.resourceSummary?.available === false,
|
|
),
|
|
);
|
|
const unresolvedMcpBindings = computed(() =>
|
|
selected.value.filter((binding) => {
|
|
if (binding.toolType !== 'MCP') return false;
|
|
const state = mcpStates[String(binding.targetId)];
|
|
if (state?.loading || state?.error || !state?.data) return true;
|
|
return !(
|
|
state.data.manifestHash === binding.mcpToolManifestHash ||
|
|
confirmedMcpManifests[String(binding.targetId)] ===
|
|
state.data.manifestHash
|
|
);
|
|
}),
|
|
);
|
|
const saveDisabled = computed(
|
|
() =>
|
|
saving.value ||
|
|
totalToolCount.value > 20 ||
|
|
invalidBindings.value.length > 0 ||
|
|
unresolvedMcpBindings.value.length > 0,
|
|
);
|
|
const currentRows = computed(() => {
|
|
const rows = [...options.value];
|
|
for (const binding of selected.value) {
|
|
if (binding.toolType !== activeType.value) continue;
|
|
if (rows.some((item) => sameTarget(item, binding))) continue;
|
|
rows.push(optionFromBinding(binding));
|
|
}
|
|
return rows;
|
|
});
|
|
|
|
function bindingKey(binding: Pick<SkillToolBinding, 'targetId' | 'toolType'>) {
|
|
return `${binding.toolType}:${binding.targetId}`;
|
|
}
|
|
|
|
function sameTarget(
|
|
left: Pick<SkillToolBinding, 'targetId' | 'toolType'>,
|
|
right: Pick<SkillToolBinding, 'targetId' | 'toolType'>,
|
|
) {
|
|
return bindingKey(left) === bindingKey(right);
|
|
}
|
|
|
|
function cloneBinding(binding: SkillToolBinding): SkillToolBinding {
|
|
return {
|
|
hitlEnabled: Boolean(binding.hitlEnabled),
|
|
id: binding.id,
|
|
mcpToolCount: binding.mcpToolCount,
|
|
mcpToolManifestHash: binding.mcpToolManifestHash,
|
|
resourceSummary: binding.resourceSummary
|
|
? { ...binding.resourceSummary }
|
|
: undefined,
|
|
sortNo: binding.sortNo,
|
|
targetId: binding.targetId,
|
|
toolType: binding.toolType,
|
|
};
|
|
}
|
|
|
|
function optionFromBinding(binding: SkillToolBinding): SkillToolOption {
|
|
return {
|
|
approvalRequired: binding.resourceSummary?.approvalRequired,
|
|
available: binding.resourceSummary?.available !== false,
|
|
description: binding.resourceSummary?.description,
|
|
knownToolCount:
|
|
binding.mcpToolCount ?? binding.resourceSummary?.toolCount ?? 1,
|
|
targetId: binding.targetId,
|
|
title: binding.resourceSummary?.title || '当前资源不可用',
|
|
toolType: binding.toolType,
|
|
};
|
|
}
|
|
|
|
function selectedBinding(item: SkillToolOption) {
|
|
return selected.value.find((binding) => sameTarget(binding, item));
|
|
}
|
|
|
|
function bindingTitle(binding: SkillToolBinding) {
|
|
const option = options.value.find((item) => sameTarget(item, binding));
|
|
return (
|
|
option?.title ||
|
|
binding.resourceSummary?.title ||
|
|
typeLabel(binding.toolType)
|
|
);
|
|
}
|
|
|
|
function typeLabel(type: SkillToolType) {
|
|
return typeOptions.find((item) => item.value === type)?.label || type;
|
|
}
|
|
|
|
function effectiveHitl(binding: SkillToolBinding) {
|
|
return Boolean(
|
|
binding.hitlEnabled || binding.resourceSummary?.approvalRequired,
|
|
);
|
|
}
|
|
|
|
function bindingToolCount(binding: SkillToolBinding) {
|
|
if (binding.toolType !== 'MCP') return 1;
|
|
return Math.max(
|
|
0,
|
|
mcpStates[String(binding.targetId)]?.data?.toolCount ??
|
|
binding.mcpToolCount ??
|
|
binding.resourceSummary?.toolCount ??
|
|
0,
|
|
);
|
|
}
|
|
|
|
function mcpState(targetId: number | string) {
|
|
const key = String(targetId);
|
|
return (mcpStates[key] ||= {});
|
|
}
|
|
|
|
function mcpManifestChanged(binding: SkillToolBinding) {
|
|
const data = mcpState(binding.targetId).data;
|
|
return Boolean(
|
|
data?.manifestHash && data.manifestHash !== binding.mcpToolManifestHash,
|
|
);
|
|
}
|
|
|
|
function resetDialog() {
|
|
activeType.value = 'WORKFLOW';
|
|
keyword.value = '';
|
|
actionError.value = '';
|
|
selected.value = props.bindings.map((binding) => cloneBinding(binding));
|
|
for (const key of Object.keys(mcpStates)) delete mcpStates[key];
|
|
for (const key of Object.keys(confirmedMcpManifests))
|
|
delete confirmedMcpManifests[key];
|
|
void loadOptions();
|
|
for (const binding of selected.value) {
|
|
if (binding.toolType === 'MCP') void loadMcp(binding.targetId);
|
|
}
|
|
}
|
|
|
|
async function loadOptions() {
|
|
const request = ++optionRequest;
|
|
loading.value = true;
|
|
loadError.value = '';
|
|
try {
|
|
const response = await getSkillToolOptions({
|
|
keyword: keyword.value.trim() || undefined,
|
|
pageNum: 1,
|
|
pageSize: 100,
|
|
toolType: activeType.value,
|
|
});
|
|
if (request !== optionRequest) return;
|
|
if (response.errorCode !== 0) throw response;
|
|
options.value = response.data?.records || [];
|
|
} catch (error) {
|
|
if (request !== optionRequest) return;
|
|
options.value = [];
|
|
loadError.value = resolveSkillApiErrorMessage(
|
|
error,
|
|
'候选加载失败,请重试',
|
|
);
|
|
} finally {
|
|
if (request === optionRequest) loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function loadMcp(targetId: number | string, force = false) {
|
|
const state = mcpState(targetId);
|
|
if (state.loading || (!force && state.data)) return state.data;
|
|
state.loading = true;
|
|
state.error = '';
|
|
try {
|
|
const response = await getSkillMcpTools(targetId);
|
|
if (response.errorCode !== 0) throw response;
|
|
state.data = response.data;
|
|
return state.data;
|
|
} catch (error) {
|
|
state.error = resolveSkillApiErrorMessage(
|
|
error,
|
|
'工具清单加载失败,请重试',
|
|
);
|
|
} finally {
|
|
state.loading = false;
|
|
}
|
|
}
|
|
|
|
async function toggleCandidate(item: SkillToolOption) {
|
|
if (!item.available || saving.value) return;
|
|
const existing = selectedBinding(item);
|
|
if (existing) {
|
|
selected.value = selected.value.filter(
|
|
(binding) => !sameTarget(binding, item),
|
|
);
|
|
return;
|
|
}
|
|
let manifest: SkillMcpToolManifest | undefined;
|
|
if (item.toolType === 'MCP') {
|
|
manifest = await loadMcp(item.targetId);
|
|
if (!manifest) return;
|
|
confirmedMcpManifests[String(item.targetId)] = manifest.manifestHash;
|
|
}
|
|
selected.value.push({
|
|
hitlEnabled: Boolean(item.approvalRequired),
|
|
mcpToolCount: manifest?.toolCount,
|
|
mcpToolManifestHash: manifest?.manifestHash,
|
|
resourceSummary: {
|
|
approvalRequired: item.approvalRequired,
|
|
available: item.available,
|
|
description: item.description,
|
|
title: item.title,
|
|
toolCount: manifest?.toolCount ?? item.knownToolCount ?? 1,
|
|
},
|
|
sortNo: selected.value.length,
|
|
targetId: item.targetId,
|
|
toolType: item.toolType,
|
|
});
|
|
}
|
|
|
|
function removeBinding(binding: SkillToolBinding) {
|
|
selected.value = selected.value.filter((item) => !sameTarget(item, binding));
|
|
}
|
|
|
|
async function toggleMcpExpanded(targetId: number | string) {
|
|
const state = mcpState(targetId);
|
|
state.expanded = !state.expanded;
|
|
if (state.expanded) await loadMcp(targetId);
|
|
}
|
|
|
|
function acceptLatestManifest(binding: SkillToolBinding) {
|
|
const data = mcpState(binding.targetId).data;
|
|
if (!data) return;
|
|
confirmedMcpManifests[String(binding.targetId)] = data.manifestHash;
|
|
selected.value = selected.value.map((item) =>
|
|
sameTarget(item, binding)
|
|
? {
|
|
...item,
|
|
mcpToolCount: data.toolCount,
|
|
mcpToolManifestHash: data.manifestHash,
|
|
resourceSummary: item.resourceSummary
|
|
? { ...item.resourceSummary, toolCount: data.toolCount }
|
|
: item.resourceSummary,
|
|
}
|
|
: item,
|
|
);
|
|
}
|
|
|
|
async function save() {
|
|
if (saveDisabled.value) return;
|
|
saving.value = true;
|
|
actionError.value = '';
|
|
try {
|
|
const response = await updateSkillToolBindings(
|
|
props.skillId,
|
|
selected.value,
|
|
);
|
|
if (response.errorCode !== 0) throw response;
|
|
ElMessage.success('工具绑定已保存');
|
|
emit('saved', response.data || []);
|
|
emit('update:modelValue', false);
|
|
} catch (error) {
|
|
actionError.value = resolveSkillApiErrorMessage(error, '保存失败,请重试');
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.modelValue,
|
|
(visible) => {
|
|
if (visible) resetDialog();
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
watch(activeType, () => {
|
|
if (props.modelValue) void loadOptions();
|
|
});
|
|
watch(keyword, () => {
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(() => {
|
|
if (props.modelValue) void loadOptions();
|
|
}, 250);
|
|
});
|
|
onBeforeUnmount(() => {
|
|
if (searchTimer) clearTimeout(searchTimer);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<EasyFlowPanelModal
|
|
v-model:open="dialogVisible"
|
|
centered
|
|
:closable="!saving"
|
|
:close-on-click-modal="false"
|
|
:show-footer="false"
|
|
title="工具绑定"
|
|
width="xl"
|
|
>
|
|
<div class="skill-tools" :aria-busy="loading || saving">
|
|
<div class="skill-tools__summary">
|
|
<div class="skill-tools__metrics" aria-live="polite">
|
|
<strong>{{ selectedCount }}</strong> 项资源
|
|
<span aria-hidden="true">·</span>
|
|
<strong :class="{ 'is-overflow': totalToolCount > 20 }">
|
|
{{ totalToolCount }}
|
|
</strong>
|
|
/ 20 个工具
|
|
<span aria-hidden="true">·</span>
|
|
{{ hitlCount }} 项调用前确认
|
|
</div>
|
|
<ElTag v-if="hasToolUpdate" type="warning" effect="plain" size="small">
|
|
有未发布变更
|
|
</ElTag>
|
|
</div>
|
|
|
|
<div v-if="actionError" class="skill-tools__error" role="alert">
|
|
<span>{{ actionError }}</span>
|
|
<ElButton link type="primary" @click="actionError = ''">关闭</ElButton>
|
|
</div>
|
|
|
|
<div class="skill-tools__body">
|
|
<section class="skill-tools__catalog" aria-label="可绑定工具">
|
|
<div class="skill-tools__toolbar">
|
|
<div
|
|
class="skill-tools__types"
|
|
role="tablist"
|
|
aria-label="工具类型"
|
|
>
|
|
<button
|
|
v-for="type in typeOptions"
|
|
:key="type.value"
|
|
class="skill-tools__type"
|
|
:class="{ 'is-active': activeType === type.value }"
|
|
role="tab"
|
|
type="button"
|
|
:aria-selected="activeType === type.value"
|
|
@click="activeType = type.value"
|
|
>
|
|
<component :is="type.icon" />
|
|
{{ type.label }}
|
|
</button>
|
|
</div>
|
|
<ElInput
|
|
v-model="keyword"
|
|
clearable
|
|
:prefix-icon="Search"
|
|
placeholder="搜索名称或描述"
|
|
aria-label="搜索工具"
|
|
/>
|
|
</div>
|
|
|
|
<div v-loading="loading" class="skill-tools__list">
|
|
<div v-if="loadError" class="skill-tools__state" role="alert">
|
|
<span>{{ loadError }}</span>
|
|
<ElButton :icon="Refresh" @click="loadOptions">重新加载</ElButton>
|
|
</div>
|
|
<ElEmpty
|
|
v-else-if="!loading && currentRows.length === 0"
|
|
:description="keyword ? '没有匹配的工具' : '暂无可绑定工具'"
|
|
:image-size="72"
|
|
/>
|
|
<article
|
|
v-for="item in currentRows"
|
|
v-else
|
|
:key="bindingKey(item)"
|
|
class="skill-tools__candidate"
|
|
:class="{
|
|
'is-disabled': !item.available,
|
|
'is-selected': Boolean(selectedBinding(item)),
|
|
}"
|
|
>
|
|
<div class="skill-tools__candidate-main">
|
|
<ElCheckbox
|
|
:aria-label="`${selectedBinding(item) ? '取消绑定' : '绑定'}${item.title}`"
|
|
:disabled="!item.available || mcpState(item.targetId).loading"
|
|
:model-value="Boolean(selectedBinding(item))"
|
|
@change="toggleCandidate(item)"
|
|
/>
|
|
<button
|
|
class="skill-tools__candidate-content"
|
|
type="button"
|
|
:disabled="!item.available"
|
|
@click="toggleCandidate(item)"
|
|
>
|
|
<span class="skill-tools__candidate-title">{{
|
|
item.title
|
|
}}</span>
|
|
<span class="skill-tools__candidate-description">
|
|
{{ item.description || '暂无描述' }}
|
|
</span>
|
|
</button>
|
|
<div class="skill-tools__candidate-meta">
|
|
<ElTag
|
|
v-if="!item.available"
|
|
type="danger"
|
|
size="small"
|
|
effect="plain"
|
|
>
|
|
已失效
|
|
</ElTag>
|
|
<ElTag
|
|
v-if="item.approvalRequired"
|
|
type="warning"
|
|
size="small"
|
|
effect="plain"
|
|
>
|
|
需确认
|
|
</ElTag>
|
|
<span v-if="item.toolType !== 'MCP'">1 个工具</span>
|
|
<span v-else-if="mcpState(item.targetId).data">
|
|
{{ mcpState(item.targetId).data?.toolCount }} 个工具
|
|
</span>
|
|
<ElButton
|
|
v-if="item.toolType === 'MCP'"
|
|
link
|
|
type="primary"
|
|
:loading="mcpState(item.targetId).loading"
|
|
:aria-expanded="Boolean(mcpState(item.targetId).expanded)"
|
|
@click="toggleMcpExpanded(item.targetId)"
|
|
>
|
|
<component
|
|
:is="
|
|
mcpState(item.targetId).expanded
|
|
? ArrowDown
|
|
: ArrowRight
|
|
"
|
|
/>
|
|
查看工具
|
|
</ElButton>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="
|
|
item.toolType === 'MCP' && mcpState(item.targetId).expanded
|
|
"
|
|
class="skill-tools__mcp-tools"
|
|
>
|
|
<div
|
|
v-if="mcpState(item.targetId).error"
|
|
class="skill-tools__mcp-error"
|
|
>
|
|
<span>{{ mcpState(item.targetId).error }}</span>
|
|
<ElButton
|
|
link
|
|
type="primary"
|
|
@click="loadMcp(item.targetId, true)"
|
|
>
|
|
重试
|
|
</ElButton>
|
|
</div>
|
|
<div
|
|
v-for="tool in mcpState(item.targetId).data?.tools || []"
|
|
:key="tool.name"
|
|
class="skill-tools__mcp-tool"
|
|
>
|
|
<span>{{ tool.name }}</span>
|
|
<small>{{ tool.description || '暂无描述' }}</small>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
|
|
<aside class="skill-tools__selected" aria-label="已绑定工具">
|
|
<div class="skill-tools__selected-heading">
|
|
<div>
|
|
<strong>已绑定</strong>
|
|
<span>{{ selectedCount }} 项</span>
|
|
</div>
|
|
<span>调用前确认可逐项设置</span>
|
|
</div>
|
|
<ElEmpty
|
|
v-if="selected.length === 0"
|
|
description="从左侧选择工具"
|
|
:image-size="64"
|
|
/>
|
|
<div v-else class="skill-tools__selected-list">
|
|
<article
|
|
v-for="binding in selected"
|
|
:key="bindingKey(binding)"
|
|
class="skill-tools__selected-item"
|
|
>
|
|
<div class="skill-tools__selected-title">
|
|
<div>
|
|
<strong>{{ bindingTitle(binding) }}</strong>
|
|
<span>
|
|
{{ typeLabel(binding.toolType) }} ·
|
|
{{ bindingToolCount(binding) }} 个工具
|
|
</span>
|
|
</div>
|
|
<ElButton link type="danger" @click="removeBinding(binding)">
|
|
移除
|
|
</ElButton>
|
|
</div>
|
|
<div class="skill-tools__selected-state">
|
|
<ElTag
|
|
v-if="binding.resourceSummary?.available === false"
|
|
type="danger"
|
|
size="small"
|
|
effect="plain"
|
|
>
|
|
已失效
|
|
</ElTag>
|
|
<ElTag
|
|
v-if="
|
|
binding.toolType === 'MCP' && mcpManifestChanged(binding)
|
|
"
|
|
type="warning"
|
|
size="small"
|
|
effect="plain"
|
|
>
|
|
清单变化
|
|
</ElTag>
|
|
<ElButton
|
|
v-if="
|
|
binding.toolType === 'MCP' && mcpManifestChanged(binding)
|
|
"
|
|
link
|
|
type="primary"
|
|
@click="acceptLatestManifest(binding)"
|
|
>
|
|
确认最新清单
|
|
</ElButton>
|
|
<span v-if="mcpState(binding.targetId).error" class="is-error">
|
|
清单加载失败
|
|
</span>
|
|
</div>
|
|
<label class="skill-tools__hitl">
|
|
<span>
|
|
调用前确认
|
|
<small v-if="binding.resourceSummary?.approvalRequired">
|
|
MCP 要求
|
|
</small>
|
|
</span>
|
|
<ElSwitch
|
|
v-model="binding.hitlEnabled"
|
|
:disabled="Boolean(binding.resourceSummary?.approvalRequired)"
|
|
:aria-label="`${bindingTitle(binding)}调用前确认`"
|
|
/>
|
|
</label>
|
|
</article>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
|
|
<footer class="skill-tools__footer">
|
|
<div class="skill-tools__footer-message" aria-live="polite">
|
|
<span v-if="totalToolCount > 20" class="is-error">
|
|
每个 Skill 最多绑定 20 个工具
|
|
</span>
|
|
<span v-else-if="invalidBindings.length > 0" class="is-error">
|
|
请移除已失效的绑定
|
|
</span>
|
|
<span v-else-if="unresolvedMcpBindings.length > 0">
|
|
请等待 MCP 清单加载,并确认发生变化的清单
|
|
</span>
|
|
<span v-else>保存后随下一次发布生效</span>
|
|
</div>
|
|
<div class="skill-tools__footer-actions">
|
|
<ElButton :disabled="saving" @click="dialogVisible = false">
|
|
取消
|
|
</ElButton>
|
|
<ElButton
|
|
type="primary"
|
|
:disabled="saveDisabled"
|
|
:loading="saving"
|
|
@click="save"
|
|
>
|
|
保存绑定
|
|
</ElButton>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</EasyFlowPanelModal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.skill-tools {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: min(720px, calc(100vh - 96px));
|
|
min-height: 560px;
|
|
color: hsl(var(--text-strong));
|
|
}
|
|
|
|
.skill-tools__summary,
|
|
.skill-tools__toolbar,
|
|
.skill-tools__selected-heading,
|
|
.skill-tools__footer,
|
|
.skill-tools__candidate-main,
|
|
.skill-tools__selected-title,
|
|
.skill-tools__selected-state,
|
|
.skill-tools__hitl {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.skill-tools__summary {
|
|
justify-content: space-between;
|
|
padding: var(--space-3) var(--space-5);
|
|
color: hsl(var(--text-muted));
|
|
background: hsl(var(--surface-subtle));
|
|
border-bottom: 1px solid hsl(var(--line-subtle));
|
|
}
|
|
|
|
.skill-tools__metrics {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.skill-tools__metrics strong {
|
|
color: hsl(var(--text-strong));
|
|
}
|
|
|
|
.skill-tools__body {
|
|
display: grid;
|
|
flex: 1;
|
|
min-height: 0;
|
|
grid-template-columns: minmax(0, 1fr) 320px;
|
|
}
|
|
|
|
.skill-tools__catalog,
|
|
.skill-tools__selected {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
}
|
|
|
|
.skill-tools__selected {
|
|
background: hsl(var(--surface-subtle));
|
|
border-left: 1px solid hsl(var(--line-subtle));
|
|
}
|
|
|
|
.skill-tools__toolbar {
|
|
gap: var(--space-3);
|
|
padding: var(--space-4);
|
|
border-bottom: 1px solid hsl(var(--line-subtle));
|
|
}
|
|
|
|
.skill-tools__toolbar :deep(.el-input) {
|
|
width: min(260px, 42%);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.skill-tools__types {
|
|
display: inline-flex;
|
|
gap: var(--space-1);
|
|
padding: var(--space-1);
|
|
background: hsl(var(--surface-contrast-soft));
|
|
border-radius: var(--radius-toolbar);
|
|
}
|
|
|
|
.skill-tools__type {
|
|
display: inline-flex;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
height: 32px;
|
|
padding: 0 var(--space-3);
|
|
font-weight: 600;
|
|
color: hsl(var(--text-muted));
|
|
cursor: pointer;
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: var(--radius-control);
|
|
transition:
|
|
color var(--motion-duration-base) var(--motion-ease-standard),
|
|
background var(--motion-duration-base) var(--motion-ease-standard);
|
|
}
|
|
|
|
.skill-tools__type svg {
|
|
width: 15px;
|
|
height: 15px;
|
|
}
|
|
|
|
.skill-tools__type:hover {
|
|
color: hsl(var(--text-strong));
|
|
background: hsl(var(--table-row-hover));
|
|
}
|
|
|
|
.skill-tools__type.is-active {
|
|
color: hsl(var(--nav-item-active-foreground));
|
|
background: hsl(var(--nav-item-active));
|
|
}
|
|
|
|
.skill-tools__type:focus-visible,
|
|
.skill-tools__candidate-content:focus-visible {
|
|
outline: 2px solid hsl(var(--primary));
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.skill-tools__list,
|
|
.skill-tools__selected-list {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.skill-tools__list {
|
|
padding: var(--space-2) var(--space-4) var(--space-4);
|
|
}
|
|
|
|
.skill-tools__candidate {
|
|
border-bottom: 1px solid hsl(var(--line-subtle));
|
|
transition: background var(--motion-duration-fast) var(--motion-ease-standard);
|
|
}
|
|
|
|
.skill-tools__candidate:not(.is-disabled):hover {
|
|
background: hsl(var(--table-row-hover));
|
|
}
|
|
|
|
.skill-tools__candidate.is-selected {
|
|
background: hsl(var(--nav-item-active) / 0.55);
|
|
}
|
|
|
|
.skill-tools__candidate.is-selected:hover {
|
|
background: hsl(var(--nav-item-active) / 0.78);
|
|
}
|
|
|
|
.skill-tools__candidate.is-disabled {
|
|
background: hsl(var(--surface-subtle));
|
|
}
|
|
|
|
.skill-tools__candidate-main {
|
|
gap: var(--space-3);
|
|
min-height: 72px;
|
|
padding: var(--space-3);
|
|
}
|
|
|
|
.skill-tools__candidate-content {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
gap: var(--space-1);
|
|
min-width: 0;
|
|
padding: 0;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
border: 0;
|
|
}
|
|
|
|
.skill-tools__candidate-content:disabled {
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.skill-tools__candidate-title {
|
|
overflow: hidden;
|
|
font-weight: 650;
|
|
color: hsl(var(--text-strong));
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.skill-tools__candidate-description {
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
color: hsl(var(--text-muted));
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.skill-tools__candidate-meta {
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
font-size: 12px;
|
|
color: hsl(var(--text-muted));
|
|
}
|
|
|
|
.skill-tools__candidate-meta :deep(.el-button) svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.skill-tools__mcp-tools {
|
|
display: grid;
|
|
gap: var(--space-2);
|
|
padding: 0 var(--space-3) var(--space-3) 48px;
|
|
}
|
|
|
|
.skill-tools__mcp-tool {
|
|
display: grid;
|
|
grid-template-columns: minmax(128px, 0.45fr) minmax(0, 1fr);
|
|
gap: var(--space-3);
|
|
padding: var(--space-2) var(--space-3);
|
|
background: hsl(var(--surface-subtle));
|
|
border-radius: var(--radius-control);
|
|
}
|
|
|
|
.skill-tools__mcp-tool span {
|
|
overflow: hidden;
|
|
font-family: ui-monospace, sfmono-regular, menlo, monospace;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: hsl(var(--text-strong));
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.skill-tools__mcp-tool small {
|
|
color: hsl(var(--text-muted));
|
|
}
|
|
|
|
.skill-tools__mcp-error,
|
|
.skill-tools__error,
|
|
.skill-tools__state {
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.skill-tools__error {
|
|
padding: var(--space-2) var(--space-5);
|
|
font-size: 13px;
|
|
color: hsl(var(--destructive));
|
|
background: hsl(var(--destructive) / 0.08);
|
|
border-bottom: 1px solid hsl(var(--destructive) / 0.16);
|
|
}
|
|
|
|
.skill-tools__state {
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
min-height: 240px;
|
|
color: hsl(var(--text-muted));
|
|
}
|
|
|
|
.skill-tools__selected-heading {
|
|
justify-content: space-between;
|
|
min-height: 65px;
|
|
padding: var(--space-3) var(--space-4);
|
|
border-bottom: 1px solid hsl(var(--line-subtle));
|
|
}
|
|
|
|
.skill-tools__selected-heading > div {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
align-items: baseline;
|
|
}
|
|
|
|
.skill-tools__selected-heading > span,
|
|
.skill-tools__selected-heading div span {
|
|
font-size: 12px;
|
|
color: hsl(var(--text-muted));
|
|
}
|
|
|
|
.skill-tools__selected-list {
|
|
padding: var(--space-2);
|
|
}
|
|
|
|
.skill-tools__selected-item {
|
|
padding: var(--space-3);
|
|
background: hsl(var(--surface-panel));
|
|
border-bottom: 1px solid hsl(var(--line-subtle));
|
|
}
|
|
|
|
.skill-tools__selected-item:first-child {
|
|
border-radius: var(--radius-control) var(--radius-control) 0 0;
|
|
}
|
|
|
|
.skill-tools__selected-item:last-child {
|
|
border-bottom: 0;
|
|
border-radius: 0 0 var(--radius-control) var(--radius-control);
|
|
}
|
|
|
|
.skill-tools__selected-title {
|
|
gap: var(--space-2);
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.skill-tools__selected-title > div {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.skill-tools__selected-title strong {
|
|
overflow: hidden;
|
|
font-size: 13px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.skill-tools__selected-title span,
|
|
.skill-tools__hitl small {
|
|
font-size: 11px;
|
|
color: hsl(var(--text-muted));
|
|
}
|
|
|
|
.skill-tools__selected-state {
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
min-height: var(--space-3);
|
|
margin-top: var(--space-2);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.skill-tools__hitl {
|
|
justify-content: space-between;
|
|
margin-top: var(--space-3);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.skill-tools__hitl > span {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.skill-tools__footer {
|
|
justify-content: space-between;
|
|
min-height: 64px;
|
|
padding: var(--space-3) var(--space-5);
|
|
background: hsl(var(--modal-footer-surface));
|
|
border-top: 1px solid hsl(var(--modal-divider));
|
|
}
|
|
|
|
.skill-tools__footer-message {
|
|
font-size: 12px;
|
|
color: hsl(var(--text-muted));
|
|
}
|
|
|
|
.skill-tools__footer-actions {
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.is-error,
|
|
.is-overflow {
|
|
color: hsl(var(--destructive)) !important;
|
|
}
|
|
|
|
@media (max-width: 820px) {
|
|
.skill-tools {
|
|
min-height: min(680px, calc(100vh - 48px));
|
|
}
|
|
|
|
.skill-tools__body {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
overflow: auto;
|
|
}
|
|
|
|
.skill-tools__catalog,
|
|
.skill-tools__selected {
|
|
min-height: 360px;
|
|
}
|
|
|
|
.skill-tools__selected {
|
|
border-top: 1px solid hsl(var(--line-subtle));
|
|
border-left: 0;
|
|
}
|
|
|
|
.skill-tools__toolbar {
|
|
align-items: stretch;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.skill-tools__toolbar :deep(.el-input) {
|
|
width: 100%;
|
|
}
|
|
|
|
.skill-tools__types {
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.skill-tools__type {
|
|
flex: 1;
|
|
justify-content: center;
|
|
}
|
|
}
|
|
</style>
|