dsh-skill-picker 0.1.0 → 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 CHANGED
@@ -22,9 +22,10 @@ English: A skill picker for the DSH Web GUI — a button in the composer's right
22
22
  ## 特性
23
23
 
24
24
  - ⚡ 一键弹出全部技能(闪电图标,人人看得懂)
25
+ - **`/` 直接补全**:输入斜杠即列出全部技能,**模糊搜索**(技能名+描述任意匹配)+ **常用排序**(v0.2.0)
25
26
  - 🔍 实时搜索(技能名 / 描述都搜)
26
27
  - 🧠 **最近使用置顶、常用靠前**的智能排序(WorkBuddy 同款)
27
- - 📋 直接扫描 `~/.dsh/skills`(官方技能 provider 同源目录,标准安装即用)
28
+ - 📋 走官方宿主 skills API(与 DSH 内置 `/` 补全同一数据源,自动覆盖用户级+项目级技能)
28
29
  - 🧩 插入官方 `/技能名` 手势,加载/执行走 DSH 原生机制,**零 agent 侧改动**
29
30
  - 🎨 跟随 Web UI 主题(CSS 变量),浅色/深色自适应
30
31
  - 📦 纯 client + host 双半插件,无第三方运行时依赖
package/lib/client.js CHANGED
@@ -36,7 +36,7 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
  var import_react = __toESM(require("react"), 1);
38
38
  var import_jsx_runtime = require("react/jsx-runtime");
39
- var inject = ["slots", "connection", "sessions"];
39
+ var inject = ["slots", "connection", "sessions", "inputTriggers"];
40
40
  var USAGE_KEY = "dsh-skill-picker:usage";
41
41
  function loadUsage() {
42
42
  try {
@@ -326,6 +326,45 @@ function apply(ctx) {
326
326
  unsubscribe();
327
327
  };
328
328
  }, "dsh-skill-picker: composer input slot");
329
+ ctx.effect(() => {
330
+ const source = {
331
+ trigger: "/",
332
+ name: "skill-fuzzy",
333
+ order: -10,
334
+ async candidates(session, { query, signal }) {
335
+ const skills = await listSkills(session.sessionId);
336
+ if (signal.aborted) return [];
337
+ const usage = loadUsage();
338
+ const ordered = skills.slice().sort((a, b) => {
339
+ const ua = usage[a.name];
340
+ const ub = usage[b.name];
341
+ const la = ua?.lastUsed ?? 0;
342
+ const lb = ub?.lastUsed ?? 0;
343
+ if (la !== lb) return lb - la;
344
+ const ca = ua?.count ?? 0;
345
+ const cb = ub?.count ?? 0;
346
+ if (ca !== cb) return cb - ca;
347
+ return a.name.localeCompare(b.name);
348
+ });
349
+ const q = String(query ?? "").trim().toLowerCase();
350
+ const matches = q === "" ? ordered : ordered.filter(
351
+ (s) => s.name.toLowerCase().includes(q) || String(s.description ?? "").toLowerCase().includes(q)
352
+ );
353
+ return matches.slice(0, 50).map((s) => ({ name: s.name, description: s.description }));
354
+ },
355
+ warm(session) {
356
+ listSkills(session.sessionId).catch(() => {
357
+ });
358
+ },
359
+ onPick({ candidate }) {
360
+ return { text: `/${candidate.name} ` };
361
+ }
362
+ };
363
+ const unregister = ctx.inputTriggers.registerSource(source);
364
+ return () => {
365
+ unregister();
366
+ };
367
+ }, "dsh-skill-picker: fuzzy / source");
329
368
  }
330
369
  return module.exports; } });
331
370
  //# sourceMappingURL=client.js.map
package/lib/client.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/client/index.jsx"],
4
- "sourcesContent": ["/**\n * dsh-skill-picker \u2014 browser half: mounts a skill-picker control into the\n * composer's right tool row (`conversation.input.right` list slot, the seat\n * just before the send button). Clicking it opens a searchable list of\n * installed skills (fetched from the host route `/dsh-skill-picker/skills`),\n * ordered by usage: recently picked first, then frequently used, then the\n * untouched rest (by name). Picking one inserts the official `/skill-name`\n * gesture into the draft via the framework input machine\n * (`inputActions.setDraft`), so DSH's native user-invocation path loads the\n * skill with the message.\n *\n * All DOM/runtime wiring failures are logged, never thrown \u2014 the web shell\n * fails the whole boot when a plugin apply throws.\n *\n * @module dsh-skill-picker/client\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\n/** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback). */\nexport const inject = ['slots', 'connection', 'sessions']\n\n/** localStorage key for the picker's per-browser usage history. */\nconst USAGE_KEY = 'dsh-skill-picker:usage'\n\n/** Read the usage history {name: {count, lastUsed}}; never throws. */\nfunction loadUsage() {\n try {\n const raw = localStorage.getItem(USAGE_KEY)\n if (raw === null) return {}\n const parsed = JSON.parse(raw)\n return parsed !== null && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n}\n\n/** Persist the usage history; never throws. */\nfunction saveUsage(usage) {\n try {\n localStorage.setItem(USAGE_KEY, JSON.stringify(usage))\n } catch {\n /* storage unavailable \u2014 ordering just won't persist */\n }\n}\n\n/** Row height matches the resident chrome (access mode, plan, attach, model). */\nconst buttonStyle = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '28px',\n height: '28px',\n margin: '0 2px',\n padding: '0',\n border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))',\n borderRadius: '8px',\n background: 'transparent',\n color: 'var(--dsw-alias-label-secondary, #c9d2e0)',\n cursor: 'pointer',\n fontSize: '15px',\n lineHeight: '1',\n flex: 'none',\n}\n\nconst popoverStyle = {\n position: 'absolute',\n bottom: 'calc(100% + 8px)',\n right: '0',\n width: '340px',\n maxHeight: '320px',\n display: 'flex',\n flexDirection: 'column',\n background: 'var(--dsw-specific-tip, #1e2533)',\n border: '1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.35))',\n borderRadius: '12px',\n boxShadow: '0 8px 28px rgba(0,0,0,0.35)',\n overflow: 'hidden',\n zIndex: 1000,\n}\n\nconst searchStyle = {\n boxSizing: 'border-box',\n width: 'calc(100% - 16px)',\n margin: '8px',\n padding: '6px 10px',\n border: '1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.3))',\n borderRadius: '8px',\n background: 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.1))',\n color: 'var(--dsw-alias-label-primary, #e6ebf2)',\n fontSize: '13px',\n outline: 'none',\n}\n\nconst listStyle = {\n overflowY: 'auto',\n flex: 'auto',\n padding: '0 6px 8px',\n}\n\nconst itemStyle = {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n gap: '2px',\n width: '100%',\n padding: '7px 10px',\n border: 'none',\n borderRadius: '8px',\n background: 'transparent',\n color: 'var(--dsw-alias-label-primary, #e6ebf2)',\n cursor: 'pointer',\n textAlign: 'left',\n}\n\nconst nameStyle = {\n fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',\n fontSize: '13px',\n fontWeight: 500,\n}\n\nconst descStyle = {\n color: 'var(--dsw-alias-label-tertiary, #8a94a6)',\n fontSize: '12px',\n lineHeight: '16px',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n maxWidth: '100%',\n}\n\nconst statusStyle = {\n padding: '12px',\n color: 'var(--dsw-alias-label-tertiary, #8a94a6)',\n fontSize: '13px',\n}\n\n/** The picker's bolt glyph: DeepSeek palette gradient + slim stroke. */\nfunction BoltIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" aria-hidden=\"true\" style={{ display: 'block' }}>\n <defs>\n <linearGradient id=\"dsh-sp-bolt-grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"var(--dsw-static-deepseek-400, rgb(103, 158, 254))\" />\n <stop offset=\"100%\" stopColor=\"var(--dsw-static-deepseek-600, rgb(72, 104, 178))\" />\n </linearGradient>\n </defs>\n <path\n d=\"M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z\"\n fill=\"url(#dsh-sp-bolt-grad)\"\n stroke=\"var(--dsw-static-deepseek-600, rgb(72, 104, 178))\"\n strokeWidth=\"0.8\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )\n}\n\n/**\n * The picker control for the composer's right tool row. Rendered by the slot\n * renderer with the composed props: owner InputZone (`session`, `input`) plus\n * the framework session kit (`useInput`, `inputActions`).\n */\nfunction SkillPickerButton(props) {\n const [open, setOpen] = useState(false)\n const [skills, setSkills] = useState(undefined)\n const [error, setError] = useState(undefined)\n const [query, setQuery] = useState('')\n const [usage, setUsage] = useState(() => loadUsage())\n const boxRef = useRef(null)\n\n const load = useCallback(async () => {\n if (skills !== undefined || error !== undefined) return\n try {\n // Primary path: the official host skills API (same source as DSH's own\n // `/` completion \u2014 session-scoped, covers user + project level).\n if (typeof props.listSkills === 'function' && props.session?.sessionId !== undefined) {\n const listed = await props.listSkills(props.session.sessionId)\n setSkills(Array.isArray(listed) ? listed : [])\n return\n }\n } catch (cause) {\n console.warn('[dsh-skill-picker] official skills API failed, falling back to host route:', cause)\n }\n // Fallback path: the host's own scan route (user + project level dirs).\n try {\n const cwd = typeof props.cwd === 'string' && props.cwd !== '' ? `?cwd=${encodeURIComponent(props.cwd)}` : ''\n const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: 'application/json' } })\n const json = await res.json()\n if (!json.ok) throw new Error(json.error || 'bad response')\n setSkills(Array.isArray(json.skills) ? json.skills : [])\n } catch (cause) {\n setError(String(cause?.message ?? cause))\n }\n }, [skills, error, props.listSkills, props.session, props.cwd])\n\n const toggle = () => {\n if (!open) void load()\n setOpen(!open)\n }\n\n const pick = (name) => {\n // Draft source: owner InputZone snapshot first, framework hook second.\n let draft = ''\n try {\n if (props.input !== undefined && typeof props.input.draft === 'string') {\n draft = props.input.draft\n } else if (typeof props.useInput === 'function') {\n const state = props.useInput((s) => s)\n if (state !== undefined && typeof state.draft === 'string') draft = state.draft\n }\n } catch (cause) {\n console.error('[dsh-skill-picker] reading draft failed:', cause)\n }\n const separator = draft === '' || draft.endsWith(' ') || draft.endsWith('\\n') ? '' : ' '\n const next = `${draft}${separator}/${name} `\n try {\n if (typeof props.inputActions?.setDraft === 'function') {\n props.inputActions.setDraft(next)\n } else {\n console.error('[dsh-skill-picker] inputActions.setDraft unavailable; draft not written:', next)\n }\n } catch (cause) {\n console.error('[dsh-skill-picker] setDraft failed:', cause)\n }\n\n // Record usage for ordering (recent first, then frequent).\n const nextUsage = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }\n setUsage(nextUsage)\n saveUsage(nextUsage)\n\n setOpen(false)\n setQuery('')\n }\n\n // Close on outside pointer-down (the shell's menu convention).\n useEffect(() => {\n if (!open) return\n const onDown = (event) => {\n if (boxRef.current !== null && !boxRef.current.contains(event.target)) setOpen(false)\n }\n document.addEventListener('mousedown', onDown)\n return () => document.removeEventListener('mousedown', onDown)\n }, [open])\n\n // Usage ordering: last picked first, then most frequent, then by name.\n const ordered = (skills ?? []).slice().sort((a, b) => {\n const ua = usage[a.name]\n const ub = usage[b.name]\n const la = ua?.lastUsed ?? 0\n const lb = ub?.lastUsed ?? 0\n if (la !== lb) return lb - la\n const ca = ua?.count ?? 0\n const cb = ub?.count ?? 0\n if (ca !== cb) return cb - ca\n return a.name.localeCompare(b.name)\n })\n\n const filtered = ordered\n .filter((skill) => {\n const q = query.trim().toLowerCase()\n if (q === '') return true\n return skill.name.toLowerCase().includes(q) || String(skill.description ?? '').toLowerCase().includes(q)\n })\n .slice(0, 60)\n\n return (\n <div ref={boxRef} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }}>\n <button\n type=\"button\"\n onClick={toggle}\n title=\"\u9009\u62E9\u6280\u80FD\uFF08\u63D2\u5165 /\u6280\u80FD\u540D \u5230\u53D1\u9001\u6846\uFF09\"\n aria-label=\"\u9009\u62E9\u6280\u80FD\"\n style={{\n ...buttonStyle,\n ...(open ? { color: 'var(--dsw-alias-label-primary-bluish, #4cc9f0)' } : {}),\n }}\n >\n <BoltIcon />\n </button>\n {open && (\n <div style={popoverStyle}>\n <input\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder=\"\u641C\u7D22\u6280\u80FD\u2026\"\n style={searchStyle}\n autoFocus\n />\n {error !== undefined ? (\n <div style={statusStyle}>{`\u52A0\u8F7D\u5931\u8D25\uFF1A${error}`}</div>\n ) : skills === undefined ? (\n <div style={statusStyle}>\u52A0\u8F7D\u4E2D\u2026</div>\n ) : (\n <div style={listStyle}>\n {filtered.length === 0 ? (\n <div style={statusStyle}>\u6CA1\u6709\u5339\u914D\u7684\u6280\u80FD</div>\n ) : (\n filtered.map((skill) => (\n <button\n key={skill.name}\n type=\"button\"\n onClick={() => pick(skill.name)}\n onMouseEnter={(event) => {\n event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))'\n }}\n onMouseLeave={(event) => {\n event.currentTarget.style.background = 'transparent'\n }}\n style={itemStyle}\n >\n <span style={nameStyle}>{`/${skill.name}`}</span>\n <span style={descStyle}>{skill.description ?? ''}</span>\n </button>\n ))\n )}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/** Apply the browser half: register the picker into the composer tool row. */\nexport function apply(ctx) {\n // Primary skill source: the official host skills API (the exact RPC ui-skill\n // feeds DSH's own `/` completion with \u2014 session-scoped, all skill layers).\n const listSkills = async (sessionId) => {\n const skills = ctx.connection?.api?.skills\n if (skills === undefined || typeof skills.list !== 'function') {\n throw new Error('connection.api.skills unavailable')\n }\n const controller = new AbortController()\n const { result } = await skills.list({ sessionId }, controller.signal)\n if (!result.ok) throw new Error(`skill.list failed: ${result.error?.code}: ${result.error?.message}`)\n const raw = result.value?.skills ?? []\n return raw.map((skill) => ({ name: skill.name, description: skill.description ?? '' }))\n }\n\n // Track the active session's workspace cwd for the host-route fallback.\n let currentCwd = ''\n const syncCwd = () => {\n try {\n const snapshot = ctx.sessions.list.getSnapshot()\n const sessionId = snapshot.current\n const cwd = sessionId === undefined ? undefined : snapshot.byId[sessionId]?.cwd\n currentCwd = typeof cwd === 'string' ? cwd : ''\n } catch {\n currentCwd = ''\n }\n }\n syncCwd()\n const unsubscribe = ctx.sessions.list.subscribe(syncCwd)\n ctx.effect(() => {\n // Wrap the component so framework props pass through untouched and the\n // live workspace cwd + official skills fetcher are attached \u2014 never\n // swallow the composed props.\n const PickerWithCwd = (props) =>\n React.createElement(SkillPickerButton, { ...props, cwd: currentCwd, listSkills })\n const dispose = ctx.slots.inject('conversation.input.right', () =>\n ctx.slots.register(\n { name: 'conversation.input.right', id: 'skill-picker', order: 100, label: 'Skill picker' },\n PickerWithCwd,\n ),\n )\n return () => {\n dispose()\n unsubscribe()\n }\n }, 'dsh-skill-picker: composer input slot')\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * dsh-skill-picker \u2014 browser half: mounts a skill-picker control into the\n * composer's right tool row (`conversation.input.right` list slot, the seat\n * just before the send button). Clicking it opens a searchable list of\n * installed skills (fetched from the host route `/dsh-skill-picker/skills`),\n * ordered by usage: recently picked first, then frequently used, then the\n * untouched rest (by name). Picking one inserts the official `/skill-name`\n * gesture into the draft via the framework input machine\n * (`inputActions.setDraft`), so DSH's native user-invocation path loads the\n * skill with the message.\n *\n * All DOM/runtime wiring failures are logged, never thrown \u2014 the web shell\n * fails the whole boot when a plugin apply throws.\n *\n * @module dsh-skill-picker/client\n */\n\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\n\n/** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback), input triggers (/ fuzzy source). */\nexport const inject = ['slots', 'connection', 'sessions', 'inputTriggers']\n\n/** localStorage key for the picker's per-browser usage history. */\nconst USAGE_KEY = 'dsh-skill-picker:usage'\n\n/** Read the usage history {name: {count, lastUsed}}; never throws. */\nfunction loadUsage() {\n try {\n const raw = localStorage.getItem(USAGE_KEY)\n if (raw === null) return {}\n const parsed = JSON.parse(raw)\n return parsed !== null && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n}\n\n/** Persist the usage history; never throws. */\nfunction saveUsage(usage) {\n try {\n localStorage.setItem(USAGE_KEY, JSON.stringify(usage))\n } catch {\n /* storage unavailable \u2014 ordering just won't persist */\n }\n}\n\n/** Row height matches the resident chrome (access mode, plan, attach, model). */\nconst buttonStyle = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n width: '28px',\n height: '28px',\n margin: '0 2px',\n padding: '0',\n border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25))',\n borderRadius: '8px',\n background: 'transparent',\n color: 'var(--dsw-alias-label-secondary, #c9d2e0)',\n cursor: 'pointer',\n fontSize: '15px',\n lineHeight: '1',\n flex: 'none',\n}\n\nconst popoverStyle = {\n position: 'absolute',\n bottom: 'calc(100% + 8px)',\n right: '0',\n width: '340px',\n maxHeight: '320px',\n display: 'flex',\n flexDirection: 'column',\n background: 'var(--dsw-specific-tip, #1e2533)',\n border: '1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.35))',\n borderRadius: '12px',\n boxShadow: '0 8px 28px rgba(0,0,0,0.35)',\n overflow: 'hidden',\n zIndex: 1000,\n}\n\nconst searchStyle = {\n boxSizing: 'border-box',\n width: 'calc(100% - 16px)',\n margin: '8px',\n padding: '6px 10px',\n border: '1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.3))',\n borderRadius: '8px',\n background: 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.1))',\n color: 'var(--dsw-alias-label-primary, #e6ebf2)',\n fontSize: '13px',\n outline: 'none',\n}\n\nconst listStyle = {\n overflowY: 'auto',\n flex: 'auto',\n padding: '0 6px 8px',\n}\n\nconst itemStyle = {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-start',\n gap: '2px',\n width: '100%',\n padding: '7px 10px',\n border: 'none',\n borderRadius: '8px',\n background: 'transparent',\n color: 'var(--dsw-alias-label-primary, #e6ebf2)',\n cursor: 'pointer',\n textAlign: 'left',\n}\n\nconst nameStyle = {\n fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',\n fontSize: '13px',\n fontWeight: 500,\n}\n\nconst descStyle = {\n color: 'var(--dsw-alias-label-tertiary, #8a94a6)',\n fontSize: '12px',\n lineHeight: '16px',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n maxWidth: '100%',\n}\n\nconst statusStyle = {\n padding: '12px',\n color: 'var(--dsw-alias-label-tertiary, #8a94a6)',\n fontSize: '13px',\n}\n\n/** The picker's bolt glyph: DeepSeek palette gradient + slim stroke. */\nfunction BoltIcon() {\n return (\n <svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" aria-hidden=\"true\" style={{ display: 'block' }}>\n <defs>\n <linearGradient id=\"dsh-sp-bolt-grad\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"var(--dsw-static-deepseek-400, rgb(103, 158, 254))\" />\n <stop offset=\"100%\" stopColor=\"var(--dsw-static-deepseek-600, rgb(72, 104, 178))\" />\n </linearGradient>\n </defs>\n <path\n d=\"M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z\"\n fill=\"url(#dsh-sp-bolt-grad)\"\n stroke=\"var(--dsw-static-deepseek-600, rgb(72, 104, 178))\"\n strokeWidth=\"0.8\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )\n}\n\n/**\n * The picker control for the composer's right tool row. Rendered by the slot\n * renderer with the composed props: owner InputZone (`session`, `input`) plus\n * the framework session kit (`useInput`, `inputActions`).\n */\nfunction SkillPickerButton(props) {\n const [open, setOpen] = useState(false)\n const [skills, setSkills] = useState(undefined)\n const [error, setError] = useState(undefined)\n const [query, setQuery] = useState('')\n const [usage, setUsage] = useState(() => loadUsage())\n const boxRef = useRef(null)\n\n const load = useCallback(async () => {\n if (skills !== undefined || error !== undefined) return\n try {\n // Primary path: the official host skills API (same source as DSH's own\n // `/` completion \u2014 session-scoped, covers user + project level).\n if (typeof props.listSkills === 'function' && props.session?.sessionId !== undefined) {\n const listed = await props.listSkills(props.session.sessionId)\n setSkills(Array.isArray(listed) ? listed : [])\n return\n }\n } catch (cause) {\n console.warn('[dsh-skill-picker] official skills API failed, falling back to host route:', cause)\n }\n // Fallback path: the host's own scan route (user + project level dirs).\n try {\n const cwd = typeof props.cwd === 'string' && props.cwd !== '' ? `?cwd=${encodeURIComponent(props.cwd)}` : ''\n const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: 'application/json' } })\n const json = await res.json()\n if (!json.ok) throw new Error(json.error || 'bad response')\n setSkills(Array.isArray(json.skills) ? json.skills : [])\n } catch (cause) {\n setError(String(cause?.message ?? cause))\n }\n }, [skills, error, props.listSkills, props.session, props.cwd])\n\n const toggle = () => {\n if (!open) void load()\n setOpen(!open)\n }\n\n const pick = (name) => {\n // Draft source: owner InputZone snapshot first, framework hook second.\n let draft = ''\n try {\n if (props.input !== undefined && typeof props.input.draft === 'string') {\n draft = props.input.draft\n } else if (typeof props.useInput === 'function') {\n const state = props.useInput((s) => s)\n if (state !== undefined && typeof state.draft === 'string') draft = state.draft\n }\n } catch (cause) {\n console.error('[dsh-skill-picker] reading draft failed:', cause)\n }\n const separator = draft === '' || draft.endsWith(' ') || draft.endsWith('\\n') ? '' : ' '\n const next = `${draft}${separator}/${name} `\n try {\n if (typeof props.inputActions?.setDraft === 'function') {\n props.inputActions.setDraft(next)\n } else {\n console.error('[dsh-skill-picker] inputActions.setDraft unavailable; draft not written:', next)\n }\n } catch (cause) {\n console.error('[dsh-skill-picker] setDraft failed:', cause)\n }\n\n // Record usage for ordering (recent first, then frequent).\n const nextUsage = { ...usage, [name]: { count: (usage[name]?.count ?? 0) + 1, lastUsed: Date.now() } }\n setUsage(nextUsage)\n saveUsage(nextUsage)\n\n setOpen(false)\n setQuery('')\n }\n\n // Close on outside pointer-down (the shell's menu convention).\n useEffect(() => {\n if (!open) return\n const onDown = (event) => {\n if (boxRef.current !== null && !boxRef.current.contains(event.target)) setOpen(false)\n }\n document.addEventListener('mousedown', onDown)\n return () => document.removeEventListener('mousedown', onDown)\n }, [open])\n\n // Usage ordering: last picked first, then most frequent, then by name.\n const ordered = (skills ?? []).slice().sort((a, b) => {\n const ua = usage[a.name]\n const ub = usage[b.name]\n const la = ua?.lastUsed ?? 0\n const lb = ub?.lastUsed ?? 0\n if (la !== lb) return lb - la\n const ca = ua?.count ?? 0\n const cb = ub?.count ?? 0\n if (ca !== cb) return cb - ca\n return a.name.localeCompare(b.name)\n })\n\n const filtered = ordered\n .filter((skill) => {\n const q = query.trim().toLowerCase()\n if (q === '') return true\n return skill.name.toLowerCase().includes(q) || String(skill.description ?? '').toLowerCase().includes(q)\n })\n .slice(0, 60)\n\n return (\n <div ref={boxRef} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }}>\n <button\n type=\"button\"\n onClick={toggle}\n title=\"\u9009\u62E9\u6280\u80FD\uFF08\u63D2\u5165 /\u6280\u80FD\u540D \u5230\u53D1\u9001\u6846\uFF09\"\n aria-label=\"\u9009\u62E9\u6280\u80FD\"\n style={{\n ...buttonStyle,\n ...(open ? { color: 'var(--dsw-alias-label-primary-bluish, #4cc9f0)' } : {}),\n }}\n >\n <BoltIcon />\n </button>\n {open && (\n <div style={popoverStyle}>\n <input\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n placeholder=\"\u641C\u7D22\u6280\u80FD\u2026\"\n style={searchStyle}\n autoFocus\n />\n {error !== undefined ? (\n <div style={statusStyle}>{`\u52A0\u8F7D\u5931\u8D25\uFF1A${error}`}</div>\n ) : skills === undefined ? (\n <div style={statusStyle}>\u52A0\u8F7D\u4E2D\u2026</div>\n ) : (\n <div style={listStyle}>\n {filtered.length === 0 ? (\n <div style={statusStyle}>\u6CA1\u6709\u5339\u914D\u7684\u6280\u80FD</div>\n ) : (\n filtered.map((skill) => (\n <button\n key={skill.name}\n type=\"button\"\n onClick={() => pick(skill.name)}\n onMouseEnter={(event) => {\n event.currentTarget.style.background = 'var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))'\n }}\n onMouseLeave={(event) => {\n event.currentTarget.style.background = 'transparent'\n }}\n style={itemStyle}\n >\n <span style={nameStyle}>{`/${skill.name}`}</span>\n <span style={descStyle}>{skill.description ?? ''}</span>\n </button>\n ))\n )}\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n\n/** Apply the browser half: register the picker into the composer tool row. */\nexport function apply(ctx) {\n // Primary skill source: the official host skills API (the exact RPC ui-skill\n // feeds DSH's own `/` completion with \u2014 session-scoped, all skill layers).\n const listSkills = async (sessionId) => {\n const skills = ctx.connection?.api?.skills\n if (skills === undefined || typeof skills.list !== 'function') {\n throw new Error('connection.api.skills unavailable')\n }\n const controller = new AbortController()\n const { result } = await skills.list({ sessionId }, controller.signal)\n if (!result.ok) throw new Error(`skill.list failed: ${result.error?.code}: ${result.error?.message}`)\n const raw = result.value?.skills ?? []\n return raw.map((skill) => ({ name: skill.name, description: skill.description ?? '' }))\n }\n\n // Track the active session's workspace cwd for the host-route fallback.\n let currentCwd = ''\n const syncCwd = () => {\n try {\n const snapshot = ctx.sessions.list.getSnapshot()\n const sessionId = snapshot.current\n const cwd = sessionId === undefined ? undefined : snapshot.byId[sessionId]?.cwd\n currentCwd = typeof cwd === 'string' ? cwd : ''\n } catch {\n currentCwd = ''\n }\n }\n syncCwd()\n const unsubscribe = ctx.sessions.list.subscribe(syncCwd)\n ctx.effect(() => {\n // Wrap the component so framework props pass through untouched and the\n // live workspace cwd + official skills fetcher are attached \u2014 never\n // swallow the composed props.\n const PickerWithCwd = (props) =>\n React.createElement(SkillPickerButton, { ...props, cwd: currentCwd, listSkills })\n const dispose = ctx.slots.inject('conversation.input.right', () =>\n ctx.slots.register(\n { name: 'conversation.input.right', id: 'skill-picker', order: 100, label: 'Skill picker' },\n PickerWithCwd,\n ),\n )\n return () => {\n dispose()\n unsubscribe()\n }\n }, 'dsh-skill-picker: composer input slot')\n\n // Fuzzy `/` completion source: negative order puts the skill group ABOVE\n // the slash commands (command source sits at order 0), and beats the\n // official ui-skill prefix source too \u2014 typing `/` matches name AND\n // description anywhere, with usage ordering (same rule as the \u26A1 panel).\n ctx.effect(() => {\n const source = {\n trigger: '/',\n name: 'skill-fuzzy',\n order: -10,\n async candidates(session, { query, signal }) {\n const skills = await listSkills(session.sessionId)\n if (signal.aborted) return []\n const usage = loadUsage()\n // Usage ordering: last picked first, then most frequent, then by name\n // (same rule as the \u26A1 panel, read fresh so picks show up immediately).\n const ordered = skills.slice().sort((a, b) => {\n const ua = usage[a.name]\n const ub = usage[b.name]\n const la = ua?.lastUsed ?? 0\n const lb = ub?.lastUsed ?? 0\n if (la !== lb) return lb - la\n const ca = ua?.count ?? 0\n const cb = ub?.count ?? 0\n if (ca !== cb) return cb - ca\n return a.name.localeCompare(b.name)\n })\n const q = String(query ?? '').trim().toLowerCase()\n const matches =\n q === ''\n ? ordered\n : ordered.filter(\n (s) =>\n s.name.toLowerCase().includes(q) ||\n String(s.description ?? '').toLowerCase().includes(q),\n )\n // Empty `/` shows ALL skills (usage-ordered); typing fuzzy-filters.\n // Keep a generous cap so very large skill sets stay snappy.\n return matches.slice(0, 50).map((s) => ({ name: s.name, description: s.description }))\n },\n warm(session) {\n listSkills(session.sessionId).catch(() => {})\n },\n onPick({ candidate }) {\n return { text: `/${candidate.name} ` }\n },\n }\n const unregister = ctx.inputTriggers.registerSource(source)\n return () => {\n unregister()\n }\n }, 'dsh-skill-picker: fuzzy / source')\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["React"]
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.1.0",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -17,8 +17,8 @@
17
17
 
18
18
  import React, { useCallback, useEffect, useRef, useState } from 'react'
19
19
 
20
- /** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback). */
21
- export const inject = ['slots', 'connection', 'sessions']
20
+ /** Required services: slot registry, host connection (official skills API), sessions (workspace cwd fallback), input triggers (/ fuzzy source). */
21
+ export const inject = ['slots', 'connection', 'sessions', 'inputTriggers']
22
22
 
23
23
  /** localStorage key for the picker's per-browser usage history. */
24
24
  const USAGE_KEY = 'dsh-skill-picker:usage'
@@ -369,4 +369,56 @@ export function apply(ctx) {
369
369
  unsubscribe()
370
370
  }
371
371
  }, 'dsh-skill-picker: composer input slot')
372
+
373
+ // Fuzzy `/` completion source: negative order puts the skill group ABOVE
374
+ // the slash commands (command source sits at order 0), and beats the
375
+ // official ui-skill prefix source too — typing `/` matches name AND
376
+ // description anywhere, with usage ordering (same rule as the ⚡ panel).
377
+ ctx.effect(() => {
378
+ const source = {
379
+ trigger: '/',
380
+ name: 'skill-fuzzy',
381
+ order: -10,
382
+ async candidates(session, { query, signal }) {
383
+ const skills = await listSkills(session.sessionId)
384
+ if (signal.aborted) return []
385
+ const usage = loadUsage()
386
+ // Usage ordering: last picked first, then most frequent, then by name
387
+ // (same rule as the ⚡ panel, read fresh so picks show up immediately).
388
+ const ordered = skills.slice().sort((a, b) => {
389
+ const ua = usage[a.name]
390
+ const ub = usage[b.name]
391
+ const la = ua?.lastUsed ?? 0
392
+ const lb = ub?.lastUsed ?? 0
393
+ if (la !== lb) return lb - la
394
+ const ca = ua?.count ?? 0
395
+ const cb = ub?.count ?? 0
396
+ if (ca !== cb) return cb - ca
397
+ return a.name.localeCompare(b.name)
398
+ })
399
+ const q = String(query ?? '').trim().toLowerCase()
400
+ const matches =
401
+ q === ''
402
+ ? ordered
403
+ : ordered.filter(
404
+ (s) =>
405
+ s.name.toLowerCase().includes(q) ||
406
+ String(s.description ?? '').toLowerCase().includes(q),
407
+ )
408
+ // Empty `/` shows ALL skills (usage-ordered); typing fuzzy-filters.
409
+ // Keep a generous cap so very large skill sets stay snappy.
410
+ return matches.slice(0, 50).map((s) => ({ name: s.name, description: s.description }))
411
+ },
412
+ warm(session) {
413
+ listSkills(session.sessionId).catch(() => {})
414
+ },
415
+ onPick({ candidate }) {
416
+ return { text: `/${candidate.name} ` }
417
+ },
418
+ }
419
+ const unregister = ctx.inputTriggers.registerSource(source)
420
+ return () => {
421
+ unregister()
422
+ }
423
+ }, 'dsh-skill-picker: fuzzy / source')
372
424
  }