Skip to content

Commit feb0d79

Browse files
author
linyuan.yang
committed
skill explorer
1 parent 50ae868 commit feb0d79

29 files changed

Lines changed: 1215 additions & 656 deletions

File tree

Lines changed: 20 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,31 @@
11
<script setup lang="ts">
2-
import { ref, computed } from 'vue'
2+
import { ref } from 'vue'
33
import { useI18n } from 'vue-i18n'
4-
import { apiFetch } from '@/shared/api'
5-
import { useToast } from 'sbot-ui'
4+
import { useToast, SModal, SButton } from 'sbot-ui'
5+
import { FileExplorer, WebSocketTransport } from '@sbot/chat-ui'
66
import { sourceBadgeStyle } from '@/utils/badges'
7-
import { SModal, SButton, STree, STreeNode } from 'sbot-ui'
87
98
const { t } = useI18n()
109
const { show } = useToast()
1110
12-
// ── State ────────────────────────────────────────────────────────
1311
const visible = ref(false)
1412
const skillName = ref('')
1513
const skillBadge = ref('')
16-
const skillApiBase = ref('/api/skills')
17-
18-
// ── Tree ─────────────────────────────────────────────────────────
19-
type TreeNode = { name: string; type: 'file' | 'dir'; path: string; children?: TreeNode[] }
20-
21-
const tree = ref<TreeNode[]>([])
22-
const collapsed = ref(new Set<string>())
23-
24-
function flattenTree(nodes: TreeNode[], depth = 0): { node: TreeNode; depth: number }[] {
25-
const result: { node: TreeNode; depth: number }[] = []
26-
for (const node of nodes) {
27-
result.push({ node, depth })
28-
if (node.type === 'dir' && !collapsed.value.has(node.path)) {
29-
result.push(...flattenTree(node.children || [], depth + 1))
30-
}
14+
const skillRootId = ref('')
15+
const viewerKey = ref(0)
16+
const transport = new WebSocketTransport()
17+
const initialViewState = { expandedPaths: [], selectedPath: 'SKILL.md' }
18+
19+
function open(name: string, badge: string, rootId: string) {
20+
if (!rootId) {
21+
show('Missing skill file root id', 'error')
22+
return
3123
}
32-
return result
33-
}
34-
35-
const flatTree = computed(() => flattenTree(tree.value))
36-
37-
function toggleDir(p: string) {
38-
if (collapsed.value.has(p)) collapsed.value.delete(p)
39-
else collapsed.value.add(p)
40-
collapsed.value = new Set(collapsed.value)
41-
}
42-
43-
// ── File viewer ──────────────────────────────────────────────────
44-
const selectedPath = ref('')
45-
const fileContent = ref('')
46-
const fileLoading = ref(false)
47-
const treeLoading = ref(false)
48-
49-
async function selectFile(p: string) {
50-
if (selectedPath.value === p) return
51-
selectedPath.value = p
52-
fileLoading.value = true
53-
fileContent.value = ''
54-
try {
55-
const res = await apiFetch(`${skillApiBase.value}/${encodeURIComponent(skillName.value)}/file?path=${encodeURIComponent(p)}`)
56-
fileContent.value = res.data?.content ?? ''
57-
} catch (e: any) {
58-
show(e.message, 'error')
59-
} finally {
60-
fileLoading.value = false
61-
}
62-
}
63-
64-
// ── Public API ───────────────────────────────────────────────────
65-
async function open(name: string, badge: string, apiBase = '/api/skills') {
6624
skillName.value = name
6725
skillBadge.value = badge
68-
skillApiBase.value = apiBase
69-
tree.value = []
70-
selectedPath.value = ''
71-
fileContent.value = ''
72-
collapsed.value = new Set()
73-
treeLoading.value = true
26+
skillRootId.value = rootId
27+
viewerKey.value += 1
7428
visible.value = true
75-
76-
try {
77-
const res = await apiFetch(`${apiBase}/${encodeURIComponent(name)}/tree`)
78-
tree.value = res.data || []
79-
// Auto-select SKILL.md
80-
const skillMd = flattenTree(tree.value).find(({ node }) => node.type === 'file' && node.name === 'SKILL.md')
81-
if (skillMd) {
82-
selectFile(skillMd.node.path)
83-
}
84-
} catch (e: any) {
85-
show(e.message, 'error')
86-
} finally {
87-
treeLoading.value = false
88-
}
8929
}
9030
9131
function close() {
@@ -105,36 +45,12 @@ defineExpose({ open })
10545
</template>
10646

10747
<div class="skill-viewer-body">
108-
<!-- Left: file tree -->
109-
<STree :header="t('common.files')" class="skill-tree">
110-
<div v-if="treeLoading" class="skill-tree-loading">{{ t('common.loading') }}</div>
111-
<template v-else>
112-
<STreeNode
113-
v-for="{ node, depth } in flatTree"
114-
:key="node.path"
115-
:level="depth"
116-
:type="node.type"
117-
:selected="selectedPath === node.path"
118-
:expandable="node.type === 'dir'"
119-
:expanded="node.type === 'dir' && !collapsed.has(node.path)"
120-
@click="node.type === 'dir' ? toggleDir(node.path) : selectFile(node.path)"
121-
>
122-
{{ node.name }}
123-
</STreeNode>
124-
</template>
125-
</STree>
126-
127-
<!-- Right: file content -->
128-
<div class="skill-content">
129-
<template v-if="selectedPath">
130-
<div class="skill-content-toolbar">
131-
<span class="skill-file-path">{{ selectedPath }}</span>
132-
</div>
133-
<div v-if="fileLoading" class="skill-content-loading">{{ t('common.loading') }}</div>
134-
<pre v-else class="skill-content-pre">{{ fileContent }}</pre>
135-
</template>
136-
<div v-else class="skill-content-empty">{{ t('skills.select_file') }}</div>
137-
</div>
48+
<FileExplorer
49+
:key="viewerKey"
50+
:transport="transport"
51+
:root-id="skillRootId"
52+
:view-state="initialViewState"
53+
/>
13854
</div>
13955

14056
<template #footer>
@@ -145,78 +61,8 @@ defineExpose({ open })
14561

14662
<style scoped>
14763
.skill-viewer-body {
148-
display: flex;
14964
height: 70vh;
15065
overflow: hidden;
15166
margin: calc(-1 * var(--sui-sp-7)) calc(-1 * var(--sui-sp-8));
15267
}
153-
.skill-tree {
154-
width: 210px;
155-
flex-shrink: 0;
156-
}
157-
.skill-tree-loading {
158-
padding: 20px;
159-
text-align: center;
160-
color: var(--sui-fg-disabled);
161-
font-size: var(--sui-fs-md);
162-
}
163-
164-
.skill-content {
165-
flex: 1;
166-
display: flex;
167-
flex-direction: column;
168-
overflow: hidden;
169-
}
170-
.skill-content-toolbar {
171-
display: flex;
172-
align-items: center;
173-
gap: var(--sui-sp-3);
174-
padding: var(--sui-sp-3) var(--sui-sp-4);
175-
border-bottom: 1px solid var(--sui-border);
176-
flex-shrink: 0;
177-
background: var(--sui-bg);
178-
}
179-
.skill-file-path {
180-
font-family: var(--sui-font-mono);
181-
font-size: var(--sui-fs-sm);
182-
color: var(--sui-fg-secondary);
183-
}
184-
.skill-content-loading {
185-
flex: 1;
186-
display: flex;
187-
align-items: center;
188-
justify-content: center;
189-
color: var(--sui-fg-disabled);
190-
font-size: var(--sui-fs-md);
191-
}
192-
.skill-content-pre {
193-
margin: 0;
194-
padding: var(--sui-sp-4) var(--sui-sp-5);
195-
font-family: var(--sui-font-mono);
196-
font-size: var(--sui-fs-sm);
197-
line-height: 1.6;
198-
white-space: pre-wrap;
199-
word-break: break-word;
200-
color: var(--sui-fg);
201-
overflow: auto;
202-
flex: 1;
203-
}
204-
.skill-content-empty {
205-
flex: 1;
206-
display: flex;
207-
align-items: center;
208-
justify-content: center;
209-
color: var(--sui-fg-disabled);
210-
font-size: var(--sui-fs-md);
211-
}
212-
213-
@media (max-width: 768px) {
214-
.skill-viewer-body { flex-direction: column; }
215-
.skill-tree {
216-
width: 100%;
217-
max-height: 180px;
218-
border-right: none;
219-
border-bottom: 1px solid var(--sui-border);
220-
}
221-
}
22268
</style>

packages/admin/src/views/admin/ExplorerView.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ const { t } = useI18n()
99
1010
const STORAGE_KEY = 'explorer-root-path'
1111
const root = ref<string>('')
12+
const rootId = ref<string>('')
1213
const picker = ref<InstanceType<typeof PathPickerModal> | null>(null)
1314
const transport = new WebSocketTransport()
1415
1516
function openPicker() {
1617
picker.value?.open(root.value)
1718
}
1819
19-
function onPicked(p: string) {
20+
function onPicked(p: string, id: string) {
2021
root.value = p
22+
rootId.value = id
2123
localStorage.setItem(STORAGE_KEY, p)
2224
}
2325
@@ -36,7 +38,7 @@ onMounted(() => {
3638
</SButton>
3739
</SPageToolbar>
3840

39-
<Explorer :transport="transport" :root="root" />
41+
<Explorer :transport="transport" :root="root" :root-id="rootId" />
4042

4143
<PathPickerModal ref="picker" @confirm="onPicked" />
4244
</div>

packages/admin/src/views/agents/AgentsView.vue

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,9 @@ async function removeAgent(id: string) {
123123
124124
const skillViewRef = ref<InstanceType<typeof SkillViewerModal>>()
125125
126-
function openSkillView(agentId: string, name: string, isPrivate: boolean) {
127-
const badge = isPrivate ? t('agents.skills_exclusive_tab') : ''
128-
const apiBase = isPrivate ? `/api/agents/${encodeURIComponent(agentId)}/skills` : '/api/skills'
129-
skillViewRef.value?.open(name, badge, apiBase)
126+
function openSkillView(skill: SkillItem, isPrivate: boolean) {
127+
const badge = isPrivate ? t('agents.skills_exclusive_tab') : (skill.source || '')
128+
skillViewRef.value?.open(skill.name, badge, skill.id || '')
130129
}
131130
132131
const showToolsModal = ref(false)
@@ -440,7 +439,7 @@ async function saveMcpParams() {
440439
</template>
441440
<template #desc="{ row: s }"><span class="cell-desc">{{ s.description || '-' }}</span></template>
442441
<template #ops="{ row: s }">
443-
<SButton type="outline" size="sm" @click="openSkillView(row.id, s.name, s._private)">{{ t('common.view') }}</SButton>
442+
<SButton type="outline" size="sm" @click="openSkillView(s, s._private)">{{ t('common.view') }}</SButton>
444443
</template>
445444
<template #_empty>
446445
<div>{{ t('agents.no_skills') }}</div>

packages/admin/src/views/agents/modals/AgentSkillsModal.vue

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,8 @@ async function saveGlobalSkills() {
122122
// ── View modal ────────────────────────────────────────────────────
123123
const skillViewRef = ref<InstanceType<typeof SkillViewerModal>>()
124124
125-
function openView(name: string, badge = '') {
126-
const isGlobal = badge !== t('agents.skills_exclusive_tab')
127-
const base = isGlobal ? '/api/skills' : apiBase()
128-
skillViewRef.value?.open(name, badge, base)
125+
function openView(row: SkillItem, badge = row.source || '') {
126+
skillViewRef.value?.open(row.name, badge, row.id || '')
129127
}
130128
131129
async function remove(name: string) {
@@ -222,7 +220,7 @@ defineExpose({ open })
222220
<span style="color:var(--sui-fg-muted);font-size:var(--sui-fs-sm)">{{ row.description || '-' }}</span>
223221
</template>
224222
<template #ops="{ row }">
225-
<SButton type="outline" size="sm" @click="openView(row.name, row.source)">{{ t('common.view') }}</SButton>
223+
<SButton type="outline" size="sm" @click="openView(row, row.source)">{{ t('common.view') }}</SButton>
226224
</template>
227225
</STable>
228226
</template>
@@ -247,7 +245,7 @@ defineExpose({ open })
247245
<template #description="{ row }">{{ row.description || '-' }}</template>
248246
<template #ops="{ row }">
249247
<div class="ops-cell">
250-
<SButton type="outline" size="sm" @click="openView(row.name, t('agents.skills_exclusive_tab'))">{{ t('common.view') }}</SButton>
248+
<SButton type="outline" size="sm" @click="openView(row, t('agents.skills_exclusive_tab'))">{{ t('common.view') }}</SButton>
251249
<SButton type="danger" size="sm" @click="remove(row.name)">{{ t('common.delete') }}</SButton>
252250
</div>
253251
</template>

0 commit comments

Comments
 (0)