dsh-skill-picker 0.5.21 → 0.5.23

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
@@ -26,7 +26,7 @@ DSH Web GUI 的技能选择器:在输入框(composer)工具行右侧加一
26
26
 
27
27
  English: A skill picker for the DSH Web GUI — a button in the composer's right tool row opens a searchable list of installed skills; picking one inserts the official `/skill-name` gesture into the draft, so DSH's native user-invocation path loads the skill with your message.
28
28
 
29
- 当前版本:**v0.5.12**(**修复 `user-invocable: false` 的技能未被隐藏**(issue #10)+ 修复全局安装下 `/` 补全增强静默失效(issue #7)+ ⚡ 面板**置顶分组** + `/` 补全**自动增强补丁** + 拼音搜索 + **搜索结果按匹配相关度排序**)
29
+ 当前版本:**v0.5.22**(**「置顶 / 最近使用」在桌面端、网页端和手机之间共用同一份**(存于 `$DSH_HOME/dsh-skill-picker-state.json`,不再各存各的 localStorage)+ **`/` 菜单与 ⚡ 面板排序完全一致**(空查询也走同一套排序,置顶排最前)+ **修掉「打 `/` 菜单开到下面去」**(microtask 竞态:技能组必须 0 个 `await` 才抢得到高亮)+ **文件补丁退休**(`/` 增强改为运行时接管,默认不再改任何官方文件,并会自动把改过的还原)+ 模糊/拼音匹配 + ⚡ 面板置顶分组 + 搜索结果按匹配相关度排序)
30
30
 
31
31
  ## 为什么用它(vs 官方 `/` 补全)
32
32
 
@@ -141,6 +141,8 @@ source.onPick = (args) => { track(args?.candidate?.name); return onPick.call(sou
141
141
 
142
142
  ## 更新日志
143
143
 
144
+ - **v0.5.23**:**⚡ 面板里可以直接管理技能了:关闭 / 开启 · 定位 · 卸载(可撤回)**——起因是国豪视频评论区有人问"能加一个管理 skill 功能吗?实现删除、加、启动、关闭等操作"。查证结论:**官方没有任何技能管理界面**(把 asar 里 289 个官方包全扫了一遍,没有禁用/卸载技能相关代码;评论里说的"专门的插件"指的是 `find-skills` 那种**搜索+安装**的 skill,只解决"加"),所以这块是真空白。**交互方式(国豪的主意,比我的第一版好)**:动作**不占行内 UI**,而是**右键技能行弹出一个小菜单**(置顶 / 关闭 / 定位 / 卸载),手机则**长按 500ms** 弹同一个菜单(滑动即取消,长列表滚动不会误触);行内只留一个 glyph(📌/📍,兼作"已置顶"状态指示)。**踩到的 UI 坑**:关闭监听最初挂在**捕获阶段**,会在菜单项自己的 `onClick` 之前就把菜单关掉 → 点了没反应;改成**冒泡阶段 + 菜单自身 `stopPropagation`**。**①「关闭」是真关闭,不是障眼法**:读了官方 `dsh-skill-filesystem` 的判定逻辑 —— **一个技能 = 目录下有 `SKILL.md`(大小写严格)**,或技能根下的顶层 `.md`。所以关闭就是把 `SKILL.md` 改名成 **`SKILL.md.disabled`**:provider 直接看不见它,**agent 也不再加载**,要回来只需改回名字。**②「卸载」从不删除**:移进 `$DSH_HOME/skill-backups/<时间戳>-<名字>/`(旁边写 `manifest.json` 记原路径),面板里出现「撤回」;`skill-ops.js` 里**一个破坏性文件调用都没有**,而且有一条**结构性回归测试**盯着这件事(注释里出现过破坏性调用的名字都会让它红 —— 这是它第一次触发时教我的)。跨盘无法 `rename` 时**直接报错**,绝不退化成"复制+删除"。**③ 三道安全闸**(这些路由**不需要鉴权**就能到达,所以不是可选项):请求必须带 `x-dsh-skill-picker: 1` 自定义头(跨站表单 POST 设不了自定义头,CORS 预检 host 也不应答);**客户端只报技能名,路径由 host 自己查**(⚡ 面板主数据来自官方 API,那套 DTO 不保证带 `path`;而且浏览器说了不算);路径必须**恰好落在已知技能根的一层之下**(`../` 逃逸、根目录本身、技能内的文件、任意绝对路径全部拒绝)。**④ 一个必须补的洞**:官方 `skills/list` **不会返回已关闭的技能** → 关掉之后就再也找不到、开不回来了;所以面板额外拉一次 host 扫描,单独渲染「⏻ 已关闭」分组专门用来开回来。**⑤ 真机端到端实测(用我自己建的假技能,全程只动它一个)**:关闭后磁盘上 `SKILL.md`→`SKILL.md.disabled` ✓、**扫描仍能列出它且 `disabled:true`** ✓(这是"能开回来"的前提)、开启还原 ✓、卸载后原位置消失而备份里 `SKILL.md`+`manifest` 完整 ✓、`backups` 能列出 ✓、`restore` 回原位 ✓、不带自定义头 → **403** ✓、`name` 塞 `../../../Windows/System32` → **404** ✓、`GET` → **405** ✓;**测试后你的技能数一个没变、备份目录零残留**。**回归测试**:新增 `test/skill-ops.test.mjs` 13 例(路径闸 9 种情形、关闭/开启往返、重复关闭拒绝、开启未关闭的拒绝、卸载后原位置消失但备份完整且 manifest 正确、撤回后再占用时拒绝覆盖、备份原路径被改成根外后不可撤回、备份 id 不允许带路径、**"模块里不存在破坏性调用"**、无 manifest 的杂目录被忽略、备份目录必须落在 `DSH_HOME` 下)。全量 `npm test`:**88 tests / 87 pass / 0 fail**
145
+ - **v0.5.22**:**「置顶 / 最近使用」改成两端(乃至手机)共用一份**——起因是国豪发现"桌面端和网页端的置顶不一样",并当场质疑"不就读同一个配置吗,要一个小时?"。**他是对的,而且根因就是"读的不是同一个配置"**:`localStorage` **按源(origin)隔离**,桌面端是 `dsh-app://app`、网页端是 `http://127.0.0.1:3080`、手机连上去又是第三个源 —— **同一份数据被存了三遍**,所以在桌面端钉的技能网页端永远看不到。**修法(约 40 行 host + 一个客户端模块,不是重写数据层)**:host 侧新增 `/dsh-skill-picker/state`(GET 取 / PUT 存),存到 **`$DSH_HOME/dsh-skill-picker-state.json`**(写用 temp+rename,原子);客户端新增 `src/client/shared-state.js`,把这份**母本镜像进 localStorage**,于是渲染期的**同步**读取一行都不用改(原来的数据路径是同步的,改成异步要动一大片 —— 这个顾虑没必要)。**细节**:① 每次 `savePinned`/`saveUsage`(含 `/` 菜单里的选中记录)都会**自动回推**;② 插件启动时 + **⚡ 面板每次打开时**都会拉取,所以另一端改了立刻能看见;③ **首次同步用并集、之后才跟随**(关键,实测中发现的坑):host 端 `?migrate=1` 会把收到的状态与已有状态**合并**再返回;客户端用一个 `dsh-skill-picker:shared-synced` 标记区分"这台还没同步过"。**没同步过**且本地有数据 → 走并集迁移(绝不覆盖);**同步过** → 直接采纳文件(所以"在 A 端取消置顶"能真的传播到 B 端)。**为什么必须有这一步**:如果首次也直接采纳,那么**哪台客户端先启动,文件就被它写成什么样,后面启动的那台就会丢掉自己独有的钉子** —— 桌面端 3 条、网页端 2 条的情况下,网页端先起就会把桌面端的 `ji-zhang` 吃掉。端到端实测时正是这条被暴露出来的;④ 文件不存在 / 为空而本地有数据时,**推送而不是覆盖为空**;⑤ 全程 **best-effort**:host 不可达时行为和以前**完全一样**(纯本地),绝不会因为同步失败把选择器弄坏。**回归测试**:新增 `test/shared-state.test.mjs` 7 例(拉取即镜像、缺文件时以本机播种并带 `migrate=1`、迁移返回的并集被本地采纳、远端为空时不把本机数据清成空、推送内容正确、host 失败时一律返回 false 且不抛、同步后会广播事件让界面重读)。全量 `npm test`:**72 tests / 71 pass / 0 fail**
144
146
  - **v0.5.21**:**文件补丁正式退休:默认不再改官方文件,并自动把改过的还原回去**——起因是国豪发现**同一件事桌面端对、网页端不对**,并直接怀疑到"之前魔改过"。**查证结果:他猜对了。** 两端加载的官方 `@deepseek-ai/dsh-client-ui-skill` 根本不是同一份:**桌面端从 `app.asar` 取**(打包只读,永远是原版),**网页端从 `~/.dsh/profiles/node_modules/` 取共享那份 —— 而那份被本插件的老文件补丁改过**(实测:`__dshSkillPickerFuzzy`×2、`order: -1`,旁边躺着 `.bak`)。**这个差异为什么会让排序看起来不一样**:补丁把官方 `candidates` 改成走 `window.__dshSkillPickerFuzzy`,而它返回的**已经是「置顶+使用热度」排好序**的列表;我的包装层再做一次排序时,`groupByPinned` 把「🗂️ 全部」分组的**输入顺序**保留下来(同热度时的稳定排序)——于是网页端的「全部」是**使用热度序**、桌面端是**字母序**。**根因级别的问题**:文件补丁自 v0.5.15 起**已是纯冗余** —— 它只做三件事(技能组置顶 `order: -1`、模糊+拼音匹配、`onPick` 记使用),而**运行时接管三件全都自己做了**。它却仍在改用户的官方包,还让两个 profile 对同一个文件产生分歧。**修法**:① 新增 `revertUiSkillPatches()`,启动时**只针对"仍带我们标记"的文件**从 `.bak` 还原(用 temp+rename,绝不动 pnpm 硬链接的共享 inode;幂等);② 文件补丁改为**默认关闭**,只有显式设 `DSH_SKILL_PICKER_FILE_PATCH=1` 才走旧的自愈路径(给"触发源注册表里没有 live sources 可接管"的老内核兜底)。**本机已实测还原**:共享那份 `lib/client.js` 现在 `__dshSkillPickerFuzzy`=0、`order: 2`、**hash 与 `.bak` 完全一致**。全量 `npm test`:**65 tests / 64 pass / 0 fail**
145
147
  - **v0.5.20**:**`/` 菜单的空查询也走排序,「置顶」技能排到最前面(与 ⚡ 面板一致)**——国豪指出:按 ⚡ 按钮时列表是「📌 置顶(3)」在前、再「📁 全部(57)」,但直接打 `/` 出来的却**是纯字母序、置顶不在前面**,两边排序不一样。**根因**:包装层对**空查询**做了短路 —— `if (query === '') return cached` / `if (query === '' || ...) return everything`,即"空查询把官方列表原封不动返回"。而 ⚡ 面板走的是 `rankPickerItems(items, '')`,它会先 `groupByPinned(...)` 把置顶提到最前。**于是同一个数据源、两套排序。** **修法**:去掉空查询短路,**空查询也交给 `rank()`** —— `rankPickerItems(items, '')` 本就设计为"空查询 = 全部条目、按面板顺序(置顶优先)",所以两边从此同源同序。**顺带发现一条假通过的测试**:旧的 `an empty query returns the official result untouched` 里 `rank` 是**故意抛错**的,而 `rank` 一抛错就被包装层的 try/catch 兜住、仍然返回官方列表 —— 也就是说那条测试**测的是兜底路径,不是它声称的语义**,所以它在新行为下照样"通过"。已改写为正面断言:空查询**必须**调用 `rank`,且置顶技能排第一。全量 `npm test`:**65 tests / 64 pass / 0 fail**
146
148
  - **v0.5.19**:**修掉 v0.5.17 里"预热走错了路"的 bug**(0.5.18 因 npm 发布延迟被误判为"卡住",实际已发布;两者代码逐字节相同)—— 上一版已经把胜负手找对了(技能组要 **0 个 `await`** 才抢得到高亮),但它预热时调的是**官方 `source.warm()`**:`warm()` 只预热**官方模块自己的**缓存,**插件自己的 `itemCache` 依然是空的** → 菜单打开那次**仍然未命中**、仍然 `await` 官方、仍然比命令组多一跳、**仍然输**。这一条是从运行诊断里确认的:`{"mode":"runtime","installed":true,"took":"candidates+order+onPick"}` 与 `{"servedFromCache":false,"sessionId":"session-de1f2494-…","primed":true,"count":71}` —— 即"接管装上了、缓存也被填过,但菜单那次仍是 `servedFromCache:false`"。**修法**:预热改为**走插件自己的包装路径**(一次空查询 `candidates`),**一次调用同时填满两层**(包装层写快照 + 官方层自然预热)。另:缓存**热重载后不再被清空**,所以重载后的第一次打开也是命中。**桌面端已实测确认修好**。**回归测试**:新增一条直击要害的用例 —— 官方取数故意慢 60ms,预热后菜单调用**必须在 30ms 内返回**;另把"预热走官方 `warm()`"这条反例锁死(断言**不会**只调 `warm()`)。全量 `npm test`:**65 tests / 64 pass / 0 fail**
package/lib/client.js CHANGED
@@ -25778,6 +25778,112 @@ function useWorkspaceCwd(props) {
25778
25778
  return typeof cwd === "string" ? cwd : "";
25779
25779
  }
25780
25780
 
25781
+ // src/client/shared-state.js
25782
+ var PINNED_KEY = "dsh-skill-picker:pinned";
25783
+ var USAGE_KEY = "dsh-skill-picker:usage";
25784
+ var SYNCED_KEY = "dsh-skill-picker:shared-synced";
25785
+ var ENDPOINT = "/dsh-skill-picker/state";
25786
+ var SHARED_STATE_EVENT = "dsh-skill-picker:shared-state";
25787
+ function readLocalPinned() {
25788
+ try {
25789
+ const parsed = JSON.parse(localStorage.getItem(PINNED_KEY) ?? "[]");
25790
+ return Array.isArray(parsed) ? parsed.filter((name) => typeof name === "string") : [];
25791
+ } catch {
25792
+ return [];
25793
+ }
25794
+ }
25795
+ function readLocalUsage() {
25796
+ try {
25797
+ const parsed = JSON.parse(localStorage.getItem(USAGE_KEY) ?? "{}");
25798
+ return parsed !== null && typeof parsed === "object" ? parsed : {};
25799
+ } catch {
25800
+ return {};
25801
+ }
25802
+ }
25803
+ function writeLocal(state) {
25804
+ try {
25805
+ localStorage.setItem(PINNED_KEY, JSON.stringify(state?.pinned ?? []));
25806
+ localStorage.setItem(USAGE_KEY, JSON.stringify(state?.usage ?? {}));
25807
+ } catch {
25808
+ }
25809
+ }
25810
+ function isEmptyState(state) {
25811
+ if (state === null || state === void 0 || typeof state !== "object") return true;
25812
+ const pinned = Array.isArray(state.pinned) ? state.pinned : [];
25813
+ const usage = state.usage !== null && typeof state.usage === "object" ? state.usage : {};
25814
+ return pinned.length === 0 && Object.keys(usage).length === 0;
25815
+ }
25816
+ function hasSynced() {
25817
+ try {
25818
+ return localStorage.getItem(SYNCED_KEY) === "1";
25819
+ } catch {
25820
+ return false;
25821
+ }
25822
+ }
25823
+ function markSynced() {
25824
+ try {
25825
+ localStorage.setItem(SYNCED_KEY, "1");
25826
+ } catch {
25827
+ }
25828
+ }
25829
+ async function pushSharedState({ migrate = false } = {}) {
25830
+ try {
25831
+ const response = await fetch(`${ENDPOINT}${migrate ? "?migrate=1" : ""}`, {
25832
+ method: "PUT",
25833
+ headers: { "content-type": "application/json" },
25834
+ body: JSON.stringify({ pinned: readLocalPinned(), usage: readLocalUsage() })
25835
+ });
25836
+ if (!response.ok) return false;
25837
+ const body = await response.json();
25838
+ if (body?.ok !== true) return false;
25839
+ if (migrate) writeLocal(body.state);
25840
+ return true;
25841
+ } catch {
25842
+ return false;
25843
+ }
25844
+ }
25845
+ async function pullSharedState() {
25846
+ try {
25847
+ const response = await fetch(ENDPOINT, { headers: { accept: "application/json" } });
25848
+ if (!response.ok) return false;
25849
+ const body = await response.json();
25850
+ if (body?.ok !== true) return false;
25851
+ const localHasData = !isEmptyState({ pinned: readLocalPinned(), usage: readLocalUsage() });
25852
+ if (body.state === null || body.state === void 0) {
25853
+ const pushed = await pushSharedState({ migrate: true });
25854
+ if (pushed) markSynced();
25855
+ return true;
25856
+ }
25857
+ if (!hasSynced() && localHasData) {
25858
+ const pushed = await pushSharedState({ migrate: true });
25859
+ if (pushed) {
25860
+ markSynced();
25861
+ return true;
25862
+ }
25863
+ }
25864
+ if (isEmptyState(body.state) && localHasData) {
25865
+ const pushed = await pushSharedState({ migrate: true });
25866
+ if (pushed) markSynced();
25867
+ return true;
25868
+ }
25869
+ writeLocal(body.state);
25870
+ markSynced();
25871
+ return true;
25872
+ } catch {
25873
+ return false;
25874
+ }
25875
+ }
25876
+ async function syncSharedState() {
25877
+ const pulled = await pullSharedState();
25878
+ if (pulled) {
25879
+ try {
25880
+ window.dispatchEvent(new Event(SHARED_STATE_EVENT));
25881
+ } catch {
25882
+ }
25883
+ }
25884
+ return pulled;
25885
+ }
25886
+
25781
25887
  // src/client/slash-source.js
25782
25888
  var WRAPPED = "__dshSkillPickerWrappedCandidates";
25783
25889
  var SKILL_SOURCE = { trigger: "/", name: "skill" };
@@ -26010,11 +26116,11 @@ function installSlashFuzzy(inputTriggers, hooks = {}, options = {}) {
26010
26116
  // src/client/index.jsx
26011
26117
  var import_jsx_runtime = require("react/jsx-runtime");
26012
26118
  var inject = ["slots", "connection", "sessions", "inputTriggers"];
26013
- var USAGE_KEY = "dsh-skill-picker:usage";
26014
- var PINNED_KEY = "dsh-skill-picker:pinned";
26119
+ var USAGE_KEY2 = "dsh-skill-picker:usage";
26120
+ var PINNED_KEY2 = "dsh-skill-picker:pinned";
26015
26121
  function loadPinned() {
26016
26122
  try {
26017
- const raw = localStorage.getItem(PINNED_KEY);
26123
+ const raw = localStorage.getItem(PINNED_KEY2);
26018
26124
  if (raw === null) return [];
26019
26125
  const parsed = JSON.parse(raw);
26020
26126
  return Array.isArray(parsed) ? parsed.filter((name) => typeof name === "string") : [];
@@ -26024,13 +26130,14 @@ function loadPinned() {
26024
26130
  }
26025
26131
  function savePinned(pinned) {
26026
26132
  try {
26027
- localStorage.setItem(PINNED_KEY, JSON.stringify(pinned));
26133
+ localStorage.setItem(PINNED_KEY2, JSON.stringify(pinned));
26028
26134
  } catch {
26029
26135
  }
26136
+ void pushSharedState();
26030
26137
  }
26031
26138
  function loadUsage() {
26032
26139
  try {
26033
- const raw = localStorage.getItem(USAGE_KEY);
26140
+ const raw = localStorage.getItem(USAGE_KEY2);
26034
26141
  if (raw === null) return {};
26035
26142
  const parsed = JSON.parse(raw);
26036
26143
  return parsed !== null && typeof parsed === "object" ? parsed : {};
@@ -26040,9 +26147,27 @@ function loadUsage() {
26040
26147
  }
26041
26148
  function saveUsage(usage) {
26042
26149
  try {
26043
- localStorage.setItem(USAGE_KEY, JSON.stringify(usage));
26150
+ localStorage.setItem(USAGE_KEY2, JSON.stringify(usage));
26044
26151
  } catch {
26045
26152
  }
26153
+ void pushSharedState();
26154
+ }
26155
+ async function skillOp(action, payload = {}) {
26156
+ try {
26157
+ const cwd = typeof payload.cwd === "string" && payload.cwd !== "" ? `?cwd=${encodeURIComponent(payload.cwd)}` : "";
26158
+ const response = await fetch(`/dsh-skill-picker/skill${cwd}`, {
26159
+ method: "POST",
26160
+ headers: { "content-type": "application/json", "x-dsh-skill-picker": "1" },
26161
+ body: JSON.stringify({ action, ...payload })
26162
+ });
26163
+ const body = await response.json().catch(() => void 0);
26164
+ if (body === void 0 || body.ok !== true) {
26165
+ return { ok: false, error: body?.error ?? `host returned ${response.status}` };
26166
+ }
26167
+ return body;
26168
+ } catch (cause) {
26169
+ return { ok: false, error: String(cause?.message ?? cause) };
26170
+ }
26046
26171
  }
26047
26172
  function isUserFacingSkill(skill) {
26048
26173
  const flag = skill?.userInvocable ?? skill?.invocation?.userInvocable;
@@ -26212,6 +26337,59 @@ var statusStyle = {
26212
26337
  color: "var(--dsw-alias-label-tertiary, #8a94a6)",
26213
26338
  fontSize: "13px"
26214
26339
  };
26340
+ var actionSpanStyle = {
26341
+ flex: "none",
26342
+ marginLeft: "6px",
26343
+ padding: "2px 4px",
26344
+ borderRadius: "6px",
26345
+ fontSize: "12px",
26346
+ lineHeight: "16px",
26347
+ cursor: "pointer",
26348
+ color: "var(--dsw-alias-label-tertiary, #8a94a6)",
26349
+ opacity: 0.55,
26350
+ userSelect: "none"
26351
+ };
26352
+ var noticeStyle = {
26353
+ display: "flex",
26354
+ alignItems: "center",
26355
+ gap: "8px",
26356
+ margin: "0 8px 8px",
26357
+ padding: "6px 10px",
26358
+ borderRadius: "8px",
26359
+ fontSize: "12px",
26360
+ lineHeight: "18px",
26361
+ background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))",
26362
+ color: "var(--dsw-alias-label-secondary, #b6bfcc)"
26363
+ };
26364
+ var menuStyle = {
26365
+ position: "fixed",
26366
+ zIndex: 2147483e3,
26367
+ minWidth: "196px",
26368
+ padding: "4px",
26369
+ borderRadius: "10px",
26370
+ border: "1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,0.24))",
26371
+ background: "var(--dsw-alias-bg-elevated, #23262e)",
26372
+ boxShadow: "0 8px 28px rgba(0,0,0,0.36)",
26373
+ color: "var(--dsw-alias-label-primary, #e6e9ef)",
26374
+ fontSize: "13px",
26375
+ userSelect: "none"
26376
+ };
26377
+ var menuHeaderStyle = {
26378
+ padding: "6px 10px 4px",
26379
+ fontSize: "11px",
26380
+ color: "var(--dsw-alias-label-tertiary, #8a94a6)",
26381
+ borderBottom: "1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,0.18))",
26382
+ marginBottom: "4px",
26383
+ overflow: "hidden",
26384
+ textOverflow: "ellipsis",
26385
+ whiteSpace: "nowrap"
26386
+ };
26387
+ var menuItemStyle = {
26388
+ padding: "6px 10px",
26389
+ borderRadius: "6px",
26390
+ cursor: "pointer",
26391
+ whiteSpace: "nowrap"
26392
+ };
26215
26393
  var sourceBadgeStyle = {
26216
26394
  display: "inline-flex",
26217
26395
  alignItems: "center",
@@ -26252,17 +26430,31 @@ function SkillPickerButton(props) {
26252
26430
  const [skills, setSkills] = (0, import_react.useState)(void 0);
26253
26431
  const [error, setError] = (0, import_react.useState)(void 0);
26254
26432
  const [source, setSource] = (0, import_react.useState)(void 0);
26433
+ const [disabledSkills, setDisabledSkills] = (0, import_react.useState)([]);
26434
+ const [busySkill, setBusySkill] = (0, import_react.useState)("");
26435
+ const [notice, setNotice] = (0, import_react.useState)(void 0);
26436
+ const [menu, setMenu] = (0, import_react.useState)(void 0);
26255
26437
  const [query, setQuery] = (0, import_react.useState)("");
26256
26438
  const [usage, setUsage] = (0, import_react.useState)(() => loadUsage());
26257
26439
  const [pinned, setPinned] = (0, import_react.useState)(() => loadPinned());
26258
26440
  const [active, setActive] = (0, import_react.useState)(0);
26259
26441
  const boxRef = (0, import_react.useRef)(null);
26260
26442
  const itemRefs = (0, import_react.useRef)([]);
26443
+ const longPressRef = (0, import_react.useRef)(0);
26261
26444
  (0, import_react.useEffect)(() => {
26262
26445
  const onUsageUpdated = () => setUsage(loadUsage());
26263
26446
  window.addEventListener("dsh-skill-picker:usage-updated", onUsageUpdated);
26264
26447
  return () => window.removeEventListener("dsh-skill-picker:usage-updated", onUsageUpdated);
26265
26448
  }, []);
26449
+ (0, import_react.useEffect)(() => {
26450
+ const onSharedState = () => {
26451
+ setUsage(loadUsage());
26452
+ setPinned(loadPinned());
26453
+ };
26454
+ window.addEventListener(SHARED_STATE_EVENT, onSharedState);
26455
+ void syncSharedState();
26456
+ return () => window.removeEventListener(SHARED_STATE_EVENT, onSharedState);
26457
+ }, []);
26266
26458
  const activeSessionId = sessionIdOf(props);
26267
26459
  (0, import_react.useEffect)(() => {
26268
26460
  if (activeSessionId !== void 0) warmSlashSkill(activeSessionId);
@@ -26277,8 +26469,8 @@ function SkillPickerButton(props) {
26277
26469
  } else if (props.input !== void 0 && typeof props.input.draft === "string") {
26278
26470
  draftRef.current = props.input.draft;
26279
26471
  }
26280
- const load = (0, import_react.useCallback)(async () => {
26281
- if (skills !== void 0 || error !== void 0) return;
26472
+ const load = (0, import_react.useCallback)(async (force = false) => {
26473
+ if (!force && (skills !== void 0 || error !== void 0)) return;
26282
26474
  const sessionId = sessionIdOf(props);
26283
26475
  try {
26284
26476
  if (typeof props.listSkills === "function" && sessionId !== void 0) {
@@ -26301,10 +26493,80 @@ function SkillPickerButton(props) {
26301
26493
  setError(String(cause?.message ?? cause));
26302
26494
  }
26303
26495
  }, [skills, error, props.listSkills, props.sessionId, props.session, props.cwd]);
26496
+ const loadDisabled = (0, import_react.useCallback)(async () => {
26497
+ try {
26498
+ const cwd = typeof props.cwd === "string" && props.cwd !== "" ? `?cwd=${encodeURIComponent(props.cwd)}` : "";
26499
+ const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: "application/json" } });
26500
+ const json = await res.json();
26501
+ if (!json.ok) return;
26502
+ setDisabledSkills((Array.isArray(json.skills) ? json.skills : []).filter((skill) => skill.disabled === true));
26503
+ } catch {
26504
+ }
26505
+ }, [props.cwd]);
26506
+ const runSkillOp = async (action, skill, extra = {}) => {
26507
+ const name = typeof skill?.name === "string" ? skill.name : String(skill ?? "");
26508
+ setBusySkill(name);
26509
+ setNotice(void 0);
26510
+ const result = await skillOp(action, { name, cwd: props.cwd, ...extra });
26511
+ setBusySkill("");
26512
+ if (result.ok !== true) {
26513
+ setNotice({ kind: "err", text: result.error ?? "\u64CD\u4F5C\u5931\u8D25" });
26514
+ return;
26515
+ }
26516
+ if (action === "uninstall") {
26517
+ const id = String(result.backupPath ?? "").split(/[\\/]/).filter(Boolean).pop();
26518
+ setNotice({ kind: "ok", text: `\u5DF2\u5378\u8F7D /${name} \u2014\u2014 \u53EA\u662F\u79FB\u8FDB\u5907\u4EFD\u76EE\u5F55\uFF0C\u6CA1\u5220`, undo: id });
26519
+ } else if (action === "disable") {
26520
+ setNotice({ kind: "ok", text: `\u5DF2\u5173\u95ED /${name} \u2014\u2014 agent \u4E5F\u4E0D\u4F1A\u518D\u52A0\u8F7D\u5B83\u4E86` });
26521
+ } else if (action === "enable") {
26522
+ setNotice({ kind: "ok", text: `\u5DF2\u5F00\u542F /${name}` });
26523
+ } else if (action === "reveal") {
26524
+ setNotice({ kind: "ok", text: `\u5DF2\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u91CC\u5B9A\u4F4D /${name}` });
26525
+ }
26526
+ await Promise.all([load(true), loadDisabled()]);
26527
+ };
26528
+ const undoUninstall = async (id) => {
26529
+ if (typeof id !== "string" || id === "") return;
26530
+ setBusySkill("(undo)");
26531
+ const result = await skillOp("restore", { id, cwd: props.cwd });
26532
+ setBusySkill("");
26533
+ if (result.ok !== true) {
26534
+ setNotice({ kind: "err", text: result.error ?? "\u64A4\u56DE\u5931\u8D25" });
26535
+ return;
26536
+ }
26537
+ setNotice({ kind: "ok", text: "\u5DF2\u64A4\u56DE\uFF0C\u6280\u80FD\u56DE\u5230\u539F\u4F4D\u7F6E" });
26538
+ await Promise.all([load(true), loadDisabled()]);
26539
+ };
26540
+ const openMenu = (event, skill, disabled) => {
26541
+ const width = 208;
26542
+ const height = disabled ? 84 : 168;
26543
+ const x = Math.max(8, Math.min(event.clientX, (window.innerWidth || 1024) - width - 8));
26544
+ const y = Math.max(8, Math.min(event.clientY, (window.innerHeight || 768) - height - 8));
26545
+ setMenu({ x, y, skill, disabled });
26546
+ };
26547
+ (0, import_react.useEffect)(() => {
26548
+ if (menu === void 0) return void 0;
26549
+ const close = () => setMenu(void 0);
26550
+ const onKey = (event) => {
26551
+ if (event.key === "Escape") close();
26552
+ };
26553
+ document.addEventListener("pointerdown", close);
26554
+ document.addEventListener("keydown", onKey);
26555
+ window.addEventListener("blur", close);
26556
+ window.addEventListener("resize", close);
26557
+ return () => {
26558
+ document.removeEventListener("pointerdown", close);
26559
+ document.removeEventListener("keydown", onKey);
26560
+ window.removeEventListener("blur", close);
26561
+ window.removeEventListener("resize", close);
26562
+ };
26563
+ }, [menu]);
26304
26564
  const toggle = () => {
26305
26565
  if (!open) {
26566
+ void syncSharedState();
26306
26567
  setUsage(loadUsage());
26307
26568
  void load();
26569
+ void loadDisabled();
26308
26570
  }
26309
26571
  setOpen(!open);
26310
26572
  };
@@ -26412,6 +26674,21 @@ function SkillPickerButton(props) {
26412
26674
  itemRefs.current[index] = el;
26413
26675
  },
26414
26676
  onClick: () => pick(skill.name),
26677
+ onContextMenu: (event) => {
26678
+ event.preventDefault();
26679
+ event.stopPropagation();
26680
+ openMenu(event, skill, false);
26681
+ },
26682
+ onTouchStart: (event) => {
26683
+ const touch = event.touches?.[0];
26684
+ if (touch === void 0) return;
26685
+ longPressRef.current = window.setTimeout(
26686
+ () => openMenu({ clientX: touch.clientX, clientY: touch.clientY }, skill, false),
26687
+ 500
26688
+ );
26689
+ },
26690
+ onTouchEnd: () => window.clearTimeout(longPressRef.current),
26691
+ onTouchMove: () => window.clearTimeout(longPressRef.current),
26415
26692
  onMouseEnter: (event) => {
26416
26693
  setActive(index);
26417
26694
  event.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))";
@@ -26490,6 +26767,87 @@ function SkillPickerButton(props) {
26490
26767
  ] }, group.title);
26491
26768
  });
26492
26769
  })() }),
26770
+ notice !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: noticeStyle, children: [
26771
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: "1", color: notice.kind === "err" ? "var(--dsw-alias-label-error, #ff7b72)" : void 0 }, children: notice.text }),
26772
+ typeof notice.undo === "string" && notice.undo !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26773
+ "span",
26774
+ {
26775
+ role: "button",
26776
+ tabIndex: -1,
26777
+ onClick: () => void undoUninstall(notice.undo),
26778
+ style: { cursor: "pointer", textDecoration: "underline", flex: "none" },
26779
+ children: "\u64A4\u56DE"
26780
+ }
26781
+ )
26782
+ ] }),
26783
+ disabledSkills.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "4px" }, children: [
26784
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26785
+ "div",
26786
+ {
26787
+ style: {
26788
+ display: "flex",
26789
+ alignItems: "center",
26790
+ justifyContent: "space-between",
26791
+ padding: "6px 10px 2px",
26792
+ color: "var(--dsw-alias-label-tertiary, #8a94a6)",
26793
+ fontSize: "11px",
26794
+ fontWeight: 600,
26795
+ letterSpacing: "0.04em"
26796
+ },
26797
+ children: [
26798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u23FB \u5DF2\u5173\u95ED" }),
26799
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.7 }, children: disabledSkills.length })
26800
+ ]
26801
+ }
26802
+ ),
26803
+ disabledSkills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26804
+ "div",
26805
+ {
26806
+ onContextMenu: (event) => {
26807
+ event.preventDefault();
26808
+ event.stopPropagation();
26809
+ openMenu(event, skill, true);
26810
+ },
26811
+ style: {
26812
+ ...itemStyle,
26813
+ flexDirection: "row",
26814
+ alignItems: "center",
26815
+ opacity: 0.55
26816
+ },
26817
+ children: [
26818
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "2px", flex: "1", minWidth: "0" }, children: [
26819
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: nameStyle, children: `/${skill.name}` }),
26820
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: descStyle, children: skill.description ?? "" })
26821
+ ] }),
26822
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26823
+ "span",
26824
+ {
26825
+ role: "button",
26826
+ tabIndex: -1,
26827
+ title: "\u91CD\u65B0\u5F00\u542F\uFF08\u628A SKILL.md.disabled \u6539\u56DE SKILL.md\uFF09",
26828
+ "aria-label": "\u91CD\u65B0\u5F00\u542F",
26829
+ onClick: () => void runSkillOp("enable", skill),
26830
+ style: actionSpanStyle,
26831
+ children: busySkill === skill.name ? "\u2026" : "\u23FB \u5F00\u542F"
26832
+ }
26833
+ ),
26834
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26835
+ "span",
26836
+ {
26837
+ role: "button",
26838
+ tabIndex: -1,
26839
+ title: "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u5B9A\u4F4D",
26840
+ "aria-label": "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u5B9A\u4F4D",
26841
+ onClick: () => void runSkillOp("reveal", skill),
26842
+ style: actionSpanStyle,
26843
+ children: "\u{1F4C2}"
26844
+ }
26845
+ )
26846
+ ]
26847
+ },
26848
+ `off-${skill.name}`
26849
+ ))
26850
+ ] }),
26493
26851
  source === "host" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: sourceBadgeStyle, title: "\u5B98\u65B9\u6280\u80FD API \u4E0D\u53EF\u7528\uFF0C\u5217\u8868\u6765\u81EA\u672C\u5730\u76EE\u5F55\u626B\u63CF\uFF08\u4E0E\u5B98\u65B9 / \u8865\u5168\u540C\u6E90\uFF09", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: sourceBadgeTextStyle, children: "\u672C\u5730\u626B\u63CF" }) }),
26494
26852
  slashEnhancementMode() === "runtime" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26495
26853
  "div",
@@ -26498,12 +26856,83 @@ function SkillPickerButton(props) {
26498
26856
  title: "`/` \u83DC\u5355\u7684\u6A21\u7CCA+\u62FC\u97F3\u641C\u7D22\u7531\u672C\u63D2\u4EF6\u5728\u8FD0\u884C\u65F6\u63A5\u7BA1\u5B98\u65B9\u6280\u80FD\u6E90\uFF08\u4E0D\u4F9D\u8D56\u6539\u5199\u4EFB\u4F55\u6587\u4EF6\uFF0Cissue #14\uFF09",
26499
26857
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: sourceBadgeTextStyle, children: "/ \u589E\u5F3A\uFF1A\u8FD0\u884C\u65F6\u63A5\u7BA1" })
26500
26858
  }
26859
+ ),
26860
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { margin: "0 10px 8px", fontSize: "11px", color: "var(--dsw-alias-label-tertiary, #8a94a6)" }, children: "\u53F3\u952E\u6280\u80FD = \u7F6E\u9876 / \u5173\u95ED / \u5B9A\u4F4D / \u5378\u8F7D\uFF08\u624B\u673A\u957F\u6309\uFF09" }),
26861
+ menu !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26862
+ "div",
26863
+ {
26864
+ style: { ...menuStyle, left: `${menu.x}px`, top: `${menu.y}px` },
26865
+ onPointerDown: (event) => event.stopPropagation(),
26866
+ children: [
26867
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: menuHeaderStyle, children: `/${menu.skill.name}` }),
26868
+ !menu.disabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26869
+ "div",
26870
+ {
26871
+ style: menuItemStyle,
26872
+ role: "button",
26873
+ tabIndex: -1,
26874
+ onClick: () => {
26875
+ setMenu(void 0);
26876
+ togglePin(menu.skill.name);
26877
+ },
26878
+ children: pinned.includes(menu.skill.name) ? "\u{1F4CC} \u53D6\u6D88\u7F6E\u9876" : "\u{1F4CC} \u7F6E\u9876\u5230\u9876\u90E8"
26879
+ }
26880
+ ),
26881
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26882
+ "div",
26883
+ {
26884
+ style: menuItemStyle,
26885
+ role: "button",
26886
+ tabIndex: -1,
26887
+ onClick: () => {
26888
+ const target = menu.skill;
26889
+ const turningOff = !menu.disabled;
26890
+ setMenu(void 0);
26891
+ void runSkillOp(turningOff ? "disable" : "enable", target);
26892
+ },
26893
+ children: menu.disabled ? "\u23FB \u5F00\u542F" : "\u23FB \u5173\u95ED\uFF08agent \u4E5F\u4E0D\u518D\u52A0\u8F7D\uFF09"
26894
+ }
26895
+ ),
26896
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26897
+ "div",
26898
+ {
26899
+ style: menuItemStyle,
26900
+ role: "button",
26901
+ tabIndex: -1,
26902
+ onClick: () => {
26903
+ const target = menu.skill;
26904
+ setMenu(void 0);
26905
+ void runSkillOp("reveal", target);
26906
+ },
26907
+ children: "\u{1F4C2} \u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u5B9A\u4F4D"
26908
+ }
26909
+ ),
26910
+ !menu.disabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26911
+ "div",
26912
+ {
26913
+ style: { ...menuItemStyle, color: "var(--dsw-alias-label-error, #ff7b72)" },
26914
+ role: "button",
26915
+ tabIndex: -1,
26916
+ onClick: () => {
26917
+ const target = menu.skill;
26918
+ setMenu(void 0);
26919
+ if (!window.confirm(`\u5378\u8F7D /${target.name}\uFF1F
26920
+
26921
+ \u5B83\u4F1A\u88AB\u79FB\u8FDB\u5907\u4EFD\u76EE\u5F55\uFF08\u4E0D\u662F\u5220\u9664\uFF09\uFF0C\u9762\u677F\u91CC\u53EF\u4EE5\u64A4\u56DE\u3002`)) return;
26922
+ void runSkillOp("uninstall", target);
26923
+ },
26924
+ children: "\u{1F5D1} \u5378\u8F7D\uFF08\u79FB\u5165\u5907\u4EFD\uFF0C\u53EF\u64A4\u56DE\uFF09"
26925
+ }
26926
+ )
26927
+ ]
26928
+ }
26501
26929
  )
26502
26930
  ] })
26503
26931
  ] })
26504
26932
  ] });
26505
26933
  }
26506
26934
  function apply(ctx) {
26935
+ void syncSharedState();
26507
26936
  const listSkills = async (sessionId) => {
26508
26937
  const remoteSkills = ctx.remote?.skills;
26509
26938
  const connectionSkills = ctx.connection?.api?.skills;