dsh-plugin-capabilities 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/lib/client.js +936 -220
- package/lib/client.js.map +4 -4
- package/lib/index.js +813 -38
- package/lib/index.js.map +4 -4
- package/market/mcp.json +164 -0
- package/market/skills.json +36 -0
- package/package.json +3 -1
- package/skills/find-skills/LICENSE +21 -0
- package/skills/find-skills/SKILL.md +141 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +247 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/src/client/CapabilitiesSection.tsx +11 -7
- package/src/client/MarketTab.tsx +263 -0
- package/src/client/McpTab.tsx +198 -0
- package/src/client/SkillsTab.tsx +261 -26
- package/src/client/css.ts +42 -0
- package/src/client/index.ts +58 -5
- package/src/client/locales.ts +112 -2
- package/src/index.ts +76 -20
- package/src/market.test.ts +57 -0
- package/src/market.ts +166 -0
- package/src/opener.ts +26 -0
- package/src/packaged-skills.test.ts +40 -0
- package/src/repos.test.ts +213 -0
- package/src/repos.ts +207 -0
- package/src/routes.ts +336 -6
- package/src/skills.test.ts +45 -1
- package/src/skills.ts +22 -1
- package/src/smoke.test.ts +61 -2
- package/src/state.test.ts +64 -0
- package/src/state.ts +109 -0
- package/src/tar.test.ts +100 -0
- package/src/tar.ts +154 -0
- package/src/types.ts +11 -1
package/src/client/SkillsTab.tsx
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
/** Settings
|
|
2
|
-
*
|
|
1
|
+
/** Settings “技能” tab: view the discovered catalog, edit the user-owned
|
|
2
|
+
* root, toggle skills on/off, open their folders, and manage the custom
|
|
3
|
+
* skill repositories (local paths and GitHub checkouts) feeding the catalog.
|
|
4
|
+
* Pure presentation-layer — data arrives through props. */
|
|
3
5
|
|
|
4
6
|
import { useEffect, useState } from 'react'
|
|
5
7
|
import type { ReactElement } from 'react'
|
|
6
8
|
import { Button, IconRefreshOutline14, IconSkillOutline16, Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
7
9
|
import { CSS } from './css.ts'
|
|
8
|
-
import type { Translate } from './index.ts'
|
|
10
|
+
import type { OpenTarget, Translate } from './index.ts'
|
|
11
|
+
import type { RootRowView } from './MarketTab.tsx'
|
|
9
12
|
|
|
10
|
-
/** One skill as the host reports it (plus the
|
|
13
|
+
/** One skill as the host reports it (plus the flags the route adds). */
|
|
11
14
|
export interface SkillRowView {
|
|
12
15
|
name: string
|
|
13
16
|
description: string
|
|
@@ -16,6 +19,10 @@ export interface SkillRowView {
|
|
|
16
19
|
source: string
|
|
17
20
|
provider: string
|
|
18
21
|
editable: boolean
|
|
22
|
+
/** The skill's folder when it lives on disk (drives open-folder). */
|
|
23
|
+
dir?: string
|
|
24
|
+
/** File-backed skills can be enabled/disabled through the policy route. */
|
|
25
|
+
policyEditable: boolean
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export interface SkillsInjected {
|
|
@@ -23,6 +30,11 @@ export interface SkillsInjected {
|
|
|
23
30
|
get(name: string): Promise<{ content: string }>
|
|
24
31
|
save(input: { name: string; description: string; whenToUse?: string; modelInvocable: boolean; userInvocable: boolean; content: string }): Promise<{ ok: boolean }>
|
|
25
32
|
remove(name: string): Promise<{ ok: boolean }>
|
|
33
|
+
policy(name: string, enabled: boolean): Promise<{ ok: boolean }>
|
|
34
|
+
open(target: OpenTarget): Promise<{ ok: boolean }>
|
|
35
|
+
roots(): Promise<{ roots: RootRowView[] }>
|
|
36
|
+
addRoot(input: { kind: 'local' | 'git'; path?: string; url?: string }): Promise<{ ok: boolean; root: RootRowView }>
|
|
37
|
+
removeRoot(id: string): Promise<{ ok: boolean }>
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
/** Editor dialog state; null when closed. */
|
|
@@ -46,6 +58,21 @@ const SOURCE_KEYS: Record<string, string> = {
|
|
|
46
58
|
custom: 'sourceCustom',
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
/** Status tag for the invocation policy; undefined = default (both allowed). */
|
|
62
|
+
function policyTag(skill: SkillRowView): { key?: string; off: boolean } {
|
|
63
|
+
const { modelInvocable, userInvocable } = skill.invocation
|
|
64
|
+
if (!modelInvocable && !userInvocable) return { key: 'skillDisabled', off: true }
|
|
65
|
+
if (!modelInvocable) return { key: 'skillUserOnly', off: false }
|
|
66
|
+
if (!userInvocable) return { key: 'skillModelOnly', off: false }
|
|
67
|
+
return { off: false }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Add-repository form state; null when the form is collapsed. */
|
|
71
|
+
interface AddRootState {
|
|
72
|
+
kind: 'local' | 'git'
|
|
73
|
+
value: string
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): ReactElement {
|
|
50
77
|
const { t, injected } = props
|
|
51
78
|
const [skills, setSkills] = useState<SkillRowView[] | null>(null)
|
|
@@ -55,6 +82,14 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
|
|
|
55
82
|
const [outcome, setOutcome] = useState<{ ok: boolean; text: string } | null>(null)
|
|
56
83
|
const [formError, setFormError] = useState<string | null>(null)
|
|
57
84
|
const [reload, setReload] = useState(0)
|
|
85
|
+
const [query, setQuery] = useState('')
|
|
86
|
+
const [sourceFilter, setSourceFilter] = useState<string>('all')
|
|
87
|
+
|
|
88
|
+
// Custom skill repositories.
|
|
89
|
+
const [roots, setRoots] = useState<RootRowView[] | null>(null)
|
|
90
|
+
const [addRoot, setAddRoot] = useState<AddRootState | null>(null)
|
|
91
|
+
const [rootBusy, setRootBusy] = useState(false)
|
|
92
|
+
const [confirmRootId, setConfirmRootId] = useState<string | null>(null)
|
|
58
93
|
|
|
59
94
|
useEffect(() => {
|
|
60
95
|
let current = true
|
|
@@ -62,6 +97,10 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
|
|
|
62
97
|
(body) => { if (current) setSkills(body.skills) },
|
|
63
98
|
(error: Error) => { if (current) { setSkills([]); setOutcome({ ok: false, text: `${t('failed')}: ${String(error.message ?? error)}` }) } },
|
|
64
99
|
)
|
|
100
|
+
void injected.roots().then(
|
|
101
|
+
(body) => { if (current) setRoots(body.roots) },
|
|
102
|
+
() => { if (current) setRoots([]) },
|
|
103
|
+
)
|
|
65
104
|
return () => { current = false }
|
|
66
105
|
}, [injected, reload, t])
|
|
67
106
|
|
|
@@ -152,8 +191,89 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
|
|
|
152
191
|
}
|
|
153
192
|
}
|
|
154
193
|
|
|
194
|
+
const doToggle = async (skill: SkillRowView): Promise<void> => {
|
|
195
|
+
const enabled = skill.invocation.modelInvocable || skill.invocation.userInvocable
|
|
196
|
+
setBusy(true)
|
|
197
|
+
try {
|
|
198
|
+
await injected.policy(skill.name, !enabled)
|
|
199
|
+
setOutcome({ ok: true, text: !enabled ? t('skillEnabled') : t('skillDisabledMsg') })
|
|
200
|
+
refreshUntil(list => {
|
|
201
|
+
const row = list.find(item => item.name === skill.name)
|
|
202
|
+
return row !== undefined && row.invocation.modelInvocable === !enabled && row.invocation.userInvocable === !enabled
|
|
203
|
+
})
|
|
204
|
+
} catch (error) {
|
|
205
|
+
setOutcome({ ok: false, text: `${t('failed')}: ${String(error instanceof Error ? error.message : error)}` })
|
|
206
|
+
} finally {
|
|
207
|
+
setBusy(false)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const doOpen = async (target: OpenTarget): Promise<void> => {
|
|
212
|
+
try {
|
|
213
|
+
await injected.open(target)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
setOutcome({ ok: false, text: `${t('failed')}: ${String(error instanceof Error ? error.message : error)}` })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const doAddRoot = async (): Promise<void> => {
|
|
220
|
+
if (addRoot === null || addRoot.value.trim() === '') return
|
|
221
|
+
setRootBusy(true)
|
|
222
|
+
setFormError(null)
|
|
223
|
+
try {
|
|
224
|
+
await injected.addRoot(addRoot.kind === 'local'
|
|
225
|
+
? { kind: 'local', path: addRoot.value.trim() }
|
|
226
|
+
: { kind: 'git', url: addRoot.value.trim() })
|
|
227
|
+
setOutcome({ ok: true, text: t('rootAdded') })
|
|
228
|
+
setAddRoot(null)
|
|
229
|
+
void injected.roots().then(
|
|
230
|
+
(body) => setRoots(body.roots),
|
|
231
|
+
() => setRoots([]),
|
|
232
|
+
)
|
|
233
|
+
// The provider remounts asynchronously; refresh the catalog a few
|
|
234
|
+
// times so the repository's skills appear without manual reload.
|
|
235
|
+
for (let tick = 0; tick < 4; tick++) {
|
|
236
|
+
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
237
|
+
void injected.list().then(
|
|
238
|
+
(body) => setSkills(body.skills),
|
|
239
|
+
() => undefined,
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
setFormError(String(error instanceof Error ? error.message : error))
|
|
244
|
+
} finally {
|
|
245
|
+
setRootBusy(false)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const doRemoveRoot = async (): Promise<void> => {
|
|
250
|
+
if (confirmRootId === null) return
|
|
251
|
+
setRootBusy(true)
|
|
252
|
+
try {
|
|
253
|
+
await injected.removeRoot(confirmRootId)
|
|
254
|
+
setOutcome({ ok: true, text: t('rootRemoved') })
|
|
255
|
+
setRoots(null)
|
|
256
|
+
void injected.roots().then(
|
|
257
|
+
(body) => setRoots(body.roots),
|
|
258
|
+
() => setRoots([]),
|
|
259
|
+
)
|
|
260
|
+
setReload((value) => value + 1)
|
|
261
|
+
} catch (error) {
|
|
262
|
+
setOutcome({ ok: false, text: `${t('failed')}: ${String(error instanceof Error ? error.message : error)}` })
|
|
263
|
+
} finally {
|
|
264
|
+
setRootBusy(false)
|
|
265
|
+
setConfirmRootId(null)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
155
269
|
const readOnly = editor?.mode === 'view'
|
|
156
270
|
|
|
271
|
+
const needle = query.trim().toLowerCase()
|
|
272
|
+
const filtered = skills === null ? [] : skills.filter(skill =>
|
|
273
|
+
(sourceFilter === 'all' || skill.source === sourceFilter)
|
|
274
|
+
&& (needle === '' || skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle)))
|
|
275
|
+
const sources = skills === null ? [] : [...new Set(skills.map(skill => skill.source))]
|
|
276
|
+
|
|
157
277
|
return (
|
|
158
278
|
<div className="dpc-section">
|
|
159
279
|
<style>{CSS}</style>
|
|
@@ -162,6 +282,7 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
|
|
|
162
282
|
<IconSkillOutline16 aria-hidden="true" />
|
|
163
283
|
<h3>{t('skillsTitle')}</h3>
|
|
164
284
|
<span className="dpc-spacer" />
|
|
285
|
+
<Button variant="ghost" size="sm" onClick={() => void doOpen({ target: 'user-skills' })}>{t('openUserSkills')}</Button>
|
|
165
286
|
<Button variant="primary" size="sm" onClick={openCreate}>{t('newSkill')}</Button>
|
|
166
287
|
</div>
|
|
167
288
|
<p className="dpc-intro">{t('skillsIntro')}</p>
|
|
@@ -175,39 +296,138 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
|
|
|
175
296
|
|
|
176
297
|
<div className="dpc-listHead">
|
|
177
298
|
<h3>{t('skillsTab')}</h3>
|
|
178
|
-
{skills !== null && <span className="dpc-count">{skills.length}</span>}
|
|
299
|
+
{skills !== null && <span className="dpc-count">{filtered.length}/{skills.length}</span>}
|
|
179
300
|
<span className="dpc-spacer" />
|
|
180
|
-
<
|
|
301
|
+
<input
|
|
302
|
+
className="dpc-search"
|
|
303
|
+
type="search"
|
|
304
|
+
placeholder={t('searchSkills')}
|
|
305
|
+
aria-label={t('searchSkills')}
|
|
306
|
+
value={query}
|
|
307
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
308
|
+
/>
|
|
309
|
+
<button type="button" className="dpc-refresh" aria-label={t('refresh')} title={t('refresh')} disabled={busy} onClick={() => setReload((value) => value + 1)}>
|
|
181
310
|
<IconRefreshOutline14 size={14} aria-hidden="true" />
|
|
182
311
|
</button>
|
|
183
312
|
</div>
|
|
184
313
|
|
|
314
|
+
{sources.length > 1 && (
|
|
315
|
+
<div className="dpc-chips" role="group" aria-label={t('source')}>
|
|
316
|
+
{[{ id: 'all', label: t('filterAll') }, ...sources.map(source => ({ id: source, label: t(SOURCE_KEYS[source] ?? 'sourceCustom') }))].map(chip => (
|
|
317
|
+
<button
|
|
318
|
+
key={chip.id}
|
|
319
|
+
type="button"
|
|
320
|
+
className="dpc-chip"
|
|
321
|
+
data-active={sourceFilter === chip.id ? 'true' : undefined}
|
|
322
|
+
onClick={() => setSourceFilter(chip.id)}
|
|
323
|
+
>
|
|
324
|
+
{chip.label}
|
|
325
|
+
</button>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
185
330
|
{skills === null && <p className="dpc-empty">{t('loading')}</p>}
|
|
186
|
-
{skills !== null &&
|
|
187
|
-
{skills !== null &&
|
|
331
|
+
{skills !== null && filtered.length === 0 && <p className="dpc-empty">{skills.length === 0 ? t('emptySkills') : t('noMatch')}</p>}
|
|
332
|
+
{skills !== null && filtered.length > 0 && (
|
|
188
333
|
<ul className="dpc-cards">
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
334
|
+
{filtered.map((skill) => {
|
|
335
|
+
const tag = policyTag(skill)
|
|
336
|
+
return (
|
|
337
|
+
<li className="dpc-card" key={`${skill.source}/${skill.name}`}>
|
|
338
|
+
<div className="dpc-cardTop">
|
|
339
|
+
<strong className="dpc-cardTitle" title={skill.name}>{skill.name}</strong>
|
|
340
|
+
<span className="dpc-tag" data-kind="source">{t(SOURCE_KEYS[skill.source] ?? 'sourceCustom')}</span>
|
|
341
|
+
{tag.key !== undefined && <span className="dpc-tag" data-kind={tag.off ? 'off' : undefined}>{t(tag.key)}</span>}
|
|
342
|
+
</div>
|
|
343
|
+
<p className="dpc-cardDesc" title={skill.description}>{skill.description}</p>
|
|
344
|
+
<div className="dpc-cardRow">
|
|
345
|
+
{skill.policyEditable && (
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
className="dpc-switch"
|
|
349
|
+
role="switch"
|
|
350
|
+
aria-checked={skill.invocation.modelInvocable || skill.invocation.userInvocable}
|
|
351
|
+
aria-label={t('toggleSkill')}
|
|
352
|
+
title={t('toggleSkillHint')}
|
|
353
|
+
disabled={busy}
|
|
354
|
+
onClick={() => void doToggle(skill)}
|
|
355
|
+
>
|
|
356
|
+
<span className="dpc-switchKnob" />
|
|
357
|
+
</button>
|
|
358
|
+
)}
|
|
359
|
+
{skill.dir !== undefined && (
|
|
360
|
+
<button type="button" className="dpc-link" onClick={() => void doOpen({ target: 'skill', name: skill.name })}>{t('openFolder')}</button>
|
|
361
|
+
)}
|
|
362
|
+
<span className="dpc-spacer" />
|
|
363
|
+
<Button variant="ghost" size="sm" disabled={busy} onClick={() => void openExisting(skill)}>
|
|
364
|
+
{skill.editable ? t('edit') : t('view')}
|
|
365
|
+
</Button>
|
|
366
|
+
{skill.editable && (
|
|
367
|
+
<Button variant="ghost" size="sm" disabled={busy} onClick={() => setConfirmName(skill.name)}>{t('delete')}</Button>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
</li>
|
|
371
|
+
)
|
|
372
|
+
})}
|
|
373
|
+
</ul>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
<div className="dpc-listHead dpc-rootsHead">
|
|
377
|
+
<h3>{t('rootsTitle')}</h3>
|
|
378
|
+
{roots !== null && <span className="dpc-count">{roots.length}</span>}
|
|
379
|
+
<span className="dpc-spacer" />
|
|
380
|
+
{addRoot === null && (
|
|
381
|
+
<Button variant="ghost" size="sm" onClick={() => setAddRoot({ kind: 'git', value: '' })}>{t('addRoot')}</Button>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
<p className="dpc-intro">{t('rootsIntro')}</p>
|
|
385
|
+
{roots === null && <p className="dpc-empty">{t('loading')}</p>}
|
|
386
|
+
{roots !== null && roots.length === 0 && <p className="dpc-empty">{t('emptyRoots')}</p>}
|
|
387
|
+
{roots !== null && roots.length > 0 && (
|
|
388
|
+
<ul className="dpc-roots">
|
|
389
|
+
{roots.map((root) => (
|
|
390
|
+
<li className="dpc-root" key={root.id}>
|
|
391
|
+
<span className="dpc-tag" data-kind="source">{root.kind === 'git' ? 'GitHub' : t('rootLocal')}</span>
|
|
392
|
+
<strong className="dpc-rootLabel" title={root.kind === 'git' ? root.url : root.path}>{root.label}</strong>
|
|
393
|
+
<span className="dpc-rootPath">{root.kind === 'git' ? root.url : root.path}</span>
|
|
394
|
+
{!root.live && <span className="dpc-tag" data-kind="off">{t('rootStale')}</span>}
|
|
395
|
+
<span className="dpc-spacer" />
|
|
396
|
+
<button type="button" className="dpc-link" onClick={() => void doOpen({ target: 'root', id: root.id })}>{t('openFolder')}</button>
|
|
397
|
+
<Button variant="ghost" size="sm" disabled={rootBusy} onClick={() => setConfirmRootId(root.id)}>{t('delete')}</Button>
|
|
207
398
|
</li>
|
|
208
399
|
))}
|
|
209
400
|
</ul>
|
|
210
401
|
)}
|
|
402
|
+
{addRoot !== null && (
|
|
403
|
+
<div className="dpc-form dpc-addRoot">
|
|
404
|
+
<div className="dpc-addRootRow">
|
|
405
|
+
<select
|
|
406
|
+
className="dpc-select"
|
|
407
|
+
aria-label={t('rootKind')}
|
|
408
|
+
value={addRoot.kind}
|
|
409
|
+
onChange={(event) => setAddRoot({ ...addRoot, kind: event.target.value as AddRootState['kind'] })}
|
|
410
|
+
>
|
|
411
|
+
<option value="git">GitHub</option>
|
|
412
|
+
<option value="local">{t('rootLocal')}</option>
|
|
413
|
+
</select>
|
|
414
|
+
<input
|
|
415
|
+
className="dpc-input"
|
|
416
|
+
type="text"
|
|
417
|
+
placeholder={addRoot.kind === 'git' ? 'https://github.com/anthropics/skills' : t('rootLocalPlaceholder')}
|
|
418
|
+
aria-label={t('rootPlaceholder')}
|
|
419
|
+
value={addRoot.value}
|
|
420
|
+
autoFocus
|
|
421
|
+
onChange={(event) => setAddRoot({ ...addRoot, value: event.target.value })}
|
|
422
|
+
onKeyDown={(event) => { if (event.key === 'Enter') void doAddRoot() }}
|
|
423
|
+
/>
|
|
424
|
+
<Button variant="primary" size="sm" disabled={rootBusy || addRoot.value.trim() === ''} onClick={() => void doAddRoot()}>{t('add')}</Button>
|
|
425
|
+
<Button variant="ghost" size="sm" disabled={rootBusy} onClick={() => { setAddRoot(null); setFormError(null) }}>{t('cancel')}</Button>
|
|
426
|
+
</div>
|
|
427
|
+
<p className="dpc-intro">{addRoot.kind === 'git' ? t('rootGitHint') : t('rootLocalHint')}</p>
|
|
428
|
+
{formError !== null && <p className="dpc-formError">{formError}</p>}
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
211
431
|
|
|
212
432
|
<Modal
|
|
213
433
|
open={editor !== null}
|
|
@@ -298,6 +518,21 @@ export function SkillsTab(props: { t: Translate; injected: SkillsInjected }): Re
|
|
|
298
518
|
>
|
|
299
519
|
<p>{t('deleteWarn')}</p>
|
|
300
520
|
</Modal>
|
|
521
|
+
|
|
522
|
+
<Modal
|
|
523
|
+
open={confirmRootId !== null}
|
|
524
|
+
onClose={() => setConfirmRootId(null)}
|
|
525
|
+
title={t('confirmRemoveRoot')}
|
|
526
|
+
description={roots?.find(root => root.id === confirmRootId)?.label ?? undefined}
|
|
527
|
+
footer={
|
|
528
|
+
<>
|
|
529
|
+
<Button variant="ghost" onClick={() => setConfirmRootId(null)}>{t('cancel')}</Button>
|
|
530
|
+
<Button variant="primary" disabled={rootBusy} onClick={() => void doRemoveRoot()}>{t('delete')}</Button>
|
|
531
|
+
</>
|
|
532
|
+
}
|
|
533
|
+
>
|
|
534
|
+
<p>{t('removeRootWarn')}</p>
|
|
535
|
+
</Modal>
|
|
301
536
|
</div>
|
|
302
537
|
)
|
|
303
538
|
}
|
package/src/client/css.ts
CHANGED
|
@@ -66,5 +66,47 @@ export const CSS = `
|
|
|
66
66
|
.dpc-importHead{display:flex;align-items:center;gap:8px;padding:0 2px}
|
|
67
67
|
.dpc-importCount{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
|
|
68
68
|
.dpc-importAll{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
69
|
+
/* Skills search + source filter chips. */
|
|
70
|
+
.dpc-search{width:200px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px}
|
|
71
|
+
.dpc-search:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
72
|
+
.dpc-chips{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
73
|
+
.dpc-chip{border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 10px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:16px;cursor:pointer}
|
|
74
|
+
.dpc-chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
75
|
+
.dpc-chip[data-active='true']{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}
|
|
76
|
+
.dpc-chip:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
77
|
+
/* Load-policy switch on skill cards (no form semantics — instant apply). */
|
|
78
|
+
.dpc-switch{position:relative;flex:none;width:30px;height:18px;border:0;border-radius:999px;background:var(--dsw-alias-bg-layer-1);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2);cursor:pointer;transition:background .15s}
|
|
79
|
+
.dpc-switch[aria-checked='true']{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent);box-shadow:none}
|
|
80
|
+
.dpc-switchKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-primary);transition:left .15s}
|
|
81
|
+
.dpc-switch[aria-checked='true'] .dpc-switchKnob{left:14px;background:#fff}
|
|
82
|
+
.dpc-switch:disabled{opacity:.55;cursor:default}
|
|
83
|
+
.dpc-switch:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
84
|
+
/* Quiet inline link-button (open folder, homepage). */
|
|
85
|
+
.dpc-link{border:0;padding:0;background:transparent;color:var(--dsw-alias-state-business-primary);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-decoration:none}
|
|
86
|
+
.dpc-link:hover{text-decoration:underline}
|
|
87
|
+
.dpc-link:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:2px}
|
|
88
|
+
/* Skill repositories list + add form. */
|
|
89
|
+
.dpc-rootsHead{margin-top:10px}
|
|
90
|
+
.dpc-roots{display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}
|
|
91
|
+
.dpc-root{display:flex;align-items:center;gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-3)}
|
|
92
|
+
.dpc-rootLabel{flex:none;font-size:13px;line-height:18px;font-weight:600}
|
|
93
|
+
.dpc-rootPath{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code)}
|
|
94
|
+
.dpc-addRoot{border:1px dashed var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px}
|
|
95
|
+
.dpc-addRootRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
96
|
+
.dpc-addRootRow .dpc-select{width:auto;flex:none}
|
|
97
|
+
.dpc-addRootRow .dpc-input{flex:1;min-width:200px}
|
|
98
|
+
/* Market segments + env hint + MCP format preview. */
|
|
99
|
+
.dpc-segments{display:inline-flex;gap:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:3px;background:var(--dsw-alias-bg-layer-1)}
|
|
100
|
+
.dpc-segment{border:0;border-radius:6px;padding:4px 14px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}
|
|
101
|
+
.dpc-segment:hover{color:var(--dsw-alias-label-primary)}
|
|
102
|
+
.dpc-segment[data-active='true']{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);font-weight:600}
|
|
103
|
+
.dpc-segment:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
|
|
104
|
+
.dpc-envHint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-warning-primary,var(--dsw-alias-label-secondary))}
|
|
105
|
+
.dpc-format{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-1)}
|
|
106
|
+
.dpc-format summary{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;user-select:none}
|
|
107
|
+
.dpc-format summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:2px}
|
|
108
|
+
.dpc-format .dpc-form{margin-top:8px}
|
|
109
|
+
.dpc-formatHint{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
|
|
110
|
+
.dpc-code{margin:4px 0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:8px 10px;overflow-x:auto;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font-family:var(--ds-font-family-code);font-size:11px;line-height:17px;white-space:pre}
|
|
69
111
|
@media(max-width:680px){.dpc-cards{grid-template-columns:minmax(0,1fr)}}
|
|
70
112
|
`
|
package/src/client/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** dsh-plugin-capabilities client entry: contributes the top-level
|
|
2
|
-
* “技能与 MCP” section into Settings (beside 通用设置/模型, with Skills
|
|
3
|
-
*
|
|
2
|
+
* “技能与 MCP” section into Settings (beside 通用设置/模型, with Skills, MCP,
|
|
3
|
+
* and 市场 as internal tabs). Calls the host routes with fetch. */
|
|
4
4
|
|
|
5
5
|
import { createElement as h } from 'react'
|
|
6
6
|
import { CapabilitiesSection } from './CapabilitiesSection.tsx'
|
|
7
7
|
import type { McpInjected, McpRow } from './McpTab.tsx'
|
|
8
|
+
import type { MarketInjected, RootRowView } from './MarketTab.tsx'
|
|
8
9
|
import type { SkillsInjected, SkillRowView } from './SkillsTab.tsx'
|
|
9
10
|
import { zh, en } from './locales.ts'
|
|
10
11
|
|
|
@@ -63,6 +64,12 @@ export interface ImportedServerView {
|
|
|
63
64
|
headers?: Record<string, string>
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/** Openable targets on the host (server resolves real paths). */
|
|
68
|
+
export type OpenTarget =
|
|
69
|
+
| { target: 'user-skills' }
|
|
70
|
+
| { target: 'skill'; name: string }
|
|
71
|
+
| { target: 'root'; id: string }
|
|
72
|
+
|
|
66
73
|
export function apply(ctx: CapabilitiesClientContext): void {
|
|
67
74
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-capabilities: dictionaries')
|
|
68
75
|
const t = ctx.locale.bind(NS)
|
|
@@ -72,6 +79,13 @@ export function apply(ctx: CapabilitiesClientContext): void {
|
|
|
72
79
|
get: (name: string) => fetchJson<SkillBody>(`/dsh-plugin-capabilities/skill?name=${encodeURIComponent(name)}`),
|
|
73
80
|
save: (input: unknown) => post('/dsh-plugin-capabilities/skill/save', input) as Promise<{ ok: boolean }>,
|
|
74
81
|
remove: (name: string) => post('/dsh-plugin-capabilities/skill/delete', { name }) as Promise<{ ok: boolean }>,
|
|
82
|
+
policy: (name: string, enabled: boolean) =>
|
|
83
|
+
post('/dsh-plugin-capabilities/skill/policy', { name, enabled }) as Promise<{ ok: boolean }>,
|
|
84
|
+
open: (target: OpenTarget) => post('/dsh-plugin-capabilities/open', target) as Promise<{ ok: boolean }>,
|
|
85
|
+
roots: () => fetchJson<{ roots: RootRowView[] }>('/dsh-plugin-capabilities/roots'),
|
|
86
|
+
addRoot: (input: { kind: 'local' | 'git'; path?: string; url?: string }) =>
|
|
87
|
+
post('/dsh-plugin-capabilities/roots/add', input) as Promise<{ ok: boolean; root: RootRowView }>,
|
|
88
|
+
removeRoot: (id: string) => post('/dsh-plugin-capabilities/roots/remove', { id }) as Promise<{ ok: boolean }>,
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
const mcpInjected: McpInjected = {
|
|
@@ -93,9 +107,19 @@ export function apply(ctx: CapabilitiesClientContext): void {
|
|
|
93
107
|
desktop: window.dshDesktop !== undefined,
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
const marketInjected: MarketInjected = {
|
|
111
|
+
skillsIndex: () => fetchJson<{ source: 'remote' | 'bundled'; repos: Array<MarketRepoView & { installedId: string | null }> }>('/dsh-plugin-capabilities/market/skills'),
|
|
112
|
+
mcpIndex: () => fetchJson<{ source: 'remote' | 'bundled'; servers: Array<MarketServerView & { installed: boolean }> }>('/dsh-plugin-capabilities/market/mcp'),
|
|
113
|
+
installSkillRepo: (url: string) =>
|
|
114
|
+
post('/dsh-plugin-capabilities/market/skills/install', { url }) as Promise<{ ok: boolean; root: RootRowView }>,
|
|
115
|
+
installMcp: (id: string) =>
|
|
116
|
+
post('/dsh-plugin-capabilities/market/mcp/install', { id }) as Promise<{ ok: boolean; id: string }>,
|
|
117
|
+
removeRoot: skillsInjected.removeRoot,
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
// One top-level nav entry (between 模型 and 插件), not a tab under the
|
|
97
|
-
// Plugins section — the section component owns its internal
|
|
98
|
-
//
|
|
121
|
+
// Plugins section — the section component owns its internal tabs directly,
|
|
122
|
+
// so nothing registers into settings.plugins.tab anymore.
|
|
99
123
|
ctx.slots.inject('settings.section', () => {
|
|
100
124
|
return ctx.slots.register({
|
|
101
125
|
name: 'settings.section',
|
|
@@ -103,6 +127,35 @@ export function apply(ctx: CapabilitiesClientContext): void {
|
|
|
103
127
|
order: 12,
|
|
104
128
|
label: () => t('sectionNav'),
|
|
105
129
|
locale: NS,
|
|
106
|
-
}, () => h(CapabilitiesSection, { t, skills: skillsInjected, mcp: mcpInjected }))
|
|
130
|
+
}, () => h(CapabilitiesSection, { t, skills: skillsInjected, mcp: mcpInjected, market: marketInjected }))
|
|
107
131
|
})
|
|
108
132
|
}
|
|
133
|
+
|
|
134
|
+
/** One skills-market repository as the browser sees it. */
|
|
135
|
+
export interface MarketRepoView {
|
|
136
|
+
id: string
|
|
137
|
+
name: string
|
|
138
|
+
nameZh?: string
|
|
139
|
+
description: string
|
|
140
|
+
descriptionZh?: string
|
|
141
|
+
url: string
|
|
142
|
+
homepage?: string
|
|
143
|
+
skillCount?: number
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One MCP-market server as the browser sees it. */
|
|
147
|
+
export interface MarketServerView {
|
|
148
|
+
id: string
|
|
149
|
+
name: string
|
|
150
|
+
nameZh?: string
|
|
151
|
+
description: string
|
|
152
|
+
descriptionZh?: string
|
|
153
|
+
transport: 'stdio' | 'streamable-http'
|
|
154
|
+
command?: string
|
|
155
|
+
args?: string[]
|
|
156
|
+
envKeys?: string[]
|
|
157
|
+
url?: string
|
|
158
|
+
homepage: string
|
|
159
|
+
category?: string
|
|
160
|
+
runtime?: string
|
|
161
|
+
}
|