@civilization/deepseek-harness-skill-mcp 0.1.4 → 0.1.6
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 +13 -4
- package/lib/client.js +314 -51
- package/package.json +1 -1
- package/src/client/controller.js +9 -0
- package/src/client/index.jsx +136 -16
- package/src/client/locales.js +10 -4
- package/src/client/styles.css +4 -1
- package/src/index.js +13 -0
- package/src/skills.js +34 -3
- package/src/store.js +36 -1
- package/src/web.js +24 -3
package/README.md
CHANGED
|
@@ -57,15 +57,17 @@ skills/
|
|
|
57
57
|
|
|
58
58
|
Skill 页面支持:
|
|
59
59
|
|
|
60
|
-
-
|
|
60
|
+
- 添加、编辑路径、启用、禁用和移除多个 Skill 来源;移除来源不会删除目录中的文件
|
|
61
61
|
- 用独立的来源、用户分组和状态条件筛选技能
|
|
62
62
|
- 为单个 Skill 设置用户分组;分组保存在对应 `SKILL.md` 的 `metadata` 中
|
|
63
63
|
- 搜索、数量统计、frontmatter 格式诊断和同名覆盖提示
|
|
64
64
|
- 查看 `scripts`、`references`、`assets` 等资源文件
|
|
65
|
+
- 编辑 Skill 描述、适用场景、指令正文和调用权限
|
|
66
|
+
- 删除单个 Skill;目录型 Skill 会连同其资源目录一起删除,并在界面中二次确认
|
|
65
67
|
- 控制模型自动选择和用户 `/name` 调用
|
|
66
68
|
- 创建只有 `SKILL.md` 的最小目录,或包含标准资源目录的 Skill
|
|
67
69
|
|
|
68
|
-
新路径默认禁用。启用后,Harness 的 Skill provider 扫描该路径中的技能。页面在前台时每 3
|
|
70
|
+
新路径默认禁用。启用后,Harness 的 Skill provider 扫描该路径中的技能。页面在前台时每 3 秒刷新一次。当前 Harness Web 会按会话缓存 `/` Skill 候选,但未把 Host 的 `skills/change` 转发给浏览器;本插件在管理操作成功或用户点击“刷新”后触发客户端目录缓存失效,使下一次打开 `/` 菜单重新读取 Skill 列表。直接在文件系统外部修改 Skill 时,在管理页点击一次“刷新”即可同步菜单。
|
|
69
71
|
|
|
70
72
|
## MCP 管理
|
|
71
73
|
|
|
@@ -94,7 +96,9 @@ HTTP 示例:
|
|
|
94
96
|
}
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
服务器卡片使用状态灯显示禁用、等待发现工具或工具可用,并可展开查看实际注册的工具列表。MCP 配置可以编辑或删除,编辑时保留服务器 id 与启用状态。新服务器默认禁用;启用 stdio 服务器会在 Agent 沙箱之外执行所配置的程序。
|
|
100
|
+
|
|
101
|
+
MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此不会出现在 Skill 斜杠菜单中;它们的动态状态以 MCP 页面中的“工具列表”和 `extensions_inspect` 为准。
|
|
98
102
|
|
|
99
103
|
当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
|
|
100
104
|
|
|
@@ -108,6 +112,9 @@ MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH
|
|
|
108
112
|
| `extensions_add_skill` | 添加 Skill 路径;Agent 调用时显式指定内部 id |
|
|
109
113
|
| `extensions_add_mcp` | 添加 stdio 或 HTTP MCP 配置 |
|
|
110
114
|
| `extensions_set_enabled` | 启用或禁用受管配置 |
|
|
115
|
+
| `extensions_update_skill` | 修改 Skill 来源路径 |
|
|
116
|
+
| `extensions_update_mcp` | 替换 MCP 连接配置并保留 id 与启用状态 |
|
|
117
|
+
| `extensions_remove` | 移除 Skill 来源或 MCP 配置;不会删除 Skill 来源目录 |
|
|
111
118
|
| `extensions_inspect` | 查看当前 Agent 实际可见的 Skill 与 MCP 工具 |
|
|
112
119
|
|
|
113
120
|
可以直接告诉 Agent:
|
|
@@ -136,7 +143,9 @@ pnpm run web
|
|
|
136
143
|
|
|
137
144
|
## 验证
|
|
138
145
|
|
|
139
|
-
自动测试覆盖配置持久化、Skill
|
|
146
|
+
自动测试覆盖配置持久化、Skill 多路径、编辑和删除、单技能内容编辑与删除、用户分组、并发 revision、Skill 创建与调用权限、MCP 工具发现和卸载、轮询状态以及中英文词典一致性。测试不需要模型 API key。
|
|
147
|
+
|
|
148
|
+
浏览器端 `/` 候选缓存失效兼容逻辑会使用公开的 `connection/reset` 客户端事件;自动测试验证 Host/RPC 和浏览器 bundle 构建,但没有在本仓库内启动完整 Harness Web 做端到端菜单点击验证。外部文件改动仍依赖 filesystem provider 先完成 watcher 失效,必要时可在变更后点击管理页“刷新”。
|
|
140
149
|
|
|
141
150
|
## 发布
|
|
142
151
|
|
package/lib/client.js
CHANGED
|
@@ -30,6 +30,17 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
30
|
var import_react = require("react");
|
|
31
31
|
|
|
32
32
|
// src/client/controller.js
|
|
33
|
+
var skillCatalogMutations = /* @__PURE__ */ new Set([
|
|
34
|
+
"add-skill",
|
|
35
|
+
"update-skill-source",
|
|
36
|
+
"create-skill",
|
|
37
|
+
"update-skill",
|
|
38
|
+
"delete-skill",
|
|
39
|
+
"set-skill-invocation"
|
|
40
|
+
]);
|
|
41
|
+
function refreshesSlashCatalog(endpoint, options = {}) {
|
|
42
|
+
return options.refreshSlashCatalog === true || skillCatalogMutations.has(endpoint);
|
|
43
|
+
}
|
|
33
44
|
var ExtensionsController = class {
|
|
34
45
|
state = { extensions: [], sources: [], skills: [], revision: "", loading: false, saving: false, error: "", saved: false };
|
|
35
46
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -106,9 +117,20 @@ var zh = {
|
|
|
106
117
|
url: "\u670D\u52A1\u5668\u5730\u5740",
|
|
107
118
|
cancel: "\u53D6\u6D88",
|
|
108
119
|
save: "\u6DFB\u52A0",
|
|
120
|
+
saveChanges: "\u4FDD\u5B58\u4FEE\u6539",
|
|
109
121
|
create: "\u521B\u5EFA",
|
|
122
|
+
edit: "\u7F16\u8F91",
|
|
123
|
+
delete: "\u5220\u9664",
|
|
124
|
+
confirmDelete: "\u786E\u8BA4\u5220\u9664",
|
|
125
|
+
deleting: "\u5220\u9664\u4E2D\u2026",
|
|
110
126
|
saving: "\u4FDD\u5B58\u4E2D\u2026",
|
|
111
127
|
loading: "\u52A0\u8F7D\u4E2D\u2026",
|
|
128
|
+
deleteExtensionTitle: "\u79FB\u9664 Skill \u6765\u6E90",
|
|
129
|
+
deleteMcpTitle: "\u5220\u9664 MCP \u670D\u52A1\u5668",
|
|
130
|
+
deleteSkillTitle: "\u6C38\u4E45\u5220\u9664 Skill",
|
|
131
|
+
deleteExtensionConfirm: "\u5C06\u4ECE\u5F53\u524D\u914D\u7F6E\u4E2D\u79FB\u9664\u201C{name}\u201D\uFF0C\u76EE\u5F55\u4E2D\u7684 Skill \u6587\u4EF6\u4F1A\u4FDD\u7559\u3002",
|
|
132
|
+
deleteMcpConfirm: "\u5C06\u5220\u9664 MCP \u670D\u52A1\u5668\u201C{name}\u201D\u53CA\u5176\u8FDE\u63A5\u914D\u7F6E\u3002",
|
|
133
|
+
deleteSkillConfirm: "\u5C06\u6C38\u4E45\u5220\u9664\u201C{name}\u201D\u53CA\u5176\u76EE\u5F55\u5185\u7684\u8D44\u6E90\u6587\u4EF6\u3002\u6B64\u64CD\u4F5C\u65E0\u6CD5\u64A4\u9500\u3002",
|
|
112
134
|
enabled: "\u914D\u7F6E\u5DF2\u542F\u7528",
|
|
113
135
|
disabled: "\u5DF2\u7981\u7528",
|
|
114
136
|
enable: "\u542F\u7528",
|
|
@@ -166,6 +188,7 @@ var zh = {
|
|
|
166
188
|
source: "\u4FDD\u5B58\u6765\u6E90",
|
|
167
189
|
skillName: "Skill \u540D\u79F0",
|
|
168
190
|
description: "\u63CF\u8FF0",
|
|
191
|
+
instructions: "Skill \u6307\u4EE4\u6B63\u6587",
|
|
169
192
|
structure: "\u76EE\u5F55\u7ED3\u6784",
|
|
170
193
|
structureMinimal: "\u4EC5 SKILL.md",
|
|
171
194
|
structureStandard: "\u6807\u51C6\u76EE\u5F55\uFF08scripts / references / assets\uFF09",
|
|
@@ -204,9 +227,20 @@ var en = {
|
|
|
204
227
|
url: "Server URL",
|
|
205
228
|
cancel: "Cancel",
|
|
206
229
|
save: "Add",
|
|
230
|
+
saveChanges: "Save changes",
|
|
207
231
|
create: "Create",
|
|
232
|
+
edit: "Edit",
|
|
233
|
+
delete: "Delete",
|
|
234
|
+
confirmDelete: "Delete",
|
|
235
|
+
deleting: "Deleting\u2026",
|
|
208
236
|
saving: "Saving\u2026",
|
|
209
237
|
loading: "Loading\u2026",
|
|
238
|
+
deleteExtensionTitle: "Remove Skill source",
|
|
239
|
+
deleteMcpTitle: "Delete MCP server",
|
|
240
|
+
deleteSkillTitle: "Permanently delete Skill",
|
|
241
|
+
deleteExtensionConfirm: "Remove \u201C{name}\u201D from this configuration. Skill files in the directory will be kept.",
|
|
242
|
+
deleteMcpConfirm: "Delete MCP server \u201C{name}\u201D and its connection configuration.",
|
|
243
|
+
deleteSkillConfirm: "Permanently delete \u201C{name}\u201D and the resource files in its directory. This cannot be undone.",
|
|
210
244
|
enabled: "Configured enabled",
|
|
211
245
|
disabled: "Disabled",
|
|
212
246
|
enable: "Enable",
|
|
@@ -264,6 +298,7 @@ var en = {
|
|
|
264
298
|
source: "Source",
|
|
265
299
|
skillName: "Skill name",
|
|
266
300
|
description: "Description",
|
|
301
|
+
instructions: "Skill instructions",
|
|
267
302
|
structure: "Structure",
|
|
268
303
|
structureMinimal: "SKILL.md only",
|
|
269
304
|
structureStandard: "Standard folders (scripts / references / assets)",
|
|
@@ -278,7 +313,7 @@ var en = {
|
|
|
278
313
|
};
|
|
279
314
|
|
|
280
315
|
// src/client/styles.css
|
|
281
|
-
var styles_default = ".dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n";
|
|
316
|
+
var styles_default = ".dsh-ext{color:var(--dsw-alias-label-primary);max-width:840px;padding:4px 0 28px;font:inherit}\n.dsh-ext-heading,.dsh-ext-card>header{display:flex;justify-content:space-between;align-items:center;gap:16px}\n.dsh-ext h2{font-size:22px;letter-spacing:-.4px;margin:0 0 8px}.dsh-ext h3{font-size:15px;margin:0;display:flex;align-items:center;gap:8px}\n.dsh-ext p,.dsh-ext small{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6}.dsh-ext-heading p{margin:0}\n.dsh-ext button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 13px;white-space:nowrap}\n.dsh-ext button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-ext button:disabled{opacity:.5;cursor:default}\n.dsh-ext :is(button,input,select,textarea):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}\n.dsh-ext-card{border:1px solid var(--dsw-alias-border-l4);border-radius:16px;margin-top:20px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-ext-card>header{padding:18px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext-count{font-size:11px;font-weight:400;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-empty{padding:32px 20px}.dsh-ext-empty strong{font-size:14px;font-weight:500}.dsh-ext-empty p{margin:6px 0 0}\n.dsh-ext ul{list-style:none;padding:0;margin:0}.dsh-ext li{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid var(--dsw-alias-border-l4)}.dsh-ext li:last-child{border:0}\n.dsh-ext-details{display:flex;flex-direction:column;gap:5px;min-width:0}.dsh-ext-details strong{font-size:14px}.dsh-ext-details code{font-size:12px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}\n.dsh-ext-runtime{display:flex;align-items:center;flex-wrap:wrap;gap:7px;font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-ext-status{width:7px;height:7px;border-radius:50%;background:#858585;box-shadow:0 0 0 3px color-mix(in srgb,#858585 16%,transparent)}\n.dsh-ext-status-available{background:#38a169;box-shadow:0 0 0 3px color-mix(in srgb,#38a169 18%,transparent)}.dsh-ext-status-waiting{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-ext-runtime details{width:100%;margin-top:2px}.dsh-ext-runtime summary{width:max-content;cursor:pointer;color:var(--dsw-alias-label-secondary);user-select:none}.dsh-ext-runtime summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-ext-runtime .dsh-ext-tools{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.dsh-ext-runtime .dsh-ext-tools li{display:block;border:0;padding:4px 8px;border-radius:6px;background:var(--dsw-alias-bg-layer-3)}\n.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}\n.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}\n.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}\n.dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}\n.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}\n.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}\n.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}\n.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}\n.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}\n.dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}\n.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}\n.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}\n.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}\n.dsh-skill-row details{margin-top:9px}.dsh-skill-row summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary)}.dsh-skill-detail{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}.dsh-skill-detail>p{grid-column:1/-1;margin:0}.dsh-skill-detail p strong,.dsh-skill-detail>div>strong{display:block;margin-bottom:5px;color:var(--dsw-alias-label-secondary);font-size:12px}\n.dsh-skill-invocation{display:flex;flex-direction:column;gap:7px}.dsh-skill-invocation label,.dsh-skill-create-flags label{display:flex;flex-direction:row;align-items:center;gap:5px}.dsh-ext input[type=checkbox]{width:auto;min-width:auto}.dsh-skill-detail ul,.dsh-skill-detail li{display:block!important;margin:0;padding:0!important;border:0!important}.dsh-skill-resources{display:flex!important;flex-wrap:wrap!important;gap:5px}.dsh-skill-resources li{padding:3px 6px!important;border-radius:5px!important;background:var(--dsw-alias-bg-layer-3)!important}\n.dsh-ext-span{grid-column:1/-1}.dsh-skill-create-flags{display:flex;flex-direction:column;gap:7px;font-size:13px}.dsh-skill-create-flags>span{margin-bottom:1px}.dsh-skill-source>.dsh-ext-notice{margin:12px 18px}.dsh-skill-source>.dsh-ext-empty{padding:18px}\n@media(max-width:600px){.dsh-ext fieldset,.dsh-skill-detail,.dsh-skill-filters{grid-template-columns:1fr}.dsh-ext-heading{align-items:flex-start;flex-direction:column}.dsh-ext-heading-actions{flex-wrap:wrap}.dsh-ext-card>header{padding:14px}.dsh-ext li{padding:14px}.dsh-skill-stats{grid-template-columns:repeat(2,1fr)}.dsh-skill-group-editor{align-items:stretch;flex-direction:column}}\n";
|
|
282
317
|
|
|
283
318
|
// src/client/index.jsx
|
|
284
319
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -287,8 +322,16 @@ var namespace = "settings.extension-manager";
|
|
|
287
322
|
var settingsPollIntervalMs = 3e3;
|
|
288
323
|
function apply(ctx) {
|
|
289
324
|
const controller = new ExtensionsController((endpoint, args, signal) => ctx.connection.rpc.call("/extensions", endpoint, args, signal));
|
|
325
|
+
let catalogTimer;
|
|
326
|
+
const refreshSlashCatalog = () => {
|
|
327
|
+
window.clearTimeout(catalogTimer);
|
|
328
|
+
catalogTimer = window.setTimeout(() => ctx.emit("connection/reset"), 350);
|
|
329
|
+
};
|
|
290
330
|
ctx.effect(() => ctx.locale.register(namespace, { zh, en }));
|
|
291
|
-
ctx.effect(() => () =>
|
|
331
|
+
ctx.effect(() => () => {
|
|
332
|
+
window.clearTimeout(catalogTimer);
|
|
333
|
+
controller.dispose();
|
|
334
|
+
});
|
|
292
335
|
ctx.effect(() => {
|
|
293
336
|
const style = document.createElement("style");
|
|
294
337
|
style.textContent = styles_default;
|
|
@@ -299,7 +342,12 @@ function apply(ctx) {
|
|
|
299
342
|
void controller.request("list");
|
|
300
343
|
});
|
|
301
344
|
const t = ctx.locale.bind(namespace);
|
|
302
|
-
const
|
|
345
|
+
const request = async (endpoint, args, options) => {
|
|
346
|
+
const ok = await controller.request(endpoint, args, options);
|
|
347
|
+
if (ok && refreshesSlashCatalog(endpoint, options)) refreshSlashCatalog();
|
|
348
|
+
return ok;
|
|
349
|
+
};
|
|
350
|
+
const face = { hooks: { manager: controller }, request };
|
|
303
351
|
const section = (id, kind) => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
304
352
|
name: "settings.section",
|
|
305
353
|
id,
|
|
@@ -343,7 +391,7 @@ function SkillSection({ t, useManager, request }) {
|
|
|
343
391
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-heading-actions", children: [
|
|
344
392
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm("source"), children: t("add") }),
|
|
345
393
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || sources.length === 0, onClick: () => setForm("skill"), children: t("newSkill") }),
|
|
346
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => request("list"), children: t("refresh") })
|
|
394
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => request("list", {}, { refreshSlashCatalog: true }), children: t("refresh") })
|
|
347
395
|
] })
|
|
348
396
|
] }),
|
|
349
397
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PageState, { state, t }),
|
|
@@ -420,42 +468,71 @@ function SkillSection({ t, useManager, request }) {
|
|
|
420
468
|
] });
|
|
421
469
|
}
|
|
422
470
|
function SkillSources({ sources, skills, t, busy, request }) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className:
|
|
432
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
433
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
434
|
-
source.id,
|
|
435
|
-
" ",
|
|
436
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
|
|
437
|
-
] }),
|
|
438
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: source.location }),
|
|
439
|
-
source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
|
|
440
|
-
] })
|
|
471
|
+
const [editing, setEditing] = (0, import_react.useState)("");
|
|
472
|
+
const [confirming, setConfirming] = (0, import_react.useState)("");
|
|
473
|
+
const target = sources.find((source) => source.id === confirming);
|
|
474
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
475
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("details", { className: "dsh-ext-card dsh-skill-source-manager", children: [
|
|
476
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("summary", { children: [
|
|
477
|
+
t("skillSources"),
|
|
478
|
+
" ",
|
|
479
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: sources.length })
|
|
441
480
|
] }),
|
|
442
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
443
|
-
"
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sources.map((source) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
|
|
482
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-source-title", children: [
|
|
483
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-ext-status ${source.enabled ? "dsh-ext-status-available" : ""}`, "aria-hidden": "true" }),
|
|
484
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
485
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
|
|
486
|
+
source.id,
|
|
487
|
+
" ",
|
|
488
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: skills.filter((skill) => skill.sourceId === source.id).length })
|
|
489
|
+
] }),
|
|
490
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: source.location }),
|
|
491
|
+
source.issue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { children: t(`issue_${source.issue}`) })
|
|
492
|
+
] })
|
|
493
|
+
] }),
|
|
494
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
|
|
495
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setEditing(editing === source.id ? "" : source.id), children: t("edit") }),
|
|
496
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
497
|
+
"button",
|
|
498
|
+
{
|
|
499
|
+
type: "button",
|
|
500
|
+
role: "switch",
|
|
501
|
+
"aria-checked": source.enabled,
|
|
502
|
+
disabled: busy,
|
|
503
|
+
onClick: () => request("enable", { id: source.id, enabled: !source.enabled }, { refreshSlashCatalog: true }),
|
|
504
|
+
children: t(source.enabled ? "disable" : "enable")
|
|
505
|
+
}
|
|
506
|
+
),
|
|
507
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: () => setConfirming(source.id), children: t("delete") })
|
|
508
|
+
] }),
|
|
509
|
+
editing === source.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSourceForm, { source, t, busy, onCancel: () => setEditing(""), onSave: async (args) => {
|
|
510
|
+
if (await request("update-skill-source", args)) setEditing("");
|
|
511
|
+
} })
|
|
512
|
+
] }, source.id)) })
|
|
513
|
+
] }),
|
|
514
|
+
target && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
515
|
+
ConfirmDialog,
|
|
516
|
+
{
|
|
517
|
+
title: t("deleteExtensionTitle"),
|
|
518
|
+
message: t("deleteExtensionConfirm").replace("{name}", target.id),
|
|
519
|
+
t,
|
|
520
|
+
busy,
|
|
521
|
+
onCancel: () => setConfirming(""),
|
|
522
|
+
onConfirm: async () => {
|
|
523
|
+
if (await request("delete-extension", { id: target.id }, { refreshSlashCatalog: true })) {
|
|
524
|
+
setEditing("");
|
|
525
|
+
setConfirming("");
|
|
526
|
+
}
|
|
451
527
|
}
|
|
452
|
-
|
|
453
|
-
|
|
528
|
+
}
|
|
529
|
+
)
|
|
454
530
|
] });
|
|
455
531
|
}
|
|
456
532
|
function McpSection({ t, useManager, request }) {
|
|
457
533
|
const state = useManager((value) => value);
|
|
458
|
-
const [form, setForm] = (0, import_react.useState)(
|
|
534
|
+
const [form, setForm] = (0, import_react.useState)("");
|
|
535
|
+
const [confirming, setConfirming] = (0, import_react.useState)("");
|
|
459
536
|
const busy = state.loading || state.saving;
|
|
460
537
|
const rows = state.extensions.filter((row) => row.kind === "mcp");
|
|
461
538
|
useLiveRefresh(request);
|
|
@@ -475,7 +552,7 @@ function McpSection({ t, useManager, request }) {
|
|
|
475
552
|
" ",
|
|
476
553
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-ext-count", children: rows.length })
|
|
477
554
|
] }),
|
|
478
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm(
|
|
555
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !state.revision, onClick: () => setForm("new"), children: t("addMcp") })
|
|
479
556
|
] }),
|
|
480
557
|
rows.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-empty", children: [
|
|
481
558
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("emptyMcp") }),
|
|
@@ -485,26 +562,49 @@ function McpSection({ t, useManager, request }) {
|
|
|
485
562
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-details", children: [
|
|
486
563
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: row.id }),
|
|
487
564
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: row.location }),
|
|
488
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpRuntime, { row, t })
|
|
565
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpRuntime, { row, t }),
|
|
566
|
+
form === row.id && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { row, t, busy, onCancel: () => setForm(""), onSave: async (args) => {
|
|
567
|
+
if (await request("update-mcp", args)) setForm("");
|
|
568
|
+
} })
|
|
489
569
|
] }),
|
|
490
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
491
|
-
"button",
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
570
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
|
|
571
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy, onClick: () => setForm(form === row.id ? "" : row.id), children: t("edit") }),
|
|
572
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
573
|
+
"button",
|
|
574
|
+
{
|
|
575
|
+
type: "button",
|
|
576
|
+
role: "switch",
|
|
577
|
+
"aria-checked": row.enabled,
|
|
578
|
+
"aria-label": `${t(row.enabled ? "disable" : "enable")} ${row.id}`,
|
|
579
|
+
disabled: busy,
|
|
580
|
+
onClick: () => request("enable", { id: row.id, enabled: !row.enabled }),
|
|
581
|
+
children: t(row.enabled ? "disable" : "enable")
|
|
582
|
+
}
|
|
583
|
+
),
|
|
584
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy, onClick: () => setConfirming(row.id), children: t("delete") })
|
|
585
|
+
] })
|
|
502
586
|
] }, row.id)) }),
|
|
503
|
-
form && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
504
|
-
if (await request("add-mcp", args)) setForm(
|
|
587
|
+
form === "new" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpForm, { t, busy, onCancel: () => setForm(""), onSave: async (args) => {
|
|
588
|
+
if (await request("add-mcp", args)) setForm("");
|
|
505
589
|
} })
|
|
506
590
|
] }),
|
|
507
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") })
|
|
591
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-footnote", children: t("mcpFootnote") }),
|
|
592
|
+
confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
593
|
+
ConfirmDialog,
|
|
594
|
+
{
|
|
595
|
+
title: t("deleteMcpTitle"),
|
|
596
|
+
message: t("deleteMcpConfirm").replace("{name}", confirming),
|
|
597
|
+
t,
|
|
598
|
+
busy,
|
|
599
|
+
onCancel: () => setConfirming(""),
|
|
600
|
+
onConfirm: async () => {
|
|
601
|
+
if (await request("delete-extension", { id: confirming })) {
|
|
602
|
+
setForm("");
|
|
603
|
+
setConfirming("");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
)
|
|
508
608
|
] });
|
|
509
609
|
}
|
|
510
610
|
function useLiveRefresh(request) {
|
|
@@ -537,6 +637,8 @@ function skillState(skill) {
|
|
|
537
637
|
return !skill.valid ? "invalid" : !skill.sourceEnabled || !skill.modelInvocable && !skill.userInvocable ? "disabled" : skill.shadowed ? "shadowed" : !skill.modelInvocable ? "manual" : "available";
|
|
538
638
|
}
|
|
539
639
|
function SkillRow({ skill, source, groups, t, busy, request }) {
|
|
640
|
+
const [editing, setEditing] = (0, import_react.useState)(false);
|
|
641
|
+
const [confirming, setConfirming] = (0, import_react.useState)(false);
|
|
540
642
|
const state = skillState(skill);
|
|
541
643
|
const invocation = skill.modelInvocable && skill.userInvocable ? "both" : skill.modelInvocable ? "modelOnly" : skill.userInvocable ? "manualOnly" : "none";
|
|
542
644
|
const statusLabel = !skill.valid ? "skillStatusInvalid" : !skill.sourceEnabled ? "skillStatusSourceDisabled" : skill.shadowed ? "skillStatusShadowed" : "";
|
|
@@ -591,8 +693,119 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
|
|
|
591
693
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
592
694
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("resources") }),
|
|
593
695
|
skill.resources.length ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "dsh-skill-resources", children: skill.resources.map((path) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: path }) }, path)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("noResources") })
|
|
696
|
+
] }),
|
|
697
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-row-actions", children: [
|
|
698
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: busy || !skill.valid, onClick: () => setEditing(!editing), children: t("edit") }),
|
|
699
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-ext-danger", type: "button", disabled: busy || !skill.valid, onClick: () => setConfirming(true), children: t("delete") })
|
|
594
700
|
] })
|
|
595
701
|
] })
|
|
702
|
+
] }),
|
|
703
|
+
editing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillEditForm, { skill, t, busy, onCancel: () => setEditing(false), onSave: async (args) => {
|
|
704
|
+
if (await request("update-skill", args)) setEditing(false);
|
|
705
|
+
} }),
|
|
706
|
+
confirming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
707
|
+
ConfirmDialog,
|
|
708
|
+
{
|
|
709
|
+
title: t("deleteSkillTitle"),
|
|
710
|
+
message: t("deleteSkillConfirm").replace("{name}", skill.name),
|
|
711
|
+
t,
|
|
712
|
+
busy,
|
|
713
|
+
onCancel: () => setConfirming(false),
|
|
714
|
+
onConfirm: async () => {
|
|
715
|
+
await request("delete-skill", {
|
|
716
|
+
sourceId: skill.sourceId,
|
|
717
|
+
relativePath: skill.relativePath,
|
|
718
|
+
skillRevision: skill.revision
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
] }) });
|
|
724
|
+
}
|
|
725
|
+
function ConfirmDialog({ title, message, t, busy, onConfirm, onCancel }) {
|
|
726
|
+
const dialog = (0, import_react.useRef)(null);
|
|
727
|
+
const cancel = (0, import_react.useRef)(null);
|
|
728
|
+
const titleId = (0, import_react.useId)();
|
|
729
|
+
const messageId = (0, import_react.useId)();
|
|
730
|
+
(0, import_react.useEffect)(() => {
|
|
731
|
+
dialog.current.showModal();
|
|
732
|
+
cancel.current.focus();
|
|
733
|
+
return () => {
|
|
734
|
+
if (dialog.current?.open) dialog.current.close();
|
|
735
|
+
};
|
|
736
|
+
}, []);
|
|
737
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
738
|
+
"dialog",
|
|
739
|
+
{
|
|
740
|
+
ref: dialog,
|
|
741
|
+
className: "dsh-confirm",
|
|
742
|
+
"aria-labelledby": titleId,
|
|
743
|
+
"aria-describedby": messageId,
|
|
744
|
+
onCancel: (event) => {
|
|
745
|
+
event.preventDefault();
|
|
746
|
+
if (!busy) onCancel();
|
|
747
|
+
},
|
|
748
|
+
onMouseDown: (event) => {
|
|
749
|
+
if (event.target === event.currentTarget && !busy) onCancel();
|
|
750
|
+
},
|
|
751
|
+
children: [
|
|
752
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-confirm-icon", "aria-hidden": "true", children: "!" }),
|
|
753
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-confirm-copy", children: [
|
|
754
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { id: titleId, children: title }),
|
|
755
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id: messageId, children: message })
|
|
756
|
+
] }),
|
|
757
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-confirm-actions", children: [
|
|
758
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { ref: cancel, type: "button", disabled: busy, onClick: onCancel, children: t("cancel") }),
|
|
759
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "dsh-confirm-delete", type: "button", disabled: busy, onClick: () => void onConfirm(), children: t(busy ? "deleting" : "confirmDelete") })
|
|
760
|
+
] })
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
|
|
766
|
+
const prefix = (0, import_react.useId)();
|
|
767
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-skill-edit-form", onSubmit: (event) => {
|
|
768
|
+
event.preventDefault();
|
|
769
|
+
const data = new FormData(event.currentTarget);
|
|
770
|
+
void onSave({
|
|
771
|
+
sourceId: skill.sourceId,
|
|
772
|
+
relativePath: skill.relativePath,
|
|
773
|
+
skillRevision: skill.revision,
|
|
774
|
+
description: data.get("description"),
|
|
775
|
+
whenToUse: data.get("whenToUse"),
|
|
776
|
+
content: data.get("content"),
|
|
777
|
+
modelInvocable: data.has("modelInvocable"),
|
|
778
|
+
userInvocable: data.has("userInvocable")
|
|
779
|
+
});
|
|
780
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
|
|
781
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-description`, children: [
|
|
782
|
+
t("description"),
|
|
783
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-description`, name: "description", rows: 3, required: true, defaultValue: skill.description })
|
|
784
|
+
] }),
|
|
785
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-whenToUse`, children: [
|
|
786
|
+
t("whenToUse"),
|
|
787
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-whenToUse`, name: "whenToUse", rows: 2, defaultValue: skill.whenToUse })
|
|
788
|
+
] }),
|
|
789
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-content`, children: [
|
|
790
|
+
t("instructions"),
|
|
791
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-content`, name: "content", rows: 10, defaultValue: skill.content })
|
|
792
|
+
] }),
|
|
793
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-skill-create-flags", children: [
|
|
794
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("invocation") }),
|
|
795
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
796
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: "modelInvocable", defaultChecked: skill.modelInvocable }),
|
|
797
|
+
" ",
|
|
798
|
+
t("modelInvocable")
|
|
799
|
+
] }),
|
|
800
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
801
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "checkbox", name: "userInvocable", defaultChecked: skill.userInvocable }),
|
|
802
|
+
" ",
|
|
803
|
+
t("userInvocable")
|
|
804
|
+
] })
|
|
805
|
+
] }),
|
|
806
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
|
|
807
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
|
|
808
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : "saveChanges") })
|
|
596
809
|
] })
|
|
597
810
|
] }) });
|
|
598
811
|
}
|
|
@@ -697,6 +910,56 @@ function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
|
|
|
697
910
|
] })
|
|
698
911
|
] }) });
|
|
699
912
|
}
|
|
913
|
+
function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
|
|
914
|
+
const prefix = (0, import_react.useId)();
|
|
915
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: "dsh-ext-inline-form", onSubmit: (event) => {
|
|
916
|
+
event.preventDefault();
|
|
917
|
+
void onSave({ id: source.id, directory: new FormData(event.currentTarget).get("directory") });
|
|
918
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
|
|
919
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "dsh-ext-span", htmlFor: `${prefix}-directory`, children: [
|
|
920
|
+
t("directory"),
|
|
921
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-directory`, name: "directory", required: true, autoComplete: "off", defaultValue: source.location })
|
|
922
|
+
] }),
|
|
923
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
|
|
924
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
|
|
925
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : "saveChanges") })
|
|
926
|
+
] })
|
|
927
|
+
] }) });
|
|
928
|
+
}
|
|
929
|
+
function McpForm({ row, t, busy, onSave, onCancel }) {
|
|
930
|
+
const prefix = (0, import_react.useId)();
|
|
931
|
+
const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "" };
|
|
932
|
+
const [transport, setTransport] = (0, import_react.useState)(initial.transport);
|
|
933
|
+
const field = (name, required = true, multiline = false, value = "") => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
|
|
934
|
+
t(name),
|
|
935
|
+
multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-${name}`, name, rows: 3, defaultValue: value }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-${name}`, name, required, autoComplete: "off", defaultValue: value, ...name === "id" ? { pattern: "[A-Za-z0-9_\\-]{1,32}", maxLength: 32 } : {} })
|
|
936
|
+
] });
|
|
937
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: row ? "dsh-ext-inline-form" : "dsh-ext-form", onSubmit: (event) => {
|
|
938
|
+
event.preventDefault();
|
|
939
|
+
const data = Object.fromEntries(new FormData(event.currentTarget));
|
|
940
|
+
const configuration = JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url });
|
|
941
|
+
void onSave({ id: row?.id ?? data.id, configuration });
|
|
942
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
|
|
943
|
+
!row && field("id"),
|
|
944
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-transport`, children: [
|
|
945
|
+
t("transport"),
|
|
946
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { id: `${prefix}-transport`, value: transport, onChange: (event) => setTransport(event.target.value), children: [
|
|
947
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: t("stdio") }),
|
|
948
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "streamable-http", children: t("http") })
|
|
949
|
+
] })
|
|
950
|
+
] }),
|
|
951
|
+
transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
952
|
+
field("command", true, false, initial.command),
|
|
953
|
+
field("args", false, true, (initial.args ?? []).join("\n")),
|
|
954
|
+
field("cwd", false, false, initial.cwd)
|
|
955
|
+
] }) : field("url", true, false, initial.url),
|
|
956
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") }),
|
|
957
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
|
|
958
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
|
|
959
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", children: t(busy ? "saving" : row ? "saveChanges" : "save") })
|
|
960
|
+
] })
|
|
961
|
+
] }) });
|
|
962
|
+
}
|
|
700
963
|
function AddForm({ kind, t, busy, onSave, onCancel }) {
|
|
701
964
|
const prefix = (0, import_react.useId)();
|
|
702
965
|
const [transport, setTransport] = (0, import_react.useState)("stdio");
|
package/package.json
CHANGED
package/src/client/controller.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
/** One observable per mounted plugin; drafts stay in the settings component. */
|
|
2
|
+
const skillCatalogMutations = new Set([
|
|
3
|
+
'add-skill', 'update-skill-source', 'create-skill', 'update-skill', 'delete-skill', 'set-skill-invocation',
|
|
4
|
+
])
|
|
5
|
+
|
|
6
|
+
/** Keep broad connection cache invalidation away from ordinary page reads and MCP-only writes. */
|
|
7
|
+
export function refreshesSlashCatalog(endpoint, options = {}) {
|
|
8
|
+
return options.refreshSlashCatalog === true || skillCatalogMutations.has(endpoint)
|
|
9
|
+
}
|
|
10
|
+
|
|
2
11
|
export class ExtensionsController {
|
|
3
12
|
state = { extensions: [], sources: [], skills: [], revision: '', loading: false, saving: false, error: '', saved: false }
|
|
4
13
|
listeners = new Set()
|
package/src/client/index.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Skill and MCP settings sections registered through the public Slots and Connection services. */
|
|
2
|
-
import { useEffect, useId, useState } from 'react'
|
|
3
|
-
import { ExtensionsController } from './controller.js'
|
|
2
|
+
import { useEffect, useId, useRef, useState } from 'react'
|
|
3
|
+
import { ExtensionsController, refreshesSlashCatalog } from './controller.js'
|
|
4
4
|
import { zh, en } from './locales.js'
|
|
5
5
|
import css from './styles.css'
|
|
6
6
|
|
|
@@ -10,8 +10,13 @@ const settingsPollIntervalMs = 3000
|
|
|
10
10
|
|
|
11
11
|
export function apply(ctx) {
|
|
12
12
|
const controller = new ExtensionsController((endpoint, args, signal) => ctx.connection.rpc.call('/extensions', endpoint, args, signal))
|
|
13
|
+
let catalogTimer
|
|
14
|
+
const refreshSlashCatalog = () => {
|
|
15
|
+
window.clearTimeout(catalogTimer)
|
|
16
|
+
catalogTimer = window.setTimeout(() => ctx.emit('connection/reset'), 350)
|
|
17
|
+
}
|
|
13
18
|
ctx.effect(() => ctx.locale.register(namespace, { zh, en }))
|
|
14
|
-
ctx.effect(() => () => controller.dispose())
|
|
19
|
+
ctx.effect(() => () => { window.clearTimeout(catalogTimer); controller.dispose() })
|
|
15
20
|
ctx.effect(() => {
|
|
16
21
|
const style = document.createElement('style')
|
|
17
22
|
style.textContent = css
|
|
@@ -20,7 +25,12 @@ export function apply(ctx) {
|
|
|
20
25
|
})
|
|
21
26
|
ctx.on('connection/reset', () => { void controller.request('list') })
|
|
22
27
|
const t = ctx.locale.bind(namespace)
|
|
23
|
-
const
|
|
28
|
+
const request = async (endpoint, args, options) => {
|
|
29
|
+
const ok = await controller.request(endpoint, args, options)
|
|
30
|
+
if (ok && refreshesSlashCatalog(endpoint, options)) refreshSlashCatalog()
|
|
31
|
+
return ok
|
|
32
|
+
}
|
|
33
|
+
const face = { hooks: { manager: controller }, request }
|
|
24
34
|
// The two sections share one controller and managed patch; each kind gets its own settings page.
|
|
25
35
|
const section = (id, kind) => ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
26
36
|
name: 'settings.section', id, order: kind === 'skill' ? 16 : 17,
|
|
@@ -60,7 +70,7 @@ export function SkillSection({ t, useManager, request }) {
|
|
|
60
70
|
<header className="dsh-ext-heading"><div><h2>{t('navSkill')}</h2><p>{t('skillIntro')}</p></div>
|
|
61
71
|
<div className="dsh-ext-heading-actions"><button type="button" disabled={busy || !state.revision} onClick={() => setForm('source')}>{t('add')}</button>
|
|
62
72
|
<button type="button" disabled={busy || sources.length === 0} onClick={() => setForm('skill')}>{t('newSkill')}</button>
|
|
63
|
-
<button type="button" disabled={busy} onClick={() => request('list')}>{t('refresh')}</button></div></header>
|
|
73
|
+
<button type="button" disabled={busy} onClick={() => request('list', {}, { refreshSlashCatalog: true })}>{t('refresh')}</button></div></header>
|
|
64
74
|
<PageState state={state} t={t} />
|
|
65
75
|
<div className="dsh-skill-stats">
|
|
66
76
|
{Object.entries(counts).map(([key, value]) => <div key={key}><strong>{value}</strong><span>{t(`stat_${key}`)}</span></div>)}
|
|
@@ -96,18 +106,30 @@ export function SkillSection({ t, useManager, request }) {
|
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
function SkillSources({ sources, skills, t, busy, request }) {
|
|
99
|
-
|
|
109
|
+
const [editing, setEditing] = useState('')
|
|
110
|
+
const [confirming, setConfirming] = useState('')
|
|
111
|
+
const target = sources.find(source => source.id === confirming)
|
|
112
|
+
return <><details className="dsh-ext-card dsh-skill-source-manager"><summary>{t('skillSources')} <span className="dsh-ext-count">{sources.length}</span></summary>
|
|
100
113
|
<ul>{sources.map(source => <li key={source.id}><div className="dsh-skill-source-title"><span className={`dsh-ext-status ${source.enabled ? 'dsh-ext-status-available' : ''}`} aria-hidden="true" />
|
|
101
114
|
<div><strong>{source.id} <span className="dsh-ext-count">{skills.filter(skill => skill.sourceId === source.id).length}</span></strong><code>{source.location}</code>
|
|
102
115
|
{source.issue && <small>{t(`issue_${source.issue}`)}</small>}</div></div>
|
|
103
|
-
<button type="button"
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
<div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setEditing(editing === source.id ? '' : source.id)}>{t('edit')}</button>
|
|
117
|
+
<button type="button" role="switch" aria-checked={source.enabled} disabled={busy}
|
|
118
|
+
onClick={() => request('enable', { id: source.id, enabled: !source.enabled }, { refreshSlashCatalog: true })}>{t(source.enabled ? 'disable' : 'enable')}</button>
|
|
119
|
+
<button className="dsh-ext-danger" type="button" disabled={busy} onClick={() => setConfirming(source.id)}>{t('delete')}</button></div>
|
|
120
|
+
{editing === source.id && <SkillSourceForm source={source} t={t} busy={busy} onCancel={() => setEditing('')} onSave={async args => {
|
|
121
|
+
if (await request('update-skill-source', args)) setEditing('')
|
|
122
|
+
}} />}</li>)}</ul>
|
|
123
|
+
</details>{target && <ConfirmDialog title={t('deleteExtensionTitle')} message={t('deleteExtensionConfirm').replace('{name}', target.id)}
|
|
124
|
+
t={t} busy={busy} onCancel={() => setConfirming('')} onConfirm={async () => {
|
|
125
|
+
if (await request('delete-extension', { id: target.id }, { refreshSlashCatalog: true })) { setEditing(''); setConfirming('') }
|
|
126
|
+
}} />}</>
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
export function McpSection({ t, useManager, request }) {
|
|
109
130
|
const state = useManager(value => value)
|
|
110
|
-
const [form, setForm] = useState(
|
|
131
|
+
const [form, setForm] = useState('')
|
|
132
|
+
const [confirming, setConfirming] = useState('')
|
|
111
133
|
const busy = state.loading || state.saving
|
|
112
134
|
const rows = state.extensions.filter(row => row.kind === 'mcp')
|
|
113
135
|
useLiveRefresh(request)
|
|
@@ -117,16 +139,25 @@ export function McpSection({ t, useManager, request }) {
|
|
|
117
139
|
<PageState state={state} t={t} />
|
|
118
140
|
<section className="dsh-ext-card">
|
|
119
141
|
<header><h3>{t('mcp')} <span className="dsh-ext-count">{rows.length}</span></h3>
|
|
120
|
-
<button type="button" disabled={busy || !state.revision} onClick={() => setForm(
|
|
142
|
+
<button type="button" disabled={busy || !state.revision} onClick={() => setForm('new')}>{t('addMcp')}</button></header>
|
|
121
143
|
{rows.length === 0 && <div className="dsh-ext-empty"><strong>{t('emptyMcp')}</strong><p>{t('emptyMcpHint')}</p></div>}
|
|
122
|
-
<ul>{rows.map(row => <li key={row.id}><div className="dsh-ext-details"><strong>{row.id}</strong><code>{row.location}</code><McpRuntime row={row} t={t}
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
<ul>{rows.map(row => <li key={row.id}><div className="dsh-ext-details"><strong>{row.id}</strong><code>{row.location}</code><McpRuntime row={row} t={t} />
|
|
145
|
+
{form === row.id && <McpForm row={row} t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
|
|
146
|
+
if (await request('update-mcp', args)) setForm('')
|
|
147
|
+
}} />}</div>
|
|
148
|
+
<div className="dsh-ext-row-actions"><button type="button" disabled={busy} onClick={() => setForm(form === row.id ? '' : row.id)}>{t('edit')}</button>
|
|
149
|
+
<button type="button" role="switch" aria-checked={row.enabled} aria-label={`${t(row.enabled ? 'disable' : 'enable')} ${row.id}`} disabled={busy}
|
|
150
|
+
onClick={() => request('enable', { id: row.id, enabled: !row.enabled })}>{t(row.enabled ? 'disable' : 'enable')}</button>
|
|
151
|
+
<button className="dsh-ext-danger" type="button" disabled={busy} onClick={() => setConfirming(row.id)}>{t('delete')}</button></div></li>)}</ul>
|
|
152
|
+
{form === 'new' && <McpForm t={t} busy={busy} onCancel={() => setForm('')} onSave={async args => {
|
|
153
|
+
if (await request('add-mcp', args)) setForm('')
|
|
127
154
|
}} />}
|
|
128
155
|
</section>
|
|
129
156
|
<p className="dsh-ext-footnote">{t('mcpFootnote')}</p>
|
|
157
|
+
{confirming && <ConfirmDialog title={t('deleteMcpTitle')} message={t('deleteMcpConfirm').replace('{name}', confirming)}
|
|
158
|
+
t={t} busy={busy} onCancel={() => setConfirming('')} onConfirm={async () => {
|
|
159
|
+
if (await request('delete-extension', { id: confirming })) { setForm(''); setConfirming('') }
|
|
160
|
+
}} />}
|
|
130
161
|
</section>
|
|
131
162
|
}
|
|
132
163
|
|
|
@@ -162,6 +193,8 @@ function skillState(skill) {
|
|
|
162
193
|
}
|
|
163
194
|
|
|
164
195
|
function SkillRow({ skill, source, groups, t, busy, request }) {
|
|
196
|
+
const [editing, setEditing] = useState(false)
|
|
197
|
+
const [confirming, setConfirming] = useState(false)
|
|
165
198
|
const state = skillState(skill)
|
|
166
199
|
const invocation = skill.modelInvocable && skill.userInvocable ? 'both'
|
|
167
200
|
: skill.modelInvocable ? 'modelOnly' : skill.userInvocable ? 'manualOnly' : 'none'
|
|
@@ -187,10 +220,63 @@ function SkillRow({ skill, source, groups, t, busy, request }) {
|
|
|
187
220
|
<div><strong>{t('resources')}</strong>{skill.resources.length
|
|
188
221
|
? <ul className="dsh-skill-resources">{skill.resources.map(path => <li key={path}><code>{path}</code></li>)}</ul>
|
|
189
222
|
: <p>{t('noResources')}</p>}</div>
|
|
223
|
+
<div className="dsh-ext-row-actions"><button type="button" disabled={busy || !skill.valid} onClick={() => setEditing(!editing)}>{t('edit')}</button>
|
|
224
|
+
<button className="dsh-ext-danger" type="button" disabled={busy || !skill.valid} onClick={() => setConfirming(true)}>{t('delete')}</button></div>
|
|
190
225
|
</div></details>
|
|
226
|
+
{editing && <SkillEditForm skill={skill} t={t} busy={busy} onCancel={() => setEditing(false)} onSave={async args => {
|
|
227
|
+
if (await request('update-skill', args)) setEditing(false)
|
|
228
|
+
}} />}
|
|
229
|
+
{confirming && <ConfirmDialog title={t('deleteSkillTitle')} message={t('deleteSkillConfirm').replace('{name}', skill.name)}
|
|
230
|
+
t={t} busy={busy} onCancel={() => setConfirming(false)} onConfirm={async () => {
|
|
231
|
+
await request('delete-skill', {
|
|
232
|
+
sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
|
|
233
|
+
})
|
|
234
|
+
}} />}
|
|
191
235
|
</div></li>
|
|
192
236
|
}
|
|
193
237
|
|
|
238
|
+
function ConfirmDialog({ title, message, t, busy, onConfirm, onCancel }) {
|
|
239
|
+
const dialog = useRef(null)
|
|
240
|
+
const cancel = useRef(null)
|
|
241
|
+
const titleId = useId()
|
|
242
|
+
const messageId = useId()
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
dialog.current.showModal()
|
|
245
|
+
cancel.current.focus()
|
|
246
|
+
return () => { if (dialog.current?.open) dialog.current.close() }
|
|
247
|
+
}, [])
|
|
248
|
+
return <dialog ref={dialog} className="dsh-confirm" aria-labelledby={titleId} aria-describedby={messageId}
|
|
249
|
+
onCancel={event => { event.preventDefault(); if (!busy) onCancel() }} onMouseDown={event => {
|
|
250
|
+
if (event.target === event.currentTarget && !busy) onCancel()
|
|
251
|
+
}}>
|
|
252
|
+
<div className="dsh-confirm-icon" aria-hidden="true">!</div>
|
|
253
|
+
<div className="dsh-confirm-copy"><h3 id={titleId}>{title}</h3><p id={messageId}>{message}</p></div>
|
|
254
|
+
<div className="dsh-confirm-actions"><button ref={cancel} type="button" disabled={busy} onClick={onCancel}>{t('cancel')}</button>
|
|
255
|
+
<button className="dsh-confirm-delete" type="button" disabled={busy} onClick={() => void onConfirm()}>{t(busy ? 'deleting' : 'confirmDelete')}</button></div>
|
|
256
|
+
</dialog>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function SkillEditForm({ skill, t, busy, onSave, onCancel }) {
|
|
260
|
+
const prefix = useId()
|
|
261
|
+
return <form className="dsh-skill-edit-form" onSubmit={event => {
|
|
262
|
+
event.preventDefault()
|
|
263
|
+
const data = new FormData(event.currentTarget)
|
|
264
|
+
void onSave({
|
|
265
|
+
sourceId: skill.sourceId, relativePath: skill.relativePath, skillRevision: skill.revision,
|
|
266
|
+
description: data.get('description'), whenToUse: data.get('whenToUse'), content: data.get('content'),
|
|
267
|
+
modelInvocable: data.has('modelInvocable'), userInvocable: data.has('userInvocable'),
|
|
268
|
+
})
|
|
269
|
+
}}><fieldset disabled={busy}>
|
|
270
|
+
<label className="dsh-ext-span" htmlFor={`${prefix}-description`}>{t('description')}<textarea id={`${prefix}-description`} name="description" rows={3} required defaultValue={skill.description} /></label>
|
|
271
|
+
<label className="dsh-ext-span" htmlFor={`${prefix}-whenToUse`}>{t('whenToUse')}<textarea id={`${prefix}-whenToUse`} name="whenToUse" rows={2} defaultValue={skill.whenToUse} /></label>
|
|
272
|
+
<label className="dsh-ext-span" htmlFor={`${prefix}-content`}>{t('instructions')}<textarea id={`${prefix}-content`} name="content" rows={10} defaultValue={skill.content} /></label>
|
|
273
|
+
<div className="dsh-skill-create-flags"><span>{t('invocation')}</span>
|
|
274
|
+
<label><input type="checkbox" name="modelInvocable" defaultChecked={skill.modelInvocable} /> {t('modelInvocable')}</label>
|
|
275
|
+
<label><input type="checkbox" name="userInvocable" defaultChecked={skill.userInvocable} /> {t('userInvocable')}</label></div>
|
|
276
|
+
<div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'saveChanges')}</button></div>
|
|
277
|
+
</fieldset></form>
|
|
278
|
+
}
|
|
279
|
+
|
|
194
280
|
function SkillGroupEditor({ skill, groups, t, busy, request }) {
|
|
195
281
|
const currentGroup = skill.group ?? ''
|
|
196
282
|
const [group, setGroup] = useState(currentGroup)
|
|
@@ -243,6 +329,40 @@ function NewSkillForm({ sources, t, busy, onSave, onCancel }) {
|
|
|
243
329
|
</fieldset></form>
|
|
244
330
|
}
|
|
245
331
|
|
|
332
|
+
function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
|
|
333
|
+
const prefix = useId()
|
|
334
|
+
return <form className="dsh-ext-inline-form" onSubmit={event => {
|
|
335
|
+
event.preventDefault()
|
|
336
|
+
void onSave({ id: source.id, directory: new FormData(event.currentTarget).get('directory') })
|
|
337
|
+
}}><fieldset disabled={busy}><label className="dsh-ext-span" htmlFor={`${prefix}-directory`}>{t('directory')}
|
|
338
|
+
<input id={`${prefix}-directory`} name="directory" required autoComplete="off" defaultValue={source.location} /></label>
|
|
339
|
+
<div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'saveChanges')}</button></div>
|
|
340
|
+
</fieldset></form>
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function McpForm({ row, t, busy, onSave, onCancel }) {
|
|
344
|
+
const prefix = useId()
|
|
345
|
+
const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '' }
|
|
346
|
+
const [transport, setTransport] = useState(initial.transport)
|
|
347
|
+
const field = (name, required = true, multiline = false, value = '') => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
|
|
348
|
+
? <textarea id={`${prefix}-${name}`} name={name} rows={3} defaultValue={value} />
|
|
349
|
+
: <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" defaultValue={value} {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
|
|
350
|
+
return <form className={row ? 'dsh-ext-inline-form' : 'dsh-ext-form'} onSubmit={event => {
|
|
351
|
+
event.preventDefault()
|
|
352
|
+
const data = Object.fromEntries(new FormData(event.currentTarget))
|
|
353
|
+
const configuration = JSON.stringify(transport === 'stdio'
|
|
354
|
+
? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
|
|
355
|
+
: { transport, url: data.url })
|
|
356
|
+
void onSave({ id: row?.id ?? data.id, configuration })
|
|
357
|
+
}}><fieldset disabled={busy}>{!row && field('id')}
|
|
358
|
+
<label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
|
|
359
|
+
<option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option></select></label>
|
|
360
|
+
{transport === 'stdio' ? <>{field('command', true, false, initial.command)}{field('args', false, true, (initial.args ?? []).join('\n'))}{field('cwd', false, false, initial.cwd)}</> : field('url', true, false, initial.url)}
|
|
361
|
+
<p>{t('secret')}</p>
|
|
362
|
+
<div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
|
|
363
|
+
</fieldset></form>
|
|
364
|
+
}
|
|
365
|
+
|
|
246
366
|
function AddForm({ kind, t, busy, onSave, onCancel }) {
|
|
247
367
|
const prefix = useId()
|
|
248
368
|
const [transport, setTransport] = useState('stdio')
|
package/src/client/locales.js
CHANGED
|
@@ -7,7 +7,10 @@ export const zh = {
|
|
|
7
7
|
emptyMcp: '还没有添加 MCP 服务器', emptyMcpHint: '通过本地命令或 HTTP 地址连接外部工具。',
|
|
8
8
|
id: '标识', directory: 'Skill 路径', transport: '配置类型', stdio: 'stdio', http: 'HTTP',
|
|
9
9
|
command: '命令', args: '参数(每行一个)', cwd: '工作目录(可选)', url: '服务器地址',
|
|
10
|
-
cancel: '取消', save: '添加', create: '创建', saving: '保存中…', loading: '加载中…',
|
|
10
|
+
cancel: '取消', save: '添加', saveChanges: '保存修改', create: '创建', edit: '编辑', delete: '删除', confirmDelete: '确认删除', deleting: '删除中…', saving: '保存中…', loading: '加载中…',
|
|
11
|
+
deleteExtensionTitle: '移除 Skill 来源', deleteMcpTitle: '删除 MCP 服务器', deleteSkillTitle: '永久删除 Skill',
|
|
12
|
+
deleteExtensionConfirm: '将从当前配置中移除“{name}”,目录中的 Skill 文件会保留。', deleteMcpConfirm: '将删除 MCP 服务器“{name}”及其连接配置。',
|
|
13
|
+
deleteSkillConfirm: '将永久删除“{name}”及其目录内的资源文件。此操作无法撤销。',
|
|
11
14
|
enabled: '配置已启用', disabled: '已禁用', enable: '启用', disable: '禁用',
|
|
12
15
|
saved: '更改已保存。', failed: '操作失败,请检查输入、连接或服务端日志后重试。',
|
|
13
16
|
conflict: '配置已被修改,请刷新后重试。未提交的表单内容已保留。',
|
|
@@ -27,7 +30,7 @@ export const zh = {
|
|
|
27
30
|
invocation: '调用权限', modelInvocable: '允许模型自动选择', userInvocable: '允许用户通过 /name 调用',
|
|
28
31
|
invocation_both: '自动 + 手动', invocation_modelOnly: '仅模型', invocation_manualOnly: '仅手动', invocation_none: '完全禁用',
|
|
29
32
|
format_bundle: '目录', format_flat: '单文件', skillStatusInvalid: '格式异常', skillStatusSourceDisabled: '来源已禁用', skillStatusShadowed: '被同名覆盖',
|
|
30
|
-
resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述',
|
|
33
|
+
resources: '资源文件', noResources: '没有附加资源', source: '保存来源', skillName: 'Skill 名称', description: '描述', instructions: 'Skill 指令正文',
|
|
31
34
|
structure: '目录结构', structureMinimal: '仅 SKILL.md', structureStandard: '标准目录(scripts / references / assets)',
|
|
32
35
|
'issue_missing-frontmatter': '缺少 YAML frontmatter', 'issue_invalid-yaml': 'YAML 格式错误',
|
|
33
36
|
'issue_invalid-frontmatter': 'frontmatter 必须是对象', 'issue_invalid-name': 'name 必须使用 kebab-case',
|
|
@@ -42,7 +45,10 @@ export const en = {
|
|
|
42
45
|
emptyMcp: 'No MCP servers yet', emptyMcpHint: 'Connect external tools with a local command or HTTP endpoint.',
|
|
43
46
|
id: 'Identifier', directory: 'Skill path', transport: 'Type', stdio: 'stdio', http: 'HTTP',
|
|
44
47
|
command: 'Command', args: 'Arguments (one per line)', cwd: 'Working directory (optional)', url: 'Server URL',
|
|
45
|
-
cancel: 'Cancel', save: 'Add', create: 'Create', saving: 'Saving…', loading: 'Loading…',
|
|
48
|
+
cancel: 'Cancel', save: 'Add', saveChanges: 'Save changes', create: 'Create', edit: 'Edit', delete: 'Delete', confirmDelete: 'Delete', deleting: 'Deleting…', saving: 'Saving…', loading: 'Loading…',
|
|
49
|
+
deleteExtensionTitle: 'Remove Skill source', deleteMcpTitle: 'Delete MCP server', deleteSkillTitle: 'Permanently delete Skill',
|
|
50
|
+
deleteExtensionConfirm: 'Remove “{name}” from this configuration. Skill files in the directory will be kept.', deleteMcpConfirm: 'Delete MCP server “{name}” and its connection configuration.',
|
|
51
|
+
deleteSkillConfirm: 'Permanently delete “{name}” and the resource files in its directory. This cannot be undone.',
|
|
46
52
|
enabled: 'Configured enabled', disabled: 'Disabled', enable: 'Enable', disable: 'Disable',
|
|
47
53
|
saved: 'Changes saved.', failed: 'Operation failed. Check input, connection or server logs and retry.',
|
|
48
54
|
conflict: 'Configuration changed. Refresh and retry; your draft has been retained.',
|
|
@@ -62,7 +68,7 @@ export const en = {
|
|
|
62
68
|
invocation: 'Invocation', modelInvocable: 'Allow model selection', userInvocable: 'Allow user /name invocation',
|
|
63
69
|
invocation_both: 'Model + user', invocation_modelOnly: 'Model only', invocation_manualOnly: 'Manual only', invocation_none: 'Fully disabled',
|
|
64
70
|
format_bundle: 'Bundle', format_flat: 'Flat file', skillStatusInvalid: 'Invalid', skillStatusSourceDisabled: 'Source disabled', skillStatusShadowed: 'Shadowed',
|
|
65
|
-
resources: 'Resource files', noResources: 'No additional resources', source: 'Source', skillName: 'Skill name', description: 'Description',
|
|
71
|
+
resources: 'Resource files', noResources: 'No additional resources', source: 'Source', skillName: 'Skill name', description: 'Description', instructions: 'Skill instructions',
|
|
66
72
|
structure: 'Structure', structureMinimal: 'SKILL.md only', structureStandard: 'Standard folders (scripts / references / assets)',
|
|
67
73
|
'issue_missing-frontmatter': 'Missing YAML frontmatter', 'issue_invalid-yaml': 'Invalid YAML',
|
|
68
74
|
'issue_invalid-frontmatter': 'Frontmatter must be an object', 'issue_invalid-name': 'name must use kebab-case',
|
package/src/client/styles.css
CHANGED
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
.dsh-ext-form{padding:20px;border-top:1px solid var(--dsw-alias-border-l4)}.dsh-ext fieldset{border:0;padding:0;margin:0;display:grid;grid-template-columns:1fr 1fr;gap:16px}.dsh-ext label{display:flex;flex-direction:column;gap:7px;font-size:13px}
|
|
19
19
|
.dsh-ext input,.dsh-ext select,.dsh-ext textarea{box-sizing:border-box;min-width:0;width:100%;font:inherit;color:inherit;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4);border-radius:8px;padding:10px}.dsh-ext textarea{resize:vertical}
|
|
20
20
|
.dsh-ext-actions{display:flex;justify-content:flex-end;gap:8px;grid-column:1/-1}.dsh-ext fieldset>p{grid-column:1/-1;margin:0}.dsh-ext-notice{padding:12px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:10px}.dsh-ext-footnote{padding:0 4px}
|
|
21
|
+
.dsh-ext-row-actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap;justify-content:flex-end}.dsh-ext .dsh-ext-danger{color:#e98282}.dsh-ext-inline-form{width:100%;padding:14px 0 2px;border:0}.dsh-skill-edit-form{margin-top:12px;padding:14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
22
|
+
.dsh-confirm{box-sizing:border-box;width:min(430px,calc(100vw - 32px));padding:22px;border:1px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 20px 60px rgba(0,0,0,.28);font:inherit}.dsh-confirm::backdrop{background:rgba(10,12,16,.48);backdrop-filter:blur(3px)}
|
|
23
|
+
.dsh-confirm-icon{display:grid;place-items:center;width:34px;height:34px;margin-bottom:16px;border-radius:10px;background:color-mix(in srgb,#e05252 14%,transparent);color:#e98282;font-size:19px;font-weight:700}.dsh-confirm-copy h3{margin:0;font-size:16px}.dsh-confirm-copy p{margin:8px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.65}.dsh-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.dsh-confirm button{font:inherit;font-size:13px;cursor:pointer;border:1px solid var(--dsw-alias-border-l4);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:8px 14px}.dsh-confirm button:hover{background:var(--dsw-alias-bg-layer-3)}.dsh-confirm button:disabled{opacity:.5;cursor:default}.dsh-confirm button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px}.dsh-confirm .dsh-confirm-delete{border-color:color-mix(in srgb,#e05252 45%,var(--dsw-alias-border-l4));background:#b83232;color:#fff}.dsh-confirm .dsh-confirm-delete:hover{background:#c63b3b}
|
|
21
24
|
.dsh-ext-heading-actions{display:flex;gap:8px;align-items:center}.dsh-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
22
25
|
.dsh-skill-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px}.dsh-skill-stats>div{display:flex;flex-direction:column;gap:4px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l4);border-radius:12px;background:var(--dsw-alias-bg-layer-2)}.dsh-skill-stats strong{font-size:20px}.dsh-skill-stats span{font-size:12px;color:var(--dsw-alias-label-tertiary)}
|
|
23
26
|
.dsh-skill-search{margin-top:14px}.dsh-skill-search input{padding-left:13px}.dsh-skill-manager>.dsh-ext-form{margin-top:14px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2)}
|
|
24
27
|
.dsh-skill-filters{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:10px}.dsh-skill-filters label{gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary)}.dsh-skill-filters select{padding:8px 10px;color:var(--dsw-alias-label-primary)}
|
|
25
28
|
.dsh-skill-source-manager{margin-top:14px}.dsh-skill-source-manager>summary{cursor:pointer;padding:14px 18px;font-size:14px;font-weight:600;user-select:none}.dsh-skill-source-manager[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l4)}
|
|
26
|
-
.dsh-skill-source-manager li{padding:11px 18px}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}
|
|
29
|
+
.dsh-skill-source-manager li{padding:11px 18px;flex-wrap:wrap}.dsh-skill-source-title{display:flex;align-items:flex-start;gap:11px;min-width:0}.dsh-skill-source-title>.dsh-ext-status{margin-top:6px;flex:0 0 auto}.dsh-skill-source-title>div{display:flex;min-width:0;flex-direction:column;gap:3px}.dsh-skill-source-title code{font-size:11px;color:var(--dsw-alias-label-tertiary);overflow-wrap:anywhere}.dsh-skill-source-title small{font-size:11px;color:#e98282}
|
|
27
30
|
.dsh-skill-library{margin-top:14px}.dsh-skill-group-editor{display:flex;align-items:flex-end;gap:8px}.dsh-skill-group-editor label{flex:1}.dsh-skill-group-editor input{padding:7px 9px}.dsh-skill-group-editor button{padding:7px 10px}
|
|
28
31
|
.dsh-skill-row{display:block!important;padding:15px 18px!important}.dsh-skill-row-main{min-width:0}.dsh-skill-name{display:flex;align-items:center;gap:9px}.dsh-skill-name strong{font-size:14px}.dsh-skill-row-main>p{margin:6px 0 3px}.dsh-skill-row-main>code{font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
29
32
|
.dsh-skill-badge{font-size:11px;padding:2px 7px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-secondary)}.dsh-skill-badge-group{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent)}.dsh-skill-badge-invalid{color:#e98282}.dsh-skill-badge-shadowed{color:#a98af8}.dsh-ext-status-invalid{background:#e05252;box-shadow:0 0 0 3px color-mix(in srgb,#e05252 18%,transparent)}.dsh-ext-status-shadowed{background:#8b5cf6;box-shadow:0 0 0 3px color-mix(in srgb,#8b5cf6 18%,transparent)}.dsh-ext-status-manual{background:#d69e2e;box-shadow:0 0 0 3px color-mix(in srgb,#d69e2e 18%,transparent)}
|
package/src/index.js
CHANGED
|
@@ -45,6 +45,19 @@ export async function apply(ctx, config) {
|
|
|
45
45
|
register('extensions_set_enabled', 'Enable or disable a managed extension. Enabling MCP authorizes its configured executable or endpoint to run when the profile reloads. Live profiles reload automatically; startup profiles require restart.', {
|
|
46
46
|
id: string, enabled: { type: 'boolean', required: true },
|
|
47
47
|
}, (args, exec) => store.setEnabled(args.id, args.enabled, exec.signal))
|
|
48
|
+
register('extensions_update_skill', 'Change the absolute directory of a managed Skill source while preserving its id and enabled state.', {
|
|
49
|
+
id: string, directory: string,
|
|
50
|
+
}, (args, exec) => store.updateSkillSource(args.id, args.directory, exec.signal))
|
|
51
|
+
register('extensions_update_mcp', 'Replace a managed MCP server configuration while preserving its id and enabled state. configuration is JSON; do not supply secrets.', {
|
|
52
|
+
id: string, configuration: string,
|
|
53
|
+
}, (args, exec) => {
|
|
54
|
+
const input = JSON.parse(args.configuration)
|
|
55
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('configuration must be a JSON object.')
|
|
56
|
+
return store.updateMcp(args.id, input, exec.signal)
|
|
57
|
+
})
|
|
58
|
+
register('extensions_remove', 'Remove one managed Skill source or MCP configuration. Removing a Skill source does not delete its files.', {
|
|
59
|
+
id: string,
|
|
60
|
+
}, (args, exec) => store.remove(args.id, exec.signal))
|
|
48
61
|
register('extensions_inspect', 'Inspect actual Skill catalog and visible MCP tool names for the caller. Zero tools does not establish connection failure. Does not start a separate connection probe.', {
|
|
49
62
|
cwd: string,
|
|
50
63
|
}, async (args, exec) => ({
|
package/src/skills.js
CHANGED
|
@@ -87,6 +87,7 @@ async function inspectSkill(source, path, format) {
|
|
|
87
87
|
modelInvocable: parsed.modelInvocable,
|
|
88
88
|
userInvocable: parsed.userInvocable,
|
|
89
89
|
group: parsed.group,
|
|
90
|
+
content: parsed.body,
|
|
90
91
|
valid: parsed.issues.length === 0,
|
|
91
92
|
issues: parsed.issues,
|
|
92
93
|
resources,
|
|
@@ -98,7 +99,7 @@ async function inspectSkill(source, path, format) {
|
|
|
98
99
|
return {
|
|
99
100
|
sourceId: source.id, sourceEnabled: source.enabled, relativePath, format,
|
|
100
101
|
name: basename(path, extname(path)), description: '', whenToUse: '',
|
|
101
|
-
modelInvocable: false, userInvocable: false, group: '', valid: false, issues: [issue], resources: [], revision: '',
|
|
102
|
+
modelInvocable: false, userInvocable: false, group: '', content: '', valid: false, issues: [issue], resources: [], revision: '',
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -166,7 +167,7 @@ export async function createSkill(rows, input) {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
async function updateSkillFrontmatter(rows, input, update) {
|
|
170
|
+
async function updateSkillFrontmatter(rows, input, update, body) {
|
|
170
171
|
const root = skillSource(rows, input.sourceId)
|
|
171
172
|
const inventory = await inspectSkills(rows)
|
|
172
173
|
const skill = inventory.skills.find(item => item.sourceId === input.sourceId && item.relativePath === input.relativePath)
|
|
@@ -180,7 +181,7 @@ async function updateSkillFrontmatter(rows, input, update) {
|
|
|
180
181
|
const parsed = parseFrontmatter(content)
|
|
181
182
|
update(parsed.data)
|
|
182
183
|
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`)
|
|
183
|
-
await writeFile(temporary, `---\n${stringify(parsed.data)}---\n${parsed.body}`)
|
|
184
|
+
await writeFile(temporary, `---\n${stringify(parsed.data)}---\n${body === undefined ? parsed.body : body}`)
|
|
184
185
|
await rename(temporary, path)
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -208,3 +209,33 @@ export async function setSkillGroup(rows, input) {
|
|
|
208
209
|
else delete data.metadata
|
|
209
210
|
})
|
|
210
211
|
}
|
|
212
|
+
|
|
213
|
+
/** Update the editable frontmatter and instruction body of one Skill. */
|
|
214
|
+
export async function updateSkill(rows, input) {
|
|
215
|
+
if (!input.description.trim()) throw new Error('Skill description is required.')
|
|
216
|
+
await updateSkillFrontmatter(rows, input, data => {
|
|
217
|
+
data.description = input.description.trim()
|
|
218
|
+
if (input.whenToUse.trim()) data.whenToUse = input.whenToUse.trim()
|
|
219
|
+
else delete data.whenToUse
|
|
220
|
+
if (input.modelInvocable) delete data['disable-model-invocation']
|
|
221
|
+
else data['disable-model-invocation'] = true
|
|
222
|
+
if (input.userInvocable) delete data['user-invocable']
|
|
223
|
+
else data['user-invocable'] = false
|
|
224
|
+
}, input.content)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Delete one exact inspected Skill; bundle deletion removes its complete directory. */
|
|
228
|
+
export async function deleteSkill(rows, input) {
|
|
229
|
+
const root = skillSource(rows, input.sourceId)
|
|
230
|
+
const inventory = await inspectSkills(rows)
|
|
231
|
+
const skill = inventory.skills.find(item => item.sourceId === input.sourceId && item.relativePath === input.relativePath)
|
|
232
|
+
if (!skill?.valid) throw new Error('Unknown or invalid Skill.')
|
|
233
|
+
const path = resolve(root, input.relativePath)
|
|
234
|
+
const child = relative(resolve(root), path)
|
|
235
|
+
if (!child || child.startsWith('..') || resolve(root, child) !== path) throw new Error('Skill path escapes its source.')
|
|
236
|
+
const content = await readFile(path, 'utf8')
|
|
237
|
+
if (createHash('sha256').update(content).digest('hex') !== input.skillRevision) {
|
|
238
|
+
throw Object.assign(new Error('Skill changed. Refresh before deleting.'), { code: 'CONFLICT' })
|
|
239
|
+
}
|
|
240
|
+
await rm(skill.format === 'bundle' ? dirname(path) : path, { recursive: skill.format === 'bundle' })
|
|
241
|
+
}
|
package/src/store.js
CHANGED
|
@@ -182,6 +182,11 @@ export class ExtensionStore {
|
|
|
182
182
|
|
|
183
183
|
async addMcp(id, input, signal, expectedRevision) {
|
|
184
184
|
validateId(id)
|
|
185
|
+
const config = this.normalizeMcp(id, input)
|
|
186
|
+
return this.add({ id, name: moduleNames.mcp, disabled: true, config }, signal, expectedRevision)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
normalizeMcp(id, input) {
|
|
185
190
|
// Authentication values belong in the host credential setup, not tool arguments or logs.
|
|
186
191
|
if (input.env || input.headers) throw new Error('This version does not accept environment values or authentication headers.')
|
|
187
192
|
const normalized = input.transport === 'stdio' ? {
|
|
@@ -194,7 +199,7 @@ export class ExtensionStore {
|
|
|
194
199
|
const url = new URL(config.url)
|
|
195
200
|
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('Use an HTTP(S) endpoint without credentials, query or fragment.')
|
|
196
201
|
} else if (!config.command.trim()) throw new Error('MCP command cannot be empty.')
|
|
197
|
-
return
|
|
202
|
+
return config
|
|
198
203
|
}
|
|
199
204
|
|
|
200
205
|
async add(row, signal, expectedRevision) {
|
|
@@ -211,4 +216,34 @@ export class ExtensionStore {
|
|
|
211
216
|
row.disabled = !enabled
|
|
212
217
|
}, signal, expectedRevision)
|
|
213
218
|
}
|
|
219
|
+
|
|
220
|
+
async updateSkillSource(id, directory, signal, expectedRevision) {
|
|
221
|
+
const normalizedDirectory = normalizeWindowsPath(directory)
|
|
222
|
+
if (!isAbsolute(normalizedDirectory) || !(await stat(normalizedDirectory)).isDirectory()) throw new Error('Skill root must be an existing absolute directory containing skill bundles.')
|
|
223
|
+
return this.update(rows => {
|
|
224
|
+
const row = rows.find(row => row.id === id && !row.config.serverName)
|
|
225
|
+
if (!row) throw new Error('Unknown Skill source.')
|
|
226
|
+
if (rows.some(other => other !== row && !other.config.serverName && samePath(other.config.customSkillDirs[0], normalizedDirectory))) {
|
|
227
|
+
throw new Error('Skill path already exists.')
|
|
228
|
+
}
|
|
229
|
+
row.config.customSkillDirs = [normalizedDirectory]
|
|
230
|
+
}, signal, expectedRevision)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async updateMcp(id, input, signal, expectedRevision) {
|
|
234
|
+
const config = this.normalizeMcp(id, input)
|
|
235
|
+
return this.update(rows => {
|
|
236
|
+
const row = rows.find(row => row.id === id && row.config.serverName)
|
|
237
|
+
if (!row) throw new Error('Unknown MCP server.')
|
|
238
|
+
row.config = config
|
|
239
|
+
}, signal, expectedRevision)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async remove(id, signal, expectedRevision) {
|
|
243
|
+
return this.update(rows => {
|
|
244
|
+
const index = rows.findIndex(row => row.id === id)
|
|
245
|
+
if (index < 0) throw new Error('Unknown extension id.')
|
|
246
|
+
rows.splice(index, 1)
|
|
247
|
+
}, signal, expectedRevision)
|
|
248
|
+
}
|
|
214
249
|
}
|
package/src/web.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/** Authenticated Connection channel for the independent extension settings page. */
|
|
2
2
|
import Schema from '@deepseek-ai/schemastery'
|
|
3
3
|
import { revisionOf } from './store.js'
|
|
4
|
-
import { createSkill, inspectSkills, setSkillGroup, setSkillInvocation } from './skills.js'
|
|
4
|
+
import { createSkill, deleteSkill, inspectSkills, setSkillGroup, setSkillInvocation, updateSkill } from './skills.js'
|
|
5
5
|
|
|
6
6
|
const text = Schema.string().required()
|
|
7
7
|
const schemas = {
|
|
8
8
|
list: Schema.object({}),
|
|
9
9
|
'add-skill': Schema.object({ directory: text, revision: text }),
|
|
10
10
|
'add-mcp': Schema.object({ id: text, configuration: text, revision: text }),
|
|
11
|
+
'update-skill-source': Schema.object({ id: text, directory: text, revision: text }),
|
|
12
|
+
'update-mcp': Schema.object({ id: text, configuration: text, revision: text }),
|
|
13
|
+
'delete-extension': Schema.object({ id: text, revision: text }),
|
|
11
14
|
enable: Schema.object({ id: text, enabled: Schema.boolean().required(), revision: text }),
|
|
12
15
|
'set-skill-group': Schema.object({ sourceId: text, relativePath: text, skillRevision: text, group: Schema.string().required(), revision: text }),
|
|
13
16
|
'create-skill': Schema.object({
|
|
@@ -19,6 +22,12 @@ const schemas = {
|
|
|
19
22
|
sourceId: text, relativePath: text, skillRevision: text,
|
|
20
23
|
modelInvocable: Schema.boolean().required(), userInvocable: Schema.boolean().required(), revision: text,
|
|
21
24
|
}),
|
|
25
|
+
'update-skill': Schema.object({
|
|
26
|
+
sourceId: text, relativePath: text, skillRevision: text, description: text,
|
|
27
|
+
whenToUse: Schema.string().required(), content: Schema.string().required(),
|
|
28
|
+
modelInvocable: Schema.boolean().required(), userInvocable: Schema.boolean().required(), revision: text,
|
|
29
|
+
}),
|
|
30
|
+
'delete-skill': Schema.object({ sourceId: text, relativePath: text, skillRevision: text, revision: text }),
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/** Return redacted configuration and observed global tool availability. */
|
|
@@ -37,6 +46,9 @@ export async function describeExtensions(store, ctx) {
|
|
|
37
46
|
: row.config.transport === 'streamable-http' ? new URL(row.config.url).origin
|
|
38
47
|
: row.config.customSkillDirs[0],
|
|
39
48
|
transport: row.config.transport ?? '',
|
|
49
|
+
configuration: row.config.serverName ? (row.config.transport === 'stdio'
|
|
50
|
+
? { transport: row.config.transport, command: row.config.command, args: row.config.args ?? [], cwd: row.config.cwd ?? '' }
|
|
51
|
+
: { transport: row.config.transport, url: row.config.url }) : undefined,
|
|
40
52
|
tools: row.config.serverName
|
|
41
53
|
? toolNames.filter(name => name.startsWith(`mcp__${row.id}__`)).sort() : [],
|
|
42
54
|
})),
|
|
@@ -56,15 +68,24 @@ export function createWebHandler(store, ctx) {
|
|
|
56
68
|
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Expected an MCP configuration object.')
|
|
57
69
|
await store.addMcp(args.id, input, signal, args.revision)
|
|
58
70
|
}
|
|
71
|
+
if (endpoint === 'update-skill-source') await store.updateSkillSource(args.id, args.directory, signal, args.revision)
|
|
72
|
+
if (endpoint === 'update-mcp') {
|
|
73
|
+
const input = JSON.parse(args.configuration)
|
|
74
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Expected an MCP configuration object.')
|
|
75
|
+
await store.updateMcp(args.id, input, signal, args.revision)
|
|
76
|
+
}
|
|
77
|
+
if (endpoint === 'delete-extension') await store.remove(args.id, signal, args.revision)
|
|
59
78
|
if (endpoint === 'enable') await store.setEnabled(args.id, args.enabled, signal, args.revision)
|
|
60
|
-
if (
|
|
79
|
+
if (['create-skill', 'set-skill-invocation', 'set-skill-group', 'update-skill', 'delete-skill'].includes(endpoint)) {
|
|
61
80
|
const rows = await store.read()
|
|
62
81
|
if (revisionOf(rows) !== args.revision) {
|
|
63
82
|
throw Object.assign(new Error('Configuration changed. Refresh before saving.'), { code: 'CONFLICT' })
|
|
64
83
|
}
|
|
65
84
|
if (endpoint === 'create-skill') await createSkill(rows, args)
|
|
66
85
|
else if (endpoint === 'set-skill-invocation') await setSkillInvocation(rows, args)
|
|
67
|
-
else await setSkillGroup(rows, args)
|
|
86
|
+
else if (endpoint === 'set-skill-group') await setSkillGroup(rows, args)
|
|
87
|
+
else if (endpoint === 'update-skill') await updateSkill(rows, args)
|
|
88
|
+
else await deleteSkill(rows, args)
|
|
68
89
|
}
|
|
69
90
|
return { ok: true, value: await describeExtensions(store, ctx) }
|
|
70
91
|
} catch (error) {
|