dsh-skill-picker 0.5.22 → 0.5.24

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.24**:**删掉面板底部那两个徽标(「本地扫描」/「/ 增强:运行时接管」)**——国豪的反馈:`确实有了,不过本地扫描和增强感觉没必要啊,感觉有点占地方,有点多余`。他说得对:**它们是"给排障的人看的",不是给用户看的**,却常驻占掉两行。**但没有直接丢掉** —— 「插件一切正常」和「插件压根没在跑」在界面上一模一样,这正是 **issue #14** 的教训,所以信息搬到了**不占地方的两处**:① ⚡ 按钮的 **tooltip**(鼠标悬停才显示,如 `选择技能(插入 /技能名 到发送框) · 列表来自本地扫描 · / 增强:运行时接管`);② **控制台一行**(面板每次打开打印一次:技能条数 + 列表来源 + `/` 增强是否真的生效)。顺带把已经没人用的 `sourceBadgeStyle` / `sourceBadgeTextStyle` 一并删除。全量 `npm test`:**88 tests / 87 pass / 0 fail**
145
+ - **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**
144
146
  - **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**
145
147
  - **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**
146
148
  - **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**
package/lib/client.js CHANGED
@@ -26152,6 +26152,23 @@ function saveUsage(usage) {
26152
26152
  }
26153
26153
  void pushSharedState();
26154
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
+ }
26171
+ }
26155
26172
  function isUserFacingSkill(skill) {
26156
26173
  const flag = skill?.userInvocable ?? skill?.invocation?.userInvocable;
26157
26174
  return flag !== false;
@@ -26320,22 +26337,58 @@ var statusStyle = {
26320
26337
  color: "var(--dsw-alias-label-tertiary, #8a94a6)",
26321
26338
  fontSize: "13px"
26322
26339
  };
26323
- var sourceBadgeStyle = {
26324
- display: "inline-flex",
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",
26325
26354
  alignItems: "center",
26326
- alignSelf: "flex-start",
26355
+ gap: "8px",
26327
26356
  margin: "0 8px 8px",
26328
- padding: "2px 8px",
26329
- border: "1px solid rgba(255, 193, 7, 0.35)",
26330
- borderRadius: "999px",
26331
- background: "rgba(255, 193, 7, 0.1)",
26332
- color: "#d9a520",
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",
26333
26379
  fontSize: "11px",
26334
- lineHeight: "16px",
26335
- flex: "none"
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"
26336
26386
  };
26337
- var sourceBadgeTextStyle = {
26338
- fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)"
26387
+ var menuItemStyle = {
26388
+ padding: "6px 10px",
26389
+ borderRadius: "6px",
26390
+ cursor: "pointer",
26391
+ whiteSpace: "nowrap"
26339
26392
  };
26340
26393
  function BoltIcon() {
26341
26394
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", style: { display: "block" }, children: [
@@ -26360,12 +26413,17 @@ function SkillPickerButton(props) {
26360
26413
  const [skills, setSkills] = (0, import_react.useState)(void 0);
26361
26414
  const [error, setError] = (0, import_react.useState)(void 0);
26362
26415
  const [source, setSource] = (0, import_react.useState)(void 0);
26416
+ const [disabledSkills, setDisabledSkills] = (0, import_react.useState)([]);
26417
+ const [busySkill, setBusySkill] = (0, import_react.useState)("");
26418
+ const [notice, setNotice] = (0, import_react.useState)(void 0);
26419
+ const [menu, setMenu] = (0, import_react.useState)(void 0);
26363
26420
  const [query, setQuery] = (0, import_react.useState)("");
26364
26421
  const [usage, setUsage] = (0, import_react.useState)(() => loadUsage());
26365
26422
  const [pinned, setPinned] = (0, import_react.useState)(() => loadPinned());
26366
26423
  const [active, setActive] = (0, import_react.useState)(0);
26367
26424
  const boxRef = (0, import_react.useRef)(null);
26368
26425
  const itemRefs = (0, import_react.useRef)([]);
26426
+ const longPressRef = (0, import_react.useRef)(0);
26369
26427
  (0, import_react.useEffect)(() => {
26370
26428
  const onUsageUpdated = () => setUsage(loadUsage());
26371
26429
  window.addEventListener("dsh-skill-picker:usage-updated", onUsageUpdated);
@@ -26394,8 +26452,8 @@ function SkillPickerButton(props) {
26394
26452
  } else if (props.input !== void 0 && typeof props.input.draft === "string") {
26395
26453
  draftRef.current = props.input.draft;
26396
26454
  }
26397
- const load = (0, import_react.useCallback)(async () => {
26398
- if (skills !== void 0 || error !== void 0) return;
26455
+ const load = (0, import_react.useCallback)(async (force = false) => {
26456
+ if (!force && (skills !== void 0 || error !== void 0)) return;
26399
26457
  const sessionId = sessionIdOf(props);
26400
26458
  try {
26401
26459
  if (typeof props.listSkills === "function" && sessionId !== void 0) {
@@ -26418,11 +26476,86 @@ function SkillPickerButton(props) {
26418
26476
  setError(String(cause?.message ?? cause));
26419
26477
  }
26420
26478
  }, [skills, error, props.listSkills, props.sessionId, props.session, props.cwd]);
26479
+ const loadDisabled = (0, import_react.useCallback)(async () => {
26480
+ try {
26481
+ const cwd = typeof props.cwd === "string" && props.cwd !== "" ? `?cwd=${encodeURIComponent(props.cwd)}` : "";
26482
+ const res = await fetch(`/dsh-skill-picker/skills${cwd}`, { headers: { accept: "application/json" } });
26483
+ const json = await res.json();
26484
+ if (!json.ok) return;
26485
+ setDisabledSkills((Array.isArray(json.skills) ? json.skills : []).filter((skill) => skill.disabled === true));
26486
+ } catch {
26487
+ }
26488
+ }, [props.cwd]);
26489
+ const runSkillOp = async (action, skill, extra = {}) => {
26490
+ const name = typeof skill?.name === "string" ? skill.name : String(skill ?? "");
26491
+ setBusySkill(name);
26492
+ setNotice(void 0);
26493
+ const result = await skillOp(action, { name, cwd: props.cwd, ...extra });
26494
+ setBusySkill("");
26495
+ if (result.ok !== true) {
26496
+ setNotice({ kind: "err", text: result.error ?? "\u64CD\u4F5C\u5931\u8D25" });
26497
+ return;
26498
+ }
26499
+ if (action === "uninstall") {
26500
+ const id = String(result.backupPath ?? "").split(/[\\/]/).filter(Boolean).pop();
26501
+ setNotice({ kind: "ok", text: `\u5DF2\u5378\u8F7D /${name} \u2014\u2014 \u53EA\u662F\u79FB\u8FDB\u5907\u4EFD\u76EE\u5F55\uFF0C\u6CA1\u5220`, undo: id });
26502
+ } else if (action === "disable") {
26503
+ setNotice({ kind: "ok", text: `\u5DF2\u5173\u95ED /${name} \u2014\u2014 agent \u4E5F\u4E0D\u4F1A\u518D\u52A0\u8F7D\u5B83\u4E86` });
26504
+ } else if (action === "enable") {
26505
+ setNotice({ kind: "ok", text: `\u5DF2\u5F00\u542F /${name}` });
26506
+ } else if (action === "reveal") {
26507
+ setNotice({ kind: "ok", text: `\u5DF2\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u91CC\u5B9A\u4F4D /${name}` });
26508
+ }
26509
+ await Promise.all([load(true), loadDisabled()]);
26510
+ };
26511
+ const undoUninstall = async (id) => {
26512
+ if (typeof id !== "string" || id === "") return;
26513
+ setBusySkill("(undo)");
26514
+ const result = await skillOp("restore", { id, cwd: props.cwd });
26515
+ setBusySkill("");
26516
+ if (result.ok !== true) {
26517
+ setNotice({ kind: "err", text: result.error ?? "\u64A4\u56DE\u5931\u8D25" });
26518
+ return;
26519
+ }
26520
+ setNotice({ kind: "ok", text: "\u5DF2\u64A4\u56DE\uFF0C\u6280\u80FD\u56DE\u5230\u539F\u4F4D\u7F6E" });
26521
+ await Promise.all([load(true), loadDisabled()]);
26522
+ };
26523
+ const openMenu = (event, skill, disabled) => {
26524
+ const width = 208;
26525
+ const height = disabled ? 84 : 168;
26526
+ const x = Math.max(8, Math.min(event.clientX, (window.innerWidth || 1024) - width - 8));
26527
+ const y = Math.max(8, Math.min(event.clientY, (window.innerHeight || 768) - height - 8));
26528
+ setMenu({ x, y, skill, disabled });
26529
+ };
26530
+ (0, import_react.useEffect)(() => {
26531
+ if (menu === void 0) return void 0;
26532
+ const close = () => setMenu(void 0);
26533
+ const onKey = (event) => {
26534
+ if (event.key === "Escape") close();
26535
+ };
26536
+ document.addEventListener("pointerdown", close);
26537
+ document.addEventListener("keydown", onKey);
26538
+ window.addEventListener("blur", close);
26539
+ window.addEventListener("resize", close);
26540
+ return () => {
26541
+ document.removeEventListener("pointerdown", close);
26542
+ document.removeEventListener("keydown", onKey);
26543
+ window.removeEventListener("blur", close);
26544
+ window.removeEventListener("resize", close);
26545
+ };
26546
+ }, [menu]);
26547
+ (0, import_react.useEffect)(() => {
26548
+ if (skills === void 0) return;
26549
+ console.info(
26550
+ `[dsh-skill-picker] \u6280\u80FD\u5217\u8868 ${skills.length} \u6761\uFF0C\u6765\u6E90\uFF1A${source === "host" ? "\u672C\u5730\u626B\u63CF\uFF08\u5B98\u65B9 skills API \u4E0D\u53EF\u7528\uFF09" : "\u5B98\u65B9 API"}\uFF1B/ \u589E\u5F3A\uFF1A${slashEnhancementMode() === "runtime" ? "\u8FD0\u884C\u65F6\u63A5\u7BA1\uFF0C\u5DF2\u751F\u6548" : "\u672A\u751F\u6548\uFF08\u89C1 issue #14\uFF09"}`
26551
+ );
26552
+ }, [skills, source]);
26421
26553
  const toggle = () => {
26422
26554
  if (!open) {
26423
26555
  void syncSharedState();
26424
26556
  setUsage(loadUsage());
26425
26557
  void load();
26558
+ void loadDisabled();
26426
26559
  }
26427
26560
  setOpen(!open);
26428
26561
  };
@@ -26498,7 +26631,7 @@ function SkillPickerButton(props) {
26498
26631
  {
26499
26632
  type: "button",
26500
26633
  onClick: toggle,
26501
- title: "\u9009\u62E9\u6280\u80FD\uFF08\u63D2\u5165 /\u6280\u80FD\u540D \u5230\u53D1\u9001\u6846\uFF09",
26634
+ title: `\u9009\u62E9\u6280\u80FD\uFF08\u63D2\u5165 /\u6280\u80FD\u540D \u5230\u53D1\u9001\u6846\uFF09${source === "host" ? "\u3000\xB7\u3000\u5217\u8868\u6765\u81EA\u672C\u5730\u626B\u63CF" : ""}${slashEnhancementMode() === "runtime" ? "\u3000\xB7\u3000/ \u589E\u5F3A\uFF1A\u8FD0\u884C\u65F6\u63A5\u7BA1" : ""}`,
26502
26635
  "aria-label": "\u9009\u62E9\u6280\u80FD",
26503
26636
  style: {
26504
26637
  ...buttonStyle,
@@ -26530,6 +26663,21 @@ function SkillPickerButton(props) {
26530
26663
  itemRefs.current[index] = el;
26531
26664
  },
26532
26665
  onClick: () => pick(skill.name),
26666
+ onContextMenu: (event) => {
26667
+ event.preventDefault();
26668
+ event.stopPropagation();
26669
+ openMenu(event, skill, false);
26670
+ },
26671
+ onTouchStart: (event) => {
26672
+ const touch = event.touches?.[0];
26673
+ if (touch === void 0) return;
26674
+ longPressRef.current = window.setTimeout(
26675
+ () => openMenu({ clientX: touch.clientX, clientY: touch.clientY }, skill, false),
26676
+ 500
26677
+ );
26678
+ },
26679
+ onTouchEnd: () => window.clearTimeout(longPressRef.current),
26680
+ onTouchMove: () => window.clearTimeout(longPressRef.current),
26533
26681
  onMouseEnter: (event) => {
26534
26682
  setActive(index);
26535
26683
  event.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.12))";
@@ -26608,13 +26756,155 @@ function SkillPickerButton(props) {
26608
26756
  ] }, group.title);
26609
26757
  });
26610
26758
  })() }),
26611
- 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" }) }),
26612
- slashEnhancementMode() === "runtime" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26759
+ notice !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: noticeStyle, children: [
26760
+ /* @__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 }),
26761
+ typeof notice.undo === "string" && notice.undo !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26762
+ "span",
26763
+ {
26764
+ role: "button",
26765
+ tabIndex: -1,
26766
+ onClick: () => void undoUninstall(notice.undo),
26767
+ style: { cursor: "pointer", textDecoration: "underline", flex: "none" },
26768
+ children: "\u64A4\u56DE"
26769
+ }
26770
+ )
26771
+ ] }),
26772
+ disabledSkills.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "4px" }, children: [
26773
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26774
+ "div",
26775
+ {
26776
+ style: {
26777
+ display: "flex",
26778
+ alignItems: "center",
26779
+ justifyContent: "space-between",
26780
+ padding: "6px 10px 2px",
26781
+ color: "var(--dsw-alias-label-tertiary, #8a94a6)",
26782
+ fontSize: "11px",
26783
+ fontWeight: 600,
26784
+ letterSpacing: "0.04em"
26785
+ },
26786
+ children: [
26787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u23FB \u5DF2\u5173\u95ED" }),
26788
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.7 }, children: disabledSkills.length })
26789
+ ]
26790
+ }
26791
+ ),
26792
+ disabledSkills.map((skill) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26793
+ "div",
26794
+ {
26795
+ onContextMenu: (event) => {
26796
+ event.preventDefault();
26797
+ event.stopPropagation();
26798
+ openMenu(event, skill, true);
26799
+ },
26800
+ style: {
26801
+ ...itemStyle,
26802
+ flexDirection: "row",
26803
+ alignItems: "center",
26804
+ opacity: 0.55
26805
+ },
26806
+ children: [
26807
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "2px", flex: "1", minWidth: "0" }, children: [
26808
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: nameStyle, children: `/${skill.name}` }),
26809
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: descStyle, children: skill.description ?? "" })
26810
+ ] }),
26811
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26812
+ "span",
26813
+ {
26814
+ role: "button",
26815
+ tabIndex: -1,
26816
+ title: "\u91CD\u65B0\u5F00\u542F\uFF08\u628A SKILL.md.disabled \u6539\u56DE SKILL.md\uFF09",
26817
+ "aria-label": "\u91CD\u65B0\u5F00\u542F",
26818
+ onClick: () => void runSkillOp("enable", skill),
26819
+ style: actionSpanStyle,
26820
+ children: busySkill === skill.name ? "\u2026" : "\u23FB \u5F00\u542F"
26821
+ }
26822
+ ),
26823
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26824
+ "span",
26825
+ {
26826
+ role: "button",
26827
+ tabIndex: -1,
26828
+ title: "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u5B9A\u4F4D",
26829
+ "aria-label": "\u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u5B9A\u4F4D",
26830
+ onClick: () => void runSkillOp("reveal", skill),
26831
+ style: actionSpanStyle,
26832
+ children: "\u{1F4C2}"
26833
+ }
26834
+ )
26835
+ ]
26836
+ },
26837
+ `off-${skill.name}`
26838
+ ))
26839
+ ] }),
26840
+ /* @__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" }),
26841
+ menu !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
26613
26842
  "div",
26614
26843
  {
26615
- style: sourceBadgeStyle,
26616
- 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",
26617
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: sourceBadgeTextStyle, children: "/ \u589E\u5F3A\uFF1A\u8FD0\u884C\u65F6\u63A5\u7BA1" })
26844
+ style: { ...menuStyle, left: `${menu.x}px`, top: `${menu.y}px` },
26845
+ onPointerDown: (event) => event.stopPropagation(),
26846
+ children: [
26847
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: menuHeaderStyle, children: `/${menu.skill.name}` }),
26848
+ !menu.disabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26849
+ "div",
26850
+ {
26851
+ style: menuItemStyle,
26852
+ role: "button",
26853
+ tabIndex: -1,
26854
+ onClick: () => {
26855
+ setMenu(void 0);
26856
+ togglePin(menu.skill.name);
26857
+ },
26858
+ children: pinned.includes(menu.skill.name) ? "\u{1F4CC} \u53D6\u6D88\u7F6E\u9876" : "\u{1F4CC} \u7F6E\u9876\u5230\u9876\u90E8"
26859
+ }
26860
+ ),
26861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26862
+ "div",
26863
+ {
26864
+ style: menuItemStyle,
26865
+ role: "button",
26866
+ tabIndex: -1,
26867
+ onClick: () => {
26868
+ const target = menu.skill;
26869
+ const turningOff = !menu.disabled;
26870
+ setMenu(void 0);
26871
+ void runSkillOp(turningOff ? "disable" : "enable", target);
26872
+ },
26873
+ children: menu.disabled ? "\u23FB \u5F00\u542F" : "\u23FB \u5173\u95ED\uFF08agent \u4E5F\u4E0D\u518D\u52A0\u8F7D\uFF09"
26874
+ }
26875
+ ),
26876
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26877
+ "div",
26878
+ {
26879
+ style: menuItemStyle,
26880
+ role: "button",
26881
+ tabIndex: -1,
26882
+ onClick: () => {
26883
+ const target = menu.skill;
26884
+ setMenu(void 0);
26885
+ void runSkillOp("reveal", target);
26886
+ },
26887
+ children: "\u{1F4C2} \u5728\u6587\u4EF6\u7BA1\u7406\u5668\u4E2D\u5B9A\u4F4D"
26888
+ }
26889
+ ),
26890
+ !menu.disabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
26891
+ "div",
26892
+ {
26893
+ style: { ...menuItemStyle, color: "var(--dsw-alias-label-error, #ff7b72)" },
26894
+ role: "button",
26895
+ tabIndex: -1,
26896
+ onClick: () => {
26897
+ const target = menu.skill;
26898
+ setMenu(void 0);
26899
+ if (!window.confirm(`\u5378\u8F7D /${target.name}\uFF1F
26900
+
26901
+ \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;
26902
+ void runSkillOp("uninstall", target);
26903
+ },
26904
+ children: "\u{1F5D1} \u5378\u8F7D\uFF08\u79FB\u5165\u5907\u4EFD\uFF0C\u53EF\u64A4\u56DE\uFF09"
26905
+ }
26906
+ )
26907
+ ]
26618
26908
  }
26619
26909
  )
26620
26910
  ] })