dsh-skill-picker 0.3.2 → 0.3.3

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/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 user-level plus (when a workspace cwd is known) project-level skill\n * directories, mirroring the official provider's roots. Never throws (a\n * missing user 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, userSkillsDir())\n if (typeof cwd === 'string' && cwd !== '') {\n await scanSkillsDirInto(map, path.join(cwd, '.dsh', 'skills'))\n await scanSkillsDirInto(map, path.join(cwd, '.agents', '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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",
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": ";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;AAUA,SAAS,sBAAsB;AAC7B,QAAM,aAAa,QAAQ,IAAI,mBAAmB,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS;AACnF,SAAO,KAAK,KAAK,YAAY,QAAQ;AACvC;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;AAWA,eAAe,WAAW,KAAK;AAC7B,QAAM,MAAM,oBAAI,IAAI;AACpB,QAAM,kBAAkB,KAAK,oBAAoB,CAAC;AAClD,QAAM,kBAAkB,KAAK,cAAc,CAAC;AAC5C,MAAI,OAAO,QAAQ,YAAY,QAAQ,IAAI;AACzC,UAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK,WAAW,QAAQ,CAAC;AAChE,UAAM,kBAAkB,KAAK,KAAK,KAAK,KAAK,QAAQ,QAAQ,CAAC;AAAA,EAC/D;AACA,SAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AACtE;AAMO,SAAS,MAAM,KAAK;AACzB,MAAI,OAAO,MAAM;AACf,UAAM,UAAU,OAAO,KAAK,QAAQ;AAClC,UAAI;AAEF,cAAM,MAAM,IAAI,QAAQ,SAAY,IAAI,IAAI,IAAI,KAAK,YAAY,EAAE,aAAa,IAAI,KAAK,KAAK,SAAY;AAC1G,cAAM,SAAS,MAAM,WAAW,GAAG;AACnC,YAAI,UAAU,KAAK,EAAE,gBAAgB,kCAAkC,CAAC;AACxE,YAAI,IAAI,KAAK,UAAU,EAAE,IAAI,MAAM,UAAU,MAAM,OAAO,CAAC,CAAC;AAAA,MAC9D,SAAS,OAAO;AACd,YAAI,UAAU,KAAK,EAAE,gBAAgB,kCAAkC,CAAC;AACxE,YAAI,IAAI,KAAK,UAAU,EAAE,IAAI,OAAO,OAAO,OAAO,OAAO,WAAW,KAAK,EAAE,CAAC,CAAC;AAAA,MAC/E;AAAA,IACF;AACA,WAAO,IAAI,UAAU,SAAS,EAAE,MAAM,UAAU,MAAM,qBAAqB,QAAQ,CAAC;AAAA,EACtF,GAAG,0BAA0B;AAE7B,MAAI,OAAO,MAAM,IAAI,aAAa,QAAQ;AAAA,IACxC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC,GAAG,kCAAkC;AACxC;",
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.2",
4
+ "version": "0.3.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -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 (user + project level dirs).
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
- <div style={listStyle}>
378
- {filtered.length === 0 ? (
379
- <div style={statusStyle}>没有匹配的技能</div>
380
- ) : (
381
- filtered.map((skill, index) => (
382
- <button
383
- key={skill.name}
384
- type="button"
385
- ref={(el) => {
386
- itemRefs.current[index] = el
387
- }}
388
- onClick={() => pick(skill.name)}
389
- onMouseEnter={(event) => {
390
- setActive(index)
391
- event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))'
392
- }}
393
- onMouseLeave={(event) => {
394
- event.currentTarget.style.background = 'transparent'
395
- }}
396
- style={{
397
- ...itemStyle,
398
- ...(index === active
399
- ? { background: 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))' }
400
- : {}),
401
- }}
402
- >
403
- <span style={nameStyle}>{`/${skill.name}`}</span>
404
- <span style={descStyle}>{skill.description ?? ''}</span>
405
- </button>
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
- </div>
438
+ </>
409
439
  )}
410
440
  </div>
411
441
  )}
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 user-level plus (when a workspace cwd is known) project-level skill
84
- * directories, mirroring the official provider's roots. Never throws (a
85
- * missing user dir yields []).
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
  }