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.
Files changed (51) hide show
  1. package/README.md +16 -2
  2. package/lib/client.js +936 -220
  3. package/lib/client.js.map +4 -4
  4. package/lib/index.js +813 -38
  5. package/lib/index.js.map +4 -4
  6. package/market/mcp.json +164 -0
  7. package/market/skills.json +36 -0
  8. package/package.json +3 -1
  9. package/skills/find-skills/LICENSE +21 -0
  10. package/skills/find-skills/SKILL.md +141 -0
  11. package/skills/skill-creator/LICENSE.txt +202 -0
  12. package/skills/skill-creator/SKILL.md +485 -0
  13. package/skills/skill-creator/agents/analyzer.md +274 -0
  14. package/skills/skill-creator/agents/comparator.md +202 -0
  15. package/skills/skill-creator/agents/grader.md +223 -0
  16. package/skills/skill-creator/assets/eval_review.html +146 -0
  17. package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  18. package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  19. package/skills/skill-creator/references/schemas.md +430 -0
  20. package/skills/skill-creator/scripts/__init__.py +0 -0
  21. package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  22. package/skills/skill-creator/scripts/generate_report.py +326 -0
  23. package/skills/skill-creator/scripts/improve_description.py +247 -0
  24. package/skills/skill-creator/scripts/package_skill.py +136 -0
  25. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  26. package/skills/skill-creator/scripts/run_eval.py +310 -0
  27. package/skills/skill-creator/scripts/run_loop.py +328 -0
  28. package/skills/skill-creator/scripts/utils.py +47 -0
  29. package/src/client/CapabilitiesSection.tsx +11 -7
  30. package/src/client/MarketTab.tsx +263 -0
  31. package/src/client/McpTab.tsx +198 -0
  32. package/src/client/SkillsTab.tsx +261 -26
  33. package/src/client/css.ts +42 -0
  34. package/src/client/index.ts +58 -5
  35. package/src/client/locales.ts +112 -2
  36. package/src/index.ts +76 -20
  37. package/src/market.test.ts +57 -0
  38. package/src/market.ts +166 -0
  39. package/src/opener.ts +26 -0
  40. package/src/packaged-skills.test.ts +40 -0
  41. package/src/repos.test.ts +213 -0
  42. package/src/repos.ts +207 -0
  43. package/src/routes.ts +336 -6
  44. package/src/skills.test.ts +45 -1
  45. package/src/skills.ts +22 -1
  46. package/src/smoke.test.ts +61 -2
  47. package/src/state.test.ts +64 -0
  48. package/src/state.ts +109 -0
  49. package/src/tar.test.ts +100 -0
  50. package/src/tar.ts +154 -0
  51. package/src/types.ts +11 -1
@@ -1,13 +1,16 @@
1
- /** Settings Plugins “技能/Skills” tab: view the discovered catalog, edit the
2
- * user-owned root. Pure presentation-layer data arrives through props. */
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 editable flag the route adds). */
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
- <button type="button" className="dpc-refresh" aria-label={t('view')} title={t('view')} disabled={busy} onClick={() => setReload((value) => value + 1)}>
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 && skills.length === 0 && <p className="dpc-empty">{t('emptySkills')}</p>}
187
- {skills !== null && skills.length > 0 && (
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
- {skills.map((skill) => (
190
- <li className="dpc-card" key={`${skill.source}/${skill.name}`}>
191
- <div className="dpc-cardTop">
192
- <strong className="dpc-cardTitle" title={skill.name}>{skill.name}</strong>
193
- <span className="dpc-tag" data-kind="source">{t(SOURCE_KEYS[skill.source] ?? 'sourceCustom')}</span>
194
- </div>
195
- <p className="dpc-cardDesc" title={skill.description}>{skill.description}</p>
196
- <div className="dpc-cardRow">
197
- {!skill.invocation.modelInvocable && <span className="dpc-tag">⚙</span>}
198
- {!skill.invocation.userInvocable && <span className="dpc-tag">/</span>}
199
- <span className="dpc-spacer" />
200
- <Button variant="ghost" size="sm" disabled={busy} onClick={() => void openExisting(skill)}>
201
- {skill.editable ? t('edit') : t('view')}
202
- </Button>
203
- {skill.editable && (
204
- <Button variant="ghost" size="sm" disabled={busy} onClick={() => setConfirmName(skill.name)}>{t('delete')}</Button>
205
- )}
206
- </div>
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
  `
@@ -1,10 +1,11 @@
1
1
  /** dsh-plugin-capabilities client entry: contributes the top-level
2
- * “技能与 MCP” section into Settings (beside 通用设置/模型, with Skills and
3
- * MCP as internal tabs). Calls the host routes with fetch. */
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 Skills/MCP
98
- // tabs directly, so nothing registers into settings.plugins.tab anymore.
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
+ }