dsh-skill-picker 0.3.2 → 0.3.4
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 +5 -2
- package/lib/client.js +78 -30
- package/lib/client.js.map +2 -2
- package/lib/index.js +6 -1
- package/lib/index.js.map +2 -2
- package/package.json +3 -2
- package/src/client/index.jsx +107 -39
- package/src/index.js +21 -4
package/lib/index.js
CHANGED
|
@@ -9,6 +9,10 @@ function userSkillsDir() {
|
|
|
9
9
|
const home = process.env.DSH_HOME ?? path.join(os.homedir(), ".dsh");
|
|
10
10
|
return path.join(home, "skills");
|
|
11
11
|
}
|
|
12
|
+
function userAgentsSkillsDir() {
|
|
13
|
+
const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), ".agents");
|
|
14
|
+
return path.join(agentsHome, "skills");
|
|
15
|
+
}
|
|
12
16
|
function parseFrontmatter(content) {
|
|
13
17
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
14
18
|
if (!match) return {};
|
|
@@ -47,10 +51,11 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
47
51
|
}
|
|
48
52
|
async function scanSkills(cwd) {
|
|
49
53
|
const map = /* @__PURE__ */ new Map();
|
|
54
|
+
await scanSkillsDirInto(map, userAgentsSkillsDir());
|
|
50
55
|
await scanSkillsDirInto(map, userSkillsDir());
|
|
51
56
|
if (typeof cwd === "string" && cwd !== "") {
|
|
52
|
-
await scanSkillsDirInto(map, path.join(cwd, ".dsh", "skills"));
|
|
53
57
|
await scanSkillsDirInto(map, path.join(cwd, ".agents", "skills"));
|
|
58
|
+
await scanSkillsDirInto(map, path.join(cwd, ".dsh", "skills"));
|
|
54
59
|
}
|
|
55
60
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
56
61
|
}
|
package/lib/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.js"],
|
|
4
|
-
"sourcesContent": ["/**\n * dsh-skill-picker \u2014 host half: exposes the installed-skill catalog to the\n * browser half through a small JSON route (`/dsh-skill-picker/skills`) and\n * announces the picker to every agent through the system-prompt section\n * mechanism.\n *\n * The catalog is scanned directly from the DSH user skills directory\n * (`$DSH_HOME/skills`, default `~/.dsh/skills`) by reading each skill's\n * `SKILL.md` frontmatter. Why not `ctx.skills`? The filesystem skill provider\n * is mounted in agent scope (the standard preset's standing mount), so a\n * host-context `ctx.skills.snapshot({})` sees only the global layer \u2014 which\n * is empty for user skills. Scanning the same roots the provider uses keeps\n * the picker's list in sync with what agents actually load.\n *\n * The browser half (exports \"./client\") is served by client-modules from the\n * same package's dsh.client declaration.\n *\n * @module dsh-skill-picker\n */\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/** Required services: the route registry and the prompt band. */\nexport const inject = ['webServer', 'systemPrompt']\n\n/** Order of the announcement section within the tool-guidance band. */\nconst SECTION_ORDER = 215\n\n/** Model-facing announcement: picker presence and the user-visible gesture. */\nexport const SKILL_PICKER_GUIDANCE =\n '\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002'\n\n/** Resolve the user skills directory, mirroring the official provider's default. */\nfunction userSkillsDir() {\n const home = process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\n return path.join(home, 'skills')\n}\n\n/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */\nfunction parseFrontmatter(content) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---/)\n if (!match) return {}\n const out = {}\n for (const line of match[1].split(/\\r?\\n/)) {\n const kv = line.match(/^([A-Za-z0-9_-]+):\\s*(.*)$/)\n if (!kv) continue\n const value = kv[2].trim().replace(/^[\"']|[\"']$/g, '')\n if (value !== '') out[kv[1]] = value\n }\n return out\n}\n\n/** Scan one skill directory into the map; never throws (missing dir is a no-op). */\nasync function scanSkillsDirInto(map, dir) {\n let entries\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillDir = path.join(dir, entry.name)\n let content\n try {\n content = await readFile(path.join(skillDir, 'SKILL.md'), 'utf8')\n } catch {\n continue\n }\n const meta = parseFrontmatter(content)\n // Later writes win, so project-level skills override same-named user skills.\n map.set(meta.name ?? entry.name, {\n name: meta.name ?? entry.name,\n description: meta.description ?? '',\n path: skillDir,\n })\n }\n}\n\n/**\n * Scan
|
|
5
|
-
"mappings": ";AAoBA,SAAS,UAAU,eAAe;AAClC,OAAO,QAAQ;AACf,OAAO,UAAU;AAGV,IAAM,SAAS,CAAC,aAAa,cAAc;AAGlD,IAAM,gBAAgB;AAGf,IAAM,wBACX;AAGF,SAAS,gBAAgB;AACvB,QAAM,OAAO,QAAQ,IAAI,YAAY,KAAK,KAAK,GAAG,QAAQ,GAAG,MAAM;AACnE,SAAO,KAAK,KAAK,MAAM,QAAQ;AACjC;AAGA,SAAS,iBAAiB,SAAS;AACjC,QAAM,QAAQ,QAAQ,MAAM,6BAA6B;AACzD,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,QAAM,MAAM,CAAC;AACb,aAAW,QAAQ,MAAM,CAAC,EAAE,MAAM,OAAO,GAAG;AAC1C,UAAM,KAAK,KAAK,MAAM,4BAA4B;AAClD,QAAI,CAAC,GAAI;AACT,UAAM,QAAQ,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,gBAAgB,EAAE;AACrD,QAAI,UAAU,GAAI,KAAI,GAAG,CAAC,CAAC,IAAI;AAAA,EACjC;AACA,SAAO;AACT;AAGA,eAAe,kBAAkB,KAAK,KAAK;AACzC,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC;AAAA,EACtD,QAAQ;AACN;AAAA,EACF;AACA,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,YAAY,EAAG;AAC1B,UAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,SAAS,KAAK,KAAK,UAAU,UAAU,GAAG,MAAM;AAAA,IAClE,QAAQ;AACN;AAAA,IACF;AACA,UAAM,OAAO,iBAAiB,OAAO;AAErC,QAAI,IAAI,KAAK,QAAQ,MAAM,MAAM;AAAA,MAC/B,MAAM,KAAK,QAAQ,MAAM;AAAA,MACzB,aAAa,KAAK,eAAe;AAAA,MACjC,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACF;
|
|
4
|
+
"sourcesContent": ["/**\n * dsh-skill-picker \u2014 host half: exposes the installed-skill catalog to the\n * browser half through a small JSON route (`/dsh-skill-picker/skills`) and\n * announces the picker to every agent through the system-prompt section\n * mechanism.\n *\n * The catalog is scanned directly from the DSH user skills directory\n * (`$DSH_HOME/skills`, default `~/.dsh/skills`) by reading each skill's\n * `SKILL.md` frontmatter. Why not `ctx.skills`? The filesystem skill provider\n * is mounted in agent scope (the standard preset's standing mount), so a\n * host-context `ctx.skills.snapshot({})` sees only the global layer \u2014 which\n * is empty for user skills. Scanning the same roots the provider uses keeps\n * the picker's list in sync with what agents actually load.\n *\n * The browser half (exports \"./client\") is served by client-modules from the\n * same package's dsh.client declaration.\n *\n * @module dsh-skill-picker\n */\n\nimport { readFile, readdir } from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\n\n/** Required services: the route registry and the prompt band. */\nexport const inject = ['webServer', 'systemPrompt']\n\n/** Order of the announcement section within the tool-guidance band. */\nconst SECTION_ORDER = 215\n\n/** Model-facing announcement: picker presence and the user-visible gesture. */\nexport const SKILL_PICKER_GUIDANCE =\n '\u672C\u673A\u5DF2\u5B89\u88C5 dsh-skill-picker \u63D2\u4EF6\uFF08Web GUI \u7684\u6280\u80FD\u9009\u62E9\u5668\uFF09\uFF1A\u8F93\u5165\u6846\u65C1\u6709\u6280\u80FD\u6309\u94AE\uFF0C\u7528\u6237\u70B9\u9009\u6280\u80FD\u540E\u4F1A\u628A `/\u6280\u80FD\u540D`\uFF08\u5982 /duo-xuan-pi-gai\uFF09\u63D2\u5165\u53D1\u9001\u6846\u5E76\u968F\u6D88\u606F\u53D1\u51FA\u3002DSH \u5B98\u65B9\u673A\u5236\u4F1A\u628A\u7528\u6237\u6D88\u606F\u91CC\u7684 `/\u6280\u80FD\u540D` \u624B\u52BF\u5F53\u4F5C\u6280\u80FD\u76F4\u63A5\u8C03\u7528\u5E76\u81EA\u52A8\u52A0\u8F7D\u6280\u80FD\u5185\u5BB9\u2014\u2014\u4F60\u7167\u5E38\u6309\u52A0\u8F7D\u540E\u7684\u6280\u80FD\u6307\u4EE4\u6267\u884C\u5373\u53EF\uFF0C\u65E0\u9700\u989D\u5916\u64CD\u4F5C\u3002\u7528\u6237\u8BF4\u300C\u6280\u80FD\u9009\u62E9\u5668 / \u9009\u4E2A\u6280\u80FD / \u6280\u80FD\u5217\u8868\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\u3002'\n\n/** Resolve the user skills directory, mirroring the official provider's default. */\nfunction userSkillsDir() {\n const home = process.env.DSH_HOME ?? path.join(os.homedir(), '.dsh')\n return path.join(home, 'skills')\n}\n\n/**\n * Resolve the user agents-home skills directory, mirroring the official\n * provider's default (`$DSH_AGENTS_HOME` > `~/.agents`). This is the\n * cross-tool `.agents` convention; the official `dsh-skill-filesystem` scans\n * it as its `user-agents` root (rank 500), so the fallback must too \u2014 else\n * the picker's list silently misses skills that DSH's own `/` completion\n * shows (issue #5).\n */\nfunction userAgentsSkillsDir() {\n const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), '.agents')\n return path.join(agentsHome, 'skills')\n}\n\n/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */\nfunction parseFrontmatter(content) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---/)\n if (!match) return {}\n const out = {}\n for (const line of match[1].split(/\\r?\\n/)) {\n const kv = line.match(/^([A-Za-z0-9_-]+):\\s*(.*)$/)\n if (!kv) continue\n const value = kv[2].trim().replace(/^[\"']|[\"']$/g, '')\n if (value !== '') out[kv[1]] = value\n }\n return out\n}\n\n/** Scan one skill directory into the map; never throws (missing dir is a no-op). */\nasync function scanSkillsDirInto(map, dir) {\n let entries\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillDir = path.join(dir, entry.name)\n let content\n try {\n content = await readFile(path.join(skillDir, 'SKILL.md'), 'utf8')\n } catch {\n continue\n }\n const meta = parseFrontmatter(content)\n // Later writes win, so project-level skills override same-named user skills.\n map.set(meta.name ?? entry.name, {\n name: meta.name ?? entry.name,\n description: meta.description ?? '',\n path: skillDir,\n })\n }\n}\n\n/**\n * Scan the same roots the official `dsh-skill-filesystem` provider uses, so\n * the fallback route stays in sync with what agents actually load:\n * project `.dsh/skills` (rank 100) > project `.agents/skills` (200) >\n * user `~/.dsh/skills` (400) > user `~/.agents/skills` (500). Scanned\n * low-priority first, so later writes (higher priority) win in the map.\n * Never throws (a missing dir yields []).\n * @param cwd - the active session's workspace root (undefined = user level only).\n */\nasync function scanSkills(cwd) {\n const map = new Map()\n await scanSkillsDirInto(map, userAgentsSkillsDir())\n await scanSkillsDirInto(map, userSkillsDir())\n if (typeof cwd === 'string' && cwd !== '') {\n await scanSkillsDirInto(map, path.join(cwd, '.agents', 'skills'))\n await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))\n }\n return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * Mount the skills route and the prompt section.\n * @param ctx - context carrying webServer and systemPrompt.\n */\nexport function apply(ctx) {\n ctx.effect(() => {\n const handler = async (req, res) => {\n try {\n // cwd query carries the active session's workspace root from the client.\n const cwd = req.url !== undefined ? new URL(req.url, 'http://dsh').searchParams.get('cwd') ?? undefined : undefined\n const skills = await scanSkills(cwd)\n res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify({ ok: true, complete: true, skills }))\n } catch (error) {\n res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })\n res.end(JSON.stringify({ ok: false, error: String(error?.message ?? error) }))\n }\n }\n return ctx.webServer.register({ kind: 'prefix', path: '/dsh-skill-picker', handler })\n }, 'dsh-skill-picker: routes')\n\n ctx.effect(() => ctx.systemPrompt.section({\n name: 'plugin:skill-picker',\n order: SECTION_ORDER,\n text: SKILL_PICKER_GUIDANCE,\n }), 'dsh-skill-picker: prompt section')\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-picker",
|
|
3
3
|
"description": "DSH Web GUI skill picker: a button beside the composer opens a searchable list of installed skills; picking one inserts the official `/skill-name` gesture into the input box, so the skill loads with your message (WorkBuddy-style skill invocation for DeepSeek Harness).",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"client": {
|
|
24
24
|
"inject": [
|
|
25
25
|
"@deepseek-ai/dsh-client-runtime",
|
|
26
|
-
"@deepseek-ai/dsh-client-locale"
|
|
26
|
+
"@deepseek-ai/dsh-client-locale",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-input-trigger"
|
|
27
28
|
],
|
|
28
29
|
"platform": "web"
|
|
29
30
|
},
|
package/src/client/index.jsx
CHANGED
|
@@ -188,6 +188,26 @@ const statusStyle = {
|
|
|
188
188
|
fontSize: '13px',
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/** Lightweight source badge shown only when the list came from the host scan fallback (official API unavailable). */
|
|
192
|
+
const sourceBadgeStyle = {
|
|
193
|
+
display: 'inline-flex',
|
|
194
|
+
alignItems: 'center',
|
|
195
|
+
alignSelf: 'flex-start',
|
|
196
|
+
margin: '0 8px 8px',
|
|
197
|
+
padding: '2px 8px',
|
|
198
|
+
border: '1px solid rgba(255, 193, 7, 0.35)',
|
|
199
|
+
borderRadius: '999px',
|
|
200
|
+
background: 'rgba(255, 193, 7, 0.1)',
|
|
201
|
+
color: '#d9a520',
|
|
202
|
+
fontSize: '11px',
|
|
203
|
+
lineHeight: '16px',
|
|
204
|
+
flex: 'none',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const sourceBadgeTextStyle = {
|
|
208
|
+
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/** The picker's bolt glyph: DeepSeek palette gradient + slim stroke. */
|
|
192
212
|
function BoltIcon() {
|
|
193
213
|
return (
|
|
@@ -218,6 +238,7 @@ function SkillPickerButton(props) {
|
|
|
218
238
|
const [open, setOpen] = useState(false)
|
|
219
239
|
const [skills, setSkills] = useState(undefined)
|
|
220
240
|
const [error, setError] = useState(undefined)
|
|
241
|
+
const [source, setSource] = useState(undefined)
|
|
221
242
|
const [query, setQuery] = useState('')
|
|
222
243
|
const [usage, setUsage] = useState(() => loadUsage())
|
|
223
244
|
const [active, setActive] = useState(0)
|
|
@@ -232,18 +253,20 @@ function SkillPickerButton(props) {
|
|
|
232
253
|
if (typeof props.listSkills === 'function' && props.session?.sessionId !== undefined) {
|
|
233
254
|
const listed = await props.listSkills(props.session.sessionId)
|
|
234
255
|
setSkills(Array.isArray(listed) ? listed : [])
|
|
256
|
+
setSource('official')
|
|
235
257
|
return
|
|
236
258
|
}
|
|
237
259
|
} catch (cause) {
|
|
238
260
|
console.warn('[dsh-skill-picker] official skills API failed, falling back to host route:', cause)
|
|
239
261
|
}
|
|
240
|
-
// Fallback path: the host's own scan route (
|
|
262
|
+
// Fallback path: the host's own scan route (official provider roots).
|
|
241
263
|
try {
|
|
242
264
|
const cwd = typeof props.cwd === 'string' && props.cwd !== '' ? `?cwd=${encodeURIComponent(props.cwd)}` : ''
|
|
243
265
|
const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: 'application/json' } })
|
|
244
266
|
const json = await res.json()
|
|
245
267
|
if (!json.ok) throw new Error(json.error || 'bad response')
|
|
246
268
|
setSkills(Array.isArray(json.skills) ? json.skills : [])
|
|
269
|
+
setSource('host')
|
|
247
270
|
} catch (cause) {
|
|
248
271
|
setError(String(cause?.message ?? cause))
|
|
249
272
|
}
|
|
@@ -374,38 +397,45 @@ function SkillPickerButton(props) {
|
|
|
374
397
|
) : skills === undefined ? (
|
|
375
398
|
<div style={statusStyle}>加载中…</div>
|
|
376
399
|
) : (
|
|
377
|
-
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
event
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
400
|
+
<>
|
|
401
|
+
<div style={listStyle}>
|
|
402
|
+
{filtered.length === 0 ? (
|
|
403
|
+
<div style={statusStyle}>没有匹配的技能</div>
|
|
404
|
+
) : (
|
|
405
|
+
filtered.map((skill, index) => (
|
|
406
|
+
<button
|
|
407
|
+
key={skill.name}
|
|
408
|
+
type="button"
|
|
409
|
+
ref={(el) => {
|
|
410
|
+
itemRefs.current[index] = el
|
|
411
|
+
}}
|
|
412
|
+
onClick={() => pick(skill.name)}
|
|
413
|
+
onMouseEnter={(event) => {
|
|
414
|
+
setActive(index)
|
|
415
|
+
event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))'
|
|
416
|
+
}}
|
|
417
|
+
onMouseLeave={(event) => {
|
|
418
|
+
event.currentTarget.style.background = 'transparent'
|
|
419
|
+
}}
|
|
420
|
+
style={{
|
|
421
|
+
...itemStyle,
|
|
422
|
+
...(index === active
|
|
423
|
+
? { background: 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))' }
|
|
424
|
+
: {}),
|
|
425
|
+
}}
|
|
426
|
+
>
|
|
427
|
+
<span style={nameStyle}>{`/${skill.name}`}</span>
|
|
428
|
+
<span style={descStyle}>{skill.description ?? ''}</span>
|
|
429
|
+
</button>
|
|
430
|
+
))
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
{source === 'host' && (
|
|
434
|
+
<div style={sourceBadgeStyle} title="官方技能 API 不可用,列表来自本地目录扫描(与官方 / 补全同源)">
|
|
435
|
+
<span style={sourceBadgeTextStyle}>本地扫描</span>
|
|
436
|
+
</div>
|
|
407
437
|
)}
|
|
408
|
-
|
|
438
|
+
</>
|
|
409
439
|
)}
|
|
410
440
|
</div>
|
|
411
441
|
)}
|
|
@@ -415,12 +445,21 @@ function SkillPickerButton(props) {
|
|
|
415
445
|
|
|
416
446
|
/** Apply the browser half: register the picker into the composer tool row. */
|
|
417
447
|
export function apply(ctx) {
|
|
418
|
-
//
|
|
419
|
-
//
|
|
448
|
+
// Resolve the trigger pipeline service at apply scope (not inside an effect),
|
|
449
|
+
// exactly like the official ui-skill plugin does — in DSH 0.1.2-alpha.x the
|
|
450
|
+
// service lives in @deepseek-ai/dsh-client-ui-input-trigger and is only
|
|
451
|
+
// reachable from the plugin root context.
|
|
452
|
+
const inputTriggers = (typeof ctx.get === 'function' ? ctx.get('inputTriggers') : ctx.inputTriggers)
|
|
453
|
+
|
|
454
|
+
// Primary skill source: the official host skills API. In DSH 0.1.2-alpha.x
|
|
455
|
+
// the RPC moved from `connection.api.skills` (rc.x) to `remote.skills`
|
|
456
|
+
// (used by the official ui-skill plugin); try both before falling back.
|
|
420
457
|
const listSkills = async (sessionId) => {
|
|
421
|
-
const
|
|
458
|
+
const remoteSkills = ctx.remote?.skills
|
|
459
|
+
const connectionSkills = ctx.connection?.api?.skills
|
|
460
|
+
const skills = remoteSkills ?? connectionSkills
|
|
422
461
|
if (skills === undefined || typeof skills.list !== 'function') {
|
|
423
|
-
throw new Error('connection.api.skills
|
|
462
|
+
throw new Error('skills RPC unavailable (remote.skills / connection.api.skills)')
|
|
424
463
|
}
|
|
425
464
|
const controller = new AbortController()
|
|
426
465
|
const { result } = await skills.list({ sessionId }, controller.signal)
|
|
@@ -441,6 +480,29 @@ export function apply(ctx) {
|
|
|
441
480
|
currentCwd = ''
|
|
442
481
|
}
|
|
443
482
|
}
|
|
483
|
+
|
|
484
|
+
// Host-route fallback: same scan the ⚡ panel uses (official provider roots).
|
|
485
|
+
const fetchHostSkills = async () => {
|
|
486
|
+
const cwd = typeof currentCwd === 'string' && currentCwd !== '' ? `?cwd=${encodeURIComponent(currentCwd)}` : ''
|
|
487
|
+
const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: 'application/json' } })
|
|
488
|
+
const json = await res.json()
|
|
489
|
+
if (!json.ok) throw new Error(json.error || 'bad response')
|
|
490
|
+
return Array.isArray(json.skills) ? json.skills : []
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Resolve skills by official RPC first, then the host scan route; never
|
|
494
|
+
// throws (returns [] when both fail) so `/` completion degrades gracefully.
|
|
495
|
+
const resolveSkills = async (sessionId) => {
|
|
496
|
+
try {
|
|
497
|
+
return await listSkills(sessionId)
|
|
498
|
+
} catch {
|
|
499
|
+
try {
|
|
500
|
+
return await fetchHostSkills()
|
|
501
|
+
} catch {
|
|
502
|
+
return []
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
444
506
|
syncCwd()
|
|
445
507
|
const unsubscribe = ctx.sessions.list.subscribe(syncCwd)
|
|
446
508
|
ctx.effect(() => {
|
|
@@ -477,7 +539,7 @@ export function apply(ctx) {
|
|
|
477
539
|
}
|
|
478
540
|
const refreshNames = async (sessionId) => {
|
|
479
541
|
try {
|
|
480
|
-
const skills = await
|
|
542
|
+
const skills = await resolveSkills(sessionId)
|
|
481
543
|
namesCache.set(sessionId, (Array.isArray(skills) ? skills : []).map((s) => s.name))
|
|
482
544
|
notifyLexicon(sessionId)
|
|
483
545
|
} catch {
|
|
@@ -490,7 +552,7 @@ export function apply(ctx) {
|
|
|
490
552
|
name: 'skill-fuzzy',
|
|
491
553
|
order: -10,
|
|
492
554
|
async candidates(session, { query, signal }) {
|
|
493
|
-
const skills = await
|
|
555
|
+
const skills = await resolveSkills(session.sessionId)
|
|
494
556
|
if (signal.aborted) return []
|
|
495
557
|
namesCache.set(session.sessionId, skills.map((s) => s.name))
|
|
496
558
|
notifyLexicon(session.sessionId)
|
|
@@ -551,7 +613,13 @@ export function apply(ctx) {
|
|
|
551
613
|
return { text: `/${candidate.name} ` }
|
|
552
614
|
},
|
|
553
615
|
}
|
|
554
|
-
|
|
616
|
+
// inputTriggers is resolved at apply scope (see top of apply); fail loudly
|
|
617
|
+
// instead of silently dropping the fuzzy `/` source.
|
|
618
|
+
if (inputTriggers === undefined || typeof inputTriggers.registerSource !== 'function') {
|
|
619
|
+
console.warn('[dsh-skill-picker] inputTriggers service unavailable; fuzzy / completion disabled (⚡ panel still works)')
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
const unregister = inputTriggers.registerSource(source)
|
|
555
623
|
return () => {
|
|
556
624
|
unregister()
|
|
557
625
|
namesCache.clear()
|
package/src/index.js
CHANGED
|
@@ -38,6 +38,19 @@ function userSkillsDir() {
|
|
|
38
38
|
return path.join(home, 'skills')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the user agents-home skills directory, mirroring the official
|
|
43
|
+
* provider's default (`$DSH_AGENTS_HOME` > `~/.agents`). This is the
|
|
44
|
+
* cross-tool `.agents` convention; the official `dsh-skill-filesystem` scans
|
|
45
|
+
* it as its `user-agents` root (rank 500), so the fallback must too — else
|
|
46
|
+
* the picker's list silently misses skills that DSH's own `/` completion
|
|
47
|
+
* shows (issue #5).
|
|
48
|
+
*/
|
|
49
|
+
function userAgentsSkillsDir() {
|
|
50
|
+
const agentsHome = process.env.DSH_AGENTS_HOME ?? path.join(os.homedir(), '.agents')
|
|
51
|
+
return path.join(agentsHome, 'skills')
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/** Parse a SKILL.md frontmatter block into a key/value map (flat YAML subset). */
|
|
42
55
|
function parseFrontmatter(content) {
|
|
43
56
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
@@ -80,17 +93,21 @@ async function scanSkillsDirInto(map, dir) {
|
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
/**
|
|
83
|
-
* Scan
|
|
84
|
-
*
|
|
85
|
-
*
|
|
96
|
+
* Scan the same roots the official `dsh-skill-filesystem` provider uses, so
|
|
97
|
+
* the fallback route stays in sync with what agents actually load:
|
|
98
|
+
* project `.dsh/skills` (rank 100) > project `.agents/skills` (200) >
|
|
99
|
+
* user `~/.dsh/skills` (400) > user `~/.agents/skills` (500). Scanned
|
|
100
|
+
* low-priority first, so later writes (higher priority) win in the map.
|
|
101
|
+
* Never throws (a missing dir yields []).
|
|
86
102
|
* @param cwd - the active session's workspace root (undefined = user level only).
|
|
87
103
|
*/
|
|
88
104
|
async function scanSkills(cwd) {
|
|
89
105
|
const map = new Map()
|
|
106
|
+
await scanSkillsDirInto(map, userAgentsSkillsDir())
|
|
90
107
|
await scanSkillsDirInto(map, userSkillsDir())
|
|
91
108
|
if (typeof cwd === 'string' && cwd !== '') {
|
|
92
|
-
await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))
|
|
93
109
|
await scanSkillsDirInto(map, path.join(cwd, '.agents', 'skills'))
|
|
110
|
+
await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))
|
|
94
111
|
}
|
|
95
112
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
|
|
96
113
|
}
|