@civilization/deepseek-harness-skill-mcp 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -145,7 +145,11 @@ MCP 工具是提供给模型的工具 schema,不是用户 `/` 命令,因此
145
145
 
146
146
  当前版本不接受 `env`、认证 header、带凭据或 token 查询参数的 URL。请勿把密钥放进工具参数、MCP 地址或提交到仓库。
147
147
 
148
- MCP 对端离线、命令启动失败或首次工具发现失败不会阻止 DSH 启动。stdio 与 Streamable HTTP 使用 Harness MCP client 的后台重连策略;旧版 SSE 当前依赖连接自身,在断线后可通过刷新配置重新加载。设置页在连接恢复并发现工具后更新状态。插件加载时也会把旧版本写入的严格启动配置迁移为非致命启动策略。
148
+ ### MCP 自动重连与周期配置
149
+
150
+ - **重连周期滑块(3–60 秒,默认 7 秒)**:用户在添加或编辑 MCP 服务器时,可使用滑块指定最大重连等待阈值(`reconnectInterval`)。
151
+ - **无限长效重试保底**:解除底层连续 10 次失败后彻底放弃重试的硬限制,改为长效保底探测模式。当服务端离线或重启时,客户端从 500ms 开始指数退避,并在达到设定的阈值后持续周期探测;只要 MCP 服务端恢复,客户端将在设定的阈值时间内自动重新建立连接并热重载工具。
152
+ - **全协议统一管理**:`stdio`、`streamable-http` 与旧版 `sse` 均统一纳入监督重连体系,端点离线或初次发现失败绝不阻塞 DSH 正常启动。
149
153
 
150
154
  ## Agent 工具与 Skill
151
155
 
@@ -196,12 +200,12 @@ pnpm run web
196
200
 
197
201
  浏览器端 `/` 候选缓存失效兼容逻辑会使用公开的 `connection/reset` 客户端事件;服务端通过注入 `webServer` 与 `connection` 双重挂载 `/extensions` 路由并由 Cordis effect 托管生命周期,防止动态请求掉入前端静态资源的 405 fallback。自动测试验证 Host/RPC、HTTP 桥接和浏览器 bundle 构建,但没有在本仓库内启动完整 Harness Web 做端到端菜单点击验证。外部文件改动仍依赖 filesystem provider 先完成 watcher 失效,必要时可在变更后点击管理页“刷新”。
198
202
 
199
- ## Harness 版本兼容
203
+ ## Harness / Cordis 版本兼容
200
204
 
201
- 当前版本在 DeepSeek Harness **0.2.0-rc.2** 上验证:`pnpm test`(33 项)与 `pnpm run build` 通过,并逐项核对了所用公开接口——`tools.register/get/schemas/guard`、`tools/pre-execute` 的 `deny` 决策、`skills.register/snapshot`、`webServer.register(kind: 'prefix')`、服务端 `connection.rpc.handle` 与浏览器端 `connection.rpc.call`、`settings.section` 槽位、`locale.register/bind`、`connection/reset` 事件、客户端 bundle 的 `window.__ModuleLoader__.load({ id, factory })` 格式。
205
+ 当前版本在 DeepSeek Harness **0.2.1-rc.2** 及 Cordis **4.0.5** 上验证:`pnpm test`(35 项全部通过)与 `pnpm run build` 通过,并逐项核对了所用公开接口——`tools.register/get/schemas/guard`、`tools/pre-execute` 的 `deny` 决策、`skills.register/snapshot`、`webServer.register(kind: 'prefix')`、服务端 `connection.rpc.handle` 与浏览器端 `connection.rpc.call`、`settings.section` 槽位、`locale.register/bind`、`connection/reset` 事件、客户端 bundle 的 `window.__ModuleLoader__.load({ id, factory })` 格式。
202
206
 
203
- - `peerDependencies` 只声明**下界**(`@deepseek-ai/dsh-tools: >=0.1.5-rc.2`,无上界),以便跟随 Harness 的快速迭代:Harness 的插件兼容性判定用运行时版本比对这些范围。代价是不再对未来的破坏性变更做“响亮拒绝”,且**不保证前序版本**。
204
- - `@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/dsh-skill-filesystem` 作为 `dependencies` 固定在 `0.2.0-rc.2`:插件只用它们的 `Config` 校验/规范化托管配置行(真正的连接由 Harness 自带的同名插件完成)。Schemastery 对象对未知字段**透传**,已实测 0.2.0 新增的 `maxInstructionBytes` 在旧 schema 下也不会被丢弃;但若未来 Harness 改的是必填约束或字段语义,这个固定版本会成为陈旧校验源,届时应同步升级。
207
+ - `peerDependencies` 只声明**下界**(`@deepseek-ai/cordis: ^4.0.2`,`@deepseek-ai/dsh-tools: >=0.1.5-rc.2`,无上界),以便跟随 Harness 的快速迭代:Harness 的插件兼容性判定用运行时版本比对这些范围。
208
+ - `@deepseek-ai/dsh-mcp-client` 与 `@deepseek-ai/dsh-skill-filesystem` 作为 `dependencies` 对齐至 `0.2.1-rc.2`,并对齐 `@deepseek-ai/schemastery: ~3.18.5`。
205
209
  - 工具目录的核心工具名单同样是版本相关数据,见上文“名单维护”。
206
210
 
207
211
  ## 发布
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" fill="none">
2
+ <rect x="1" y="1" width="30" height="30" rx="8" fill="#7C3AED"/>
3
+ <path d="M11 6v5M21 6v5M8 11h16v6a8 8 0 0 1-8 8h0a8 8 0 0 1-8-8v-6z" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
4
+ <path d="M16 25v3" stroke="#FFFFFF" stroke-width="2.2" stroke-linecap="round"/>
5
+ </svg>
package/lib/client.js CHANGED
@@ -23,6 +23,8 @@ var index_exports = {};
23
23
  __export(index_exports, {
24
24
  CatalogSection: () => CatalogSection,
25
25
  McpSection: () => McpSection,
26
+ SkillMcpIcon: () => SkillMcpIcon,
27
+ SkillMcpPage: () => SkillMcpPage,
26
28
  SkillSection: () => SkillSection,
27
29
  apply: () => apply,
28
30
  inject: () => inject
@@ -93,6 +95,9 @@ var ExtensionsController = class {
93
95
 
94
96
  // src/client/locales.js
95
97
  var zh = {
98
+ panel: "\u6280\u80FD\u4E0E MCP",
99
+ panelTitle: "\u6280\u80FD\u4E0E\u5916\u90E8\u6269\u5C55",
100
+ panelIntro: "\u7BA1\u7406\u81EA\u5B9A\u4E49 Skill \u6280\u80FD\u5E93\u3001MCP \u5DE5\u5177\u670D\u52A1\u4E0E\u7CFB\u7EDF\u5DE5\u5177\u6743\u9650\u3002",
96
101
  navSkill: "Skill \u7BA1\u7406",
97
102
  navMcp: "MCP \u7BA1\u7406",
98
103
  skillIntro: "\u914D\u7F6E\u4E00\u4E2A\u6216\u591A\u4E2A Skill \u8DEF\u5F84\uFF1B\u6BCF\u4E2A\u8DEF\u5F84\u4E0B\u53EF\u4EE5\u5305\u542B\u591A\u4E2A\u6280\u80FD\u3002",
@@ -216,9 +221,15 @@ var zh = {
216
221
  noCatalogResults: "\u672A\u627E\u5230\u5339\u914D\u7684\u5DE5\u5177",
217
222
  noCatalogResultsHint: "\u8BF7\u5C1D\u8BD5\u4FEE\u6539\u641C\u7D22\u8BCD\u6216\u9009\u62E9\u5176\u4ED6\u63D2\u4EF6\u5206\u7EC4\u3002",
218
223
  toolProtected: "\u6838\u5FC3\u4FDD\u7559",
219
- toolTakeoverHint: "\u6B64\u5DE5\u5177\u53D7 skill-catalog \u7EDF\u4E00\u7BA1\u7406\u3002"
224
+ toolTakeoverHint: "\u6B64\u5DE5\u5177\u53D7 skill-catalog \u7EDF\u4E00\u7BA1\u7406\u3002",
225
+ reconnectInterval: "\u91CD\u8FDE\u5468\u671F\u9608\u503C",
226
+ reconnectIntervalDesc: "\u670D\u52A1\u7AEF\u65AD\u5F00\u540E\u81EA\u52A8\u63A2\u6D4B\u5E76\u6062\u590D\u8FDE\u63A5\u7684\u6700\u5927\u7B49\u5F85\u65F6\u95F4\uFF08\u8303\u56F4 3\u201360 \u79D2\uFF0C\u9ED8\u8BA4 7 \u79D2\uFF09\u3002",
227
+ seconds: "\u79D2"
220
228
  };
221
229
  var en = {
230
+ panel: "Skills & MCP",
231
+ panelTitle: "Skills & External Extensions",
232
+ panelIntro: "Manage custom Skill repositories, MCP tool servers, and tool permissions.",
222
233
  navSkill: "Skills",
223
234
  navMcp: "MCP",
224
235
  skillIntro: "Configure one or more Skill paths; each path can contain multiple skills.",
@@ -342,11 +353,14 @@ var en = {
342
353
  noCatalogResults: "No matching tools found",
343
354
  noCatalogResultsHint: "Try adjusting your search terms or filter.",
344
355
  toolProtected: "Protected",
345
- toolTakeoverHint: "This tool is managed under skill-catalog."
356
+ toolTakeoverHint: "This tool is managed under skill-catalog.",
357
+ reconnectInterval: "Reconnect Interval",
358
+ reconnectIntervalDesc: "Maximum wait time before retrying dropped connections (range 3\u201360s, default 7s).",
359
+ seconds: "s"
346
360
  };
347
361
 
348
362
  // src/client/styles.css
349
- 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\n/* Skill Catalog Takeover Styles */\n.dsh-catalog-section{margin-top:14px}\n.dsh-catalog-header-actions{display:flex;align-items:center;gap:8px}\n.dsh-catalog-group-card{margin-top:16px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-catalog-group-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 50%,transparent);border-bottom:1px solid var(--dsw-alias-border-l4);cursor:pointer;user-select:none}\n.dsh-catalog-group-header::-webkit-details-marker{display:none}\n.dsh-catalog-group-header::marker{content:""}\n.dsh-catalog-group-card:not([open]) .dsh-catalog-group-header{border-bottom:0}\n.dsh-catalog-group-title{display:flex;align-items:center;gap:10px}\n.dsh-catalog-group-title::before{content:"\u25B6";display:inline-block;font-size:11px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}\n.dsh-catalog-group-card[open] .dsh-catalog-group-title::before{transform:rotate(90deg)}\n.dsh-catalog-group-title h4{margin:0;font-size:14px;font-weight:600}\n.dsh-catalog-group-actions{display:flex;align-items:center;gap:6px}\n.dsh-catalog-group-actions button{padding:4px 10px;font-size:12px;border-radius:7px}\n.dsh-catalog-tool-item{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);transition:background .15s ease}\n.dsh-catalog-tool-item:last-child{border-bottom:0}\n.dsh-catalog-tool-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 40%,transparent)}\n.dsh-catalog-tool-info{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}\n.dsh-catalog-tool-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.dsh-catalog-tool-header code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary)}\n.dsh-catalog-tool-info p{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}\n.dsh-catalog-switch-wrap{display:flex;align-items:center;gap:10px;flex-shrink:0}\n.dsh-catalog-switch-label{font-size:12px;font-weight:500}\n.dsh-catalog-switch{position:relative;display:inline-block;width:40px;height:22px;cursor:pointer}\n.dsh-catalog-switch input{opacity:0;width:0;height:0;position:absolute}\n.dsh-catalog-slider{position:absolute;cursor:pointer;inset:0;background:var(--dsw-alias-bg-layer-4,#3b3b3b);border:1px solid var(--dsw-alias-border-l4);border-radius:22px;transition:.2s}\n.dsh-catalog-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider{background:var(--dsw-alias-brand-primary,#3182ce);border-color:var(--dsw-alias-brand-primary,#3182ce)}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider:before{transform:translateX(18px)}\n.dsh-catalog-switch input:disabled + .dsh-catalog-slider{opacity:.4;cursor:not-allowed}\n.dsh-catalog-tool-details{margin-top:4px;width:100%}\n.dsh-catalog-tool-details summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none;transition:color .15s}\n.dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}\n\n';
363
+ 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\n/* Skill Catalog Takeover Styles */\n.dsh-catalog-section{margin-top:14px}\n.dsh-catalog-header-actions{display:flex;align-items:center;gap:8px}\n.dsh-catalog-group-card{margin-top:16px;border:1px solid var(--dsw-alias-border-l4);border-radius:14px;background:var(--dsw-alias-bg-layer-2);overflow:hidden}\n.dsh-catalog-group-header{display:flex;justify-content:space-between;align-items:center;padding:14px 18px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 50%,transparent);border-bottom:1px solid var(--dsw-alias-border-l4);cursor:pointer;user-select:none}\n.dsh-catalog-group-header::-webkit-details-marker{display:none}\n.dsh-catalog-group-header::marker{content:""}\n.dsh-catalog-group-card:not([open]) .dsh-catalog-group-header{border-bottom:0}\n.dsh-catalog-group-title{display:flex;align-items:center;gap:10px}\n.dsh-catalog-group-title::before{content:"\u25B6";display:inline-block;font-size:11px;color:var(--dsw-alias-label-tertiary);transition:transform .2s ease}\n.dsh-catalog-group-card[open] .dsh-catalog-group-title::before{transform:rotate(90deg)}\n.dsh-catalog-group-title h4{margin:0;font-size:14px;font-weight:600}\n.dsh-catalog-group-actions{display:flex;align-items:center;gap:6px}\n.dsh-catalog-group-actions button{padding:4px 10px;font-size:12px;border-radius:7px}\n.dsh-catalog-tool-item{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:14px 18px;border-bottom:1px solid var(--dsw-alias-border-l4);transition:background .15s ease}\n.dsh-catalog-tool-item:last-child{border-bottom:0}\n.dsh-catalog-tool-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 40%,transparent)}\n.dsh-catalog-tool-info{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}\n.dsh-catalog-tool-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.dsh-catalog-tool-header code{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:13px;font-weight:600;color:var(--dsw-alias-brand-primary)}\n.dsh-catalog-tool-info p{margin:0;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.5}\n.dsh-catalog-switch-wrap{display:flex;align-items:center;gap:10px;flex-shrink:0}\n.dsh-catalog-switch-label{font-size:12px;font-weight:500}\n.dsh-catalog-switch{position:relative;display:inline-block;width:40px;height:22px;cursor:pointer}\n.dsh-catalog-switch input{opacity:0;width:0;height:0;position:absolute}\n.dsh-catalog-slider{position:absolute;cursor:pointer;inset:0;background:var(--dsw-alias-bg-layer-4,#3b3b3b);border:1px solid var(--dsw-alias-border-l4);border-radius:22px;transition:.2s}\n.dsh-catalog-slider:before{position:absolute;content:"";height:16px;width:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider{background:var(--dsw-alias-brand-primary,#3182ce);border-color:var(--dsw-alias-brand-primary,#3182ce)}\n.dsh-catalog-switch input:checked + .dsh-catalog-slider:before{transform:translateX(18px)}\n.dsh-catalog-switch input:disabled + .dsh-catalog-slider{opacity:.4;cursor:not-allowed}\n.dsh-catalog-tool-details{margin-top:4px;width:100%}\n.dsh-catalog-tool-details summary{width:max-content;cursor:pointer;font-size:12px;color:var(--dsw-alias-label-secondary);user-select:none;transition:color .15s}\n.dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}\n.dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}\n\n/* MCP Reconnect Slider */\n.dsh-mcp-slider-group{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4)}\n.dsh-mcp-slider-header{display:flex;justify-content:space-between;align-items:center}\n.dsh-mcp-slider-header label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}\n.dsh-mcp-slider-badge{font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}\n.dsh-mcp-slider-group input[type=range]{width:100%;cursor:pointer;accent-color:var(--dsw-alias-brand-primary);margin:4px 0}\n.dsh-mcp-slider-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.4}\n\n\n\n/* Main Panel Page Layout */\n.dsh-ext-page{box-sizing:border-box;height:100%;display:flex;flex-direction:column;align-items:center;overflow:auto;padding:0 clamp(24px,4vw,48px) 48px;color:var(--dsw-alias-label-primary)}\n.dsh-ext-page>*{width:100%;max-width:960px}\n.dsh-ext-page-head{box-sizing:border-box;padding-top:28px}\n[data-platform=darwin] .dsh-ext-page-head{padding-top:calc(28px + var(--dsh-frame-top-clearance,0px))}\n.dsh-ext-page-title{margin:0;font-size:20px;font-weight:500;line-height:28px}\n.dsh-ext-page-intro{margin:4px 0 0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}\n.dsh-ext-page-tabs{display:flex;gap:8px;margin-top:20px;border-bottom:1px solid var(--dsw-alias-border-l4);padding-bottom:12px}\n.dsh-ext-page-tabs 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:var(--dsw-alias-label-secondary);padding:6px 14px;transition:all .15s ease}\n.dsh-ext-page-tabs button:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}\n.dsh-ext-page-tabs button.is-active{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary);font-weight:500}\n.dsh-ext-page-body{width:100%;max-width:960px}\n';
350
364
 
351
365
  // src/client/index.jsx
352
366
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -381,17 +395,20 @@ function apply(ctx) {
381
395
  return ok;
382
396
  };
383
397
  const face = { hooks: { manager: controller }, request };
384
- const section = (id, kind) => ctx.slots.inject("settings.section", () => ctx.slots.register({
385
- name: "settings.section",
386
- id,
387
- order: kind === "skill" ? 16 : kind === "mcp" ? 17 : 18,
388
- label: () => t(kind === "skill" ? "navSkill" : kind === "mcp" ? "navMcp" : "navCatalog"),
398
+ const PANEL_ID = "skill-mcp";
399
+ ctx.slots.inject("main", () => ctx.slots.register({
400
+ name: "main",
401
+ key: PANEL_ID,
389
402
  locale: namespace,
390
403
  inject: () => face
391
- }, kind === "skill" ? SkillSection : kind === "mcp" ? McpSection : CatalogSection));
392
- section("skill-manager", "skill");
393
- section("mcp-manager", "mcp");
394
- section("skill-catalog", "catalog");
404
+ }, SkillMcpPage));
405
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
406
+ name: "sidebar.panellist",
407
+ id: PANEL_ID,
408
+ order: 21,
409
+ locale: namespace,
410
+ label: () => t("panel")
411
+ }, SkillMcpIcon));
395
412
  }
396
413
  function SkillSection({ t, useManager, request }) {
397
414
  const state = useManager((value) => value);
@@ -962,8 +979,9 @@ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
962
979
  }
963
980
  function McpForm({ row, t, busy, onSave, onCancel }) {
964
981
  const prefix = (0, import_react.useId)();
965
- const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "" };
982
+ const initial = row?.configuration ?? { transport: "stdio", command: "", args: [], cwd: "", reconnectInterval: 7 };
966
983
  const [transport, setTransport] = (0, import_react.useState)(initial.transport);
984
+ const [reconnectInterval, setReconnectInterval] = (0, import_react.useState)(initial.reconnectInterval ?? 7);
967
985
  const field = (name, required = true, multiline = false, value = "") => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
968
986
  t(name),
969
987
  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 } : {} })
@@ -971,7 +989,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
971
989
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { className: row ? "dsh-ext-inline-form" : "dsh-ext-form", onSubmit: (event) => {
972
990
  event.preventDefault();
973
991
  const data = Object.fromEntries(new FormData(event.currentTarget));
974
- 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 });
992
+ const configuration = JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval } : { transport, url: data.url, reconnectInterval });
975
993
  void onSave({ id: row?.id ?? data.id, configuration });
976
994
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
977
995
  !row && field("id"),
@@ -988,6 +1006,29 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
988
1006
  field("args", false, true, (initial.args ?? []).join("\n")),
989
1007
  field("cwd", false, false, initial.cwd)
990
1008
  ] }) : field("url", true, false, initial.url),
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-group", children: [
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-header", children: [
1011
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: `${prefix}-reconnect`, children: t("reconnectInterval") }),
1012
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-mcp-slider-badge", children: [
1013
+ reconnectInterval,
1014
+ " ",
1015
+ t("seconds")
1016
+ ] })
1017
+ ] }),
1018
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1019
+ "input",
1020
+ {
1021
+ id: `${prefix}-reconnect`,
1022
+ type: "range",
1023
+ min: "3",
1024
+ max: "60",
1025
+ step: "1",
1026
+ value: reconnectInterval,
1027
+ onChange: (event) => setReconnectInterval(Number(event.target.value))
1028
+ }
1029
+ ),
1030
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-mcp-slider-hint", children: t("reconnectIntervalDesc") })
1031
+ ] }),
991
1032
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") }),
992
1033
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
993
1034
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onCancel, children: t("cancel") }),
@@ -998,6 +1039,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
998
1039
  function AddForm({ kind, t, busy, onSave, onCancel }) {
999
1040
  const prefix = (0, import_react.useId)();
1000
1041
  const [transport, setTransport] = (0, import_react.useState)("stdio");
1042
+ const [reconnectInterval, setReconnectInterval] = (0, import_react.useState)(7);
1001
1043
  const field = (name, required = true, multiline = false) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { htmlFor: `${prefix}-${name}`, children: [
1002
1044
  t(name),
1003
1045
  multiline ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", { id: `${prefix}-${name}`, name, rows: 3 }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { id: `${prefix}-${name}`, name, required, autoComplete: "off", ...name === "id" ? { pattern: "[A-Za-z0-9_\\-]{1,32}", maxLength: 32 } : {} })
@@ -1007,7 +1049,7 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
1007
1049
  const data = Object.fromEntries(new FormData(event.currentTarget));
1008
1050
  const args = kind === "skill" ? { directory: data.directory } : {
1009
1051
  id: data.id,
1010
- configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd } : { transport, url: data.url })
1052
+ configuration: JSON.stringify(transport === "stdio" ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval } : { transport, url: data.url, reconnectInterval })
1011
1053
  };
1012
1054
  void onSave(args);
1013
1055
  }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { disabled: busy, children: [
@@ -1026,6 +1068,29 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
1026
1068
  field("args", false, true),
1027
1069
  field("cwd", false)
1028
1070
  ] }) : field("url"),
1071
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-group", children: [
1072
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-mcp-slider-header", children: [
1073
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: `${prefix}-reconnect`, children: t("reconnectInterval") }),
1074
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-mcp-slider-badge", children: [
1075
+ reconnectInterval,
1076
+ " ",
1077
+ t("seconds")
1078
+ ] })
1079
+ ] }),
1080
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1081
+ "input",
1082
+ {
1083
+ id: `${prefix}-reconnect`,
1084
+ type: "range",
1085
+ min: "3",
1086
+ max: "60",
1087
+ step: "1",
1088
+ value: reconnectInterval,
1089
+ onChange: (event) => setReconnectInterval(Number(event.target.value))
1090
+ }
1091
+ ),
1092
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-mcp-slider-hint", children: t("reconnectIntervalDesc") })
1093
+ ] }),
1029
1094
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("secret") })
1030
1095
  ] }),
1031
1096
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-actions", children: [
@@ -1181,5 +1246,30 @@ function CatalogSection({ t, useManager, request }) {
1181
1246
  ] }, group.id))
1182
1247
  ] });
1183
1248
  }
1249
+ function SkillMcpIcon({ size = 16 }) {
1250
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { viewBox: "0 0 16 16", width: size, height: size, fill: "none", "aria-hidden": "true", style: { display: "block" }, children: [
1251
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5.5 2v3M10.5 2v3M3.5 5h9v3.8c0 2.2-1.8 4-4 4h-1c-2.2 0-4-1.8-4-4V5z", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round", strokeLinejoin: "round" }),
1252
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 12.8v1.7", stroke: "currentColor", strokeWidth: "1.3", strokeLinecap: "round" })
1253
+ ] });
1254
+ }
1255
+ function SkillMcpPage({ t, useManager, request }) {
1256
+ const [activeTab, setActiveTab] = (0, import_react.useState)("skills");
1257
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-page", children: [
1258
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("header", { className: "dsh-ext-page-head", children: [
1259
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "dsh-ext-page-title", children: t("panelTitle") }),
1260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "dsh-ext-page-intro", children: t("panelIntro") }),
1261
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-ext-page-tabs", role: "tablist", children: [
1262
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", role: "tab", "aria-selected": activeTab === "skills", className: activeTab === "skills" ? "is-active" : "", onClick: () => setActiveTab("skills"), children: t("navSkill") }),
1263
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", role: "tab", "aria-selected": activeTab === "mcp", className: activeTab === "mcp" ? "is-active" : "", onClick: () => setActiveTab("mcp"), children: t("navMcp") }),
1264
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", role: "tab", "aria-selected": activeTab === "catalog", className: activeTab === "catalog" ? "is-active" : "", onClick: () => setActiveTab("catalog"), children: t("navCatalog") })
1265
+ ] })
1266
+ ] }),
1267
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("main", { className: "dsh-ext-page-body", children: [
1268
+ activeTab === "skills" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SkillSection, { t, useManager, request }),
1269
+ activeTab === "mcp" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(McpSection, { t, useManager, request }),
1270
+ activeTab === "catalog" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CatalogSection, { t, useManager, request })
1271
+ ] })
1272
+ ] });
1273
+ }
1184
1274
 
1185
1275
  return module.exports;}});
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Skills & MCP",
4
+ "description": "Manage external Skill paths and MCP server connections for DeepSeek Harness."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "技能与 MCP 管理",
4
+ "description": "管理外部 Skill 技能路径与 MCP 服务连接,赋予模型外部工具调用能力。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@civilization/deepseek-harness-skill-mcp",
3
- "version": "0.3.1",
4
- "description": "Skill path and MCP server management for DeepSeek Harness Web",
3
+ "version": "0.3.3",
4
+ "description": "管理外部 Skill 技能路径与 MCP 服务连接,赋予模型外部工具调用能力。",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -18,6 +18,8 @@
18
18
  "exports": {
19
19
  ".": "./src/index.js",
20
20
  "./client": "./lib/client.js",
21
+ "./locale/*.json": "./locale/*.json",
22
+ "./icon": "./icon.svg",
21
23
  "./mcp-sse": "./src/mcp-sse.js",
22
24
  "./package.json": "./package.json"
23
25
  },
@@ -25,6 +27,8 @@
25
27
  "src",
26
28
  "lib",
27
29
  "skills",
30
+ "locale/*.json",
31
+ "icon.svg",
28
32
  "cordis.patch.yml",
29
33
  "README.md",
30
34
  "LICENSE"
@@ -35,8 +39,11 @@
35
39
  },
36
40
  "client": {
37
41
  "platform": "web",
42
+ "immediately": true,
38
43
  "inject": [
39
- "@deepseek-ai/dsh-client-ui-settings"
44
+ "@deepseek-ai/dsh-client-locale",
45
+ "@deepseek-ai/dsh-client-ui-layout",
46
+ "@deepseek-ai/dsh-client-ui-sidebar"
40
47
  ]
41
48
  }
42
49
  },
@@ -44,16 +51,17 @@
44
51
  "test": "node --test tests/*.test.js",
45
52
  "build": "node scripts/build.js",
46
53
  "web": "node scripts/web.js",
47
- "setup": "node scripts/setup.js"
54
+ "setup": "node scripts/setup.js",
55
+ "prepack": "pnpm build"
48
56
  },
49
57
  "engines": {
50
58
  "node": ">=22.19.0"
51
59
  },
52
60
  "packageManager": "pnpm@10.33.0",
53
61
  "dependencies": {
54
- "@deepseek-ai/dsh-mcp-client": "0.2.0-rc.2",
55
- "@deepseek-ai/dsh-skill-filesystem": "0.2.0-rc.2",
56
- "@deepseek-ai/schemastery": "3.18.2",
62
+ "@deepseek-ai/dsh-mcp-client": ">=0.2.1-rc.2 <0.3.0",
63
+ "@deepseek-ai/dsh-skill-filesystem": ">=0.2.1-rc.2 <0.3.0",
64
+ "@deepseek-ai/schemastery": "~3.18.5",
57
65
  "@modelcontextprotocol/sdk": "1.29.0",
58
66
  "yaml": "^2.9.0"
59
67
  },
@@ -62,10 +70,10 @@
62
70
  "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2"
63
71
  },
64
72
  "devDependencies": {
65
- "@deepseek-ai/cordis": "4.0.4",
66
- "@deepseek-ai/dsh-skill": "0.2.0-rc.2",
67
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
68
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
73
+ "@deepseek-ai/cordis": "4.0.5",
74
+ "@deepseek-ai/dsh-skill": "0.2.1-rc.2",
75
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-rc.2",
76
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
69
77
  "esbuild": "^0.28.2",
70
78
  "react": "^19.2.8"
71
79
  }
@@ -31,15 +31,15 @@ export function apply(ctx) {
31
31
  return ok
32
32
  }
33
33
  const face = { hooks: { manager: controller }, request }
34
- // The three sections share one controller and managed patch; each kind gets its own settings page.
35
- const section = (id, kind) => ctx.slots.inject('settings.section', () => ctx.slots.register({
36
- name: 'settings.section', id, order: kind === 'skill' ? 16 : kind === 'mcp' ? 17 : 18,
37
- label: () => t(kind === 'skill' ? 'navSkill' : kind === 'mcp' ? 'navMcp' : 'navCatalog'), locale: namespace,
34
+ const PANEL_ID = 'skill-mcp'
35
+ ctx.slots.inject('main', () => ctx.slots.register({
36
+ name: 'main', key: PANEL_ID, locale: namespace,
38
37
  inject: () => face,
39
- }, kind === 'skill' ? SkillSection : kind === 'mcp' ? McpSection : CatalogSection))
40
- section('skill-manager', 'skill')
41
- section('mcp-manager', 'mcp')
42
- section('skill-catalog', 'catalog')
38
+ }, SkillMcpPage))
39
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
40
+ name: 'sidebar.panellist', id: PANEL_ID, order: 21, locale: namespace,
41
+ label: () => t('panel'),
42
+ }, SkillMcpIcon))
43
43
  }
44
44
 
45
45
  export function SkillSection({ t, useManager, request }) {
@@ -343,8 +343,9 @@ function SkillSourceForm({ source, t, busy, onSave, onCancel }) {
343
343
 
344
344
  function McpForm({ row, t, busy, onSave, onCancel }) {
345
345
  const prefix = useId()
346
- const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '' }
346
+ const initial = row?.configuration ?? { transport: 'stdio', command: '', args: [], cwd: '', reconnectInterval: 7 }
347
347
  const [transport, setTransport] = useState(initial.transport)
348
+ const [reconnectInterval, setReconnectInterval] = useState(initial.reconnectInterval ?? 7)
348
349
  const field = (name, required = true, multiline = false, value = '') => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
349
350
  ? <textarea id={`${prefix}-${name}`} name={name} rows={3} defaultValue={value} />
350
351
  : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" defaultValue={value} {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
@@ -352,13 +353,29 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
352
353
  event.preventDefault()
353
354
  const data = Object.fromEntries(new FormData(event.currentTarget))
354
355
  const configuration = JSON.stringify(transport === 'stdio'
355
- ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
356
- : { transport, url: data.url })
356
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval }
357
+ : { transport, url: data.url, reconnectInterval })
357
358
  void onSave({ id: row?.id ?? data.id, configuration })
358
359
  }}><fieldset disabled={busy}>{!row && field('id')}
359
360
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
360
361
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
361
362
  {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)}
363
+ <div className="dsh-mcp-slider-group">
364
+ <div className="dsh-mcp-slider-header">
365
+ <label htmlFor={`${prefix}-reconnect`}>{t('reconnectInterval')}</label>
366
+ <span className="dsh-mcp-slider-badge">{reconnectInterval} {t('seconds')}</span>
367
+ </div>
368
+ <input
369
+ id={`${prefix}-reconnect`}
370
+ type="range"
371
+ min="3"
372
+ max="60"
373
+ step="1"
374
+ value={reconnectInterval}
375
+ onChange={event => setReconnectInterval(Number(event.target.value))}
376
+ />
377
+ <p className="dsh-mcp-slider-hint">{t('reconnectIntervalDesc')}</p>
378
+ </div>
362
379
  <p>{t('secret')}</p>
363
380
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : row ? 'saveChanges' : 'save')}</button></div>
364
381
  </fieldset></form>
@@ -367,6 +384,7 @@ function McpForm({ row, t, busy, onSave, onCancel }) {
367
384
  function AddForm({ kind, t, busy, onSave, onCancel }) {
368
385
  const prefix = useId()
369
386
  const [transport, setTransport] = useState('stdio')
387
+ const [reconnectInterval, setReconnectInterval] = useState(7)
370
388
  const field = (name, required = true, multiline = false) => <label htmlFor={`${prefix}-${name}`}>{t(name)}{multiline
371
389
  ? <textarea id={`${prefix}-${name}`} name={name} rows={3} />
372
390
  : <input id={`${prefix}-${name}`} name={name} required={required} autoComplete="off" {...(name === 'id' ? { pattern: '[A-Za-z0-9_\\-]{1,32}', maxLength: 32 } : {})} />}</label>
@@ -375,14 +393,30 @@ function AddForm({ kind, t, busy, onSave, onCancel }) {
375
393
  const data = Object.fromEntries(new FormData(event.currentTarget))
376
394
  const args = kind === 'skill' ? { directory: data.directory } : {
377
395
  id: data.id, configuration: JSON.stringify(transport === 'stdio'
378
- ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd }
379
- : { transport, url: data.url }),
396
+ ? { transport, command: data.command, args: data.args.split(/\r?\n/).filter(Boolean), cwd: data.cwd, reconnectInterval }
397
+ : { transport, url: data.url, reconnectInterval }),
380
398
  }
381
399
  void onSave(args)
382
400
  }}><fieldset disabled={busy}>{kind === 'skill' ? field('directory') : <>{field('id')}
383
401
  <label htmlFor={`${prefix}-transport`}>{t('transport')}<select id={`${prefix}-transport`} value={transport} onChange={event => setTransport(event.target.value)}>
384
402
  <option value="stdio">{t('stdio')}</option><option value="streamable-http">{t('http')}</option><option value="sse">{t('sse')}</option></select></label>
385
403
  {transport === 'stdio' ? <>{field('command')}{field('args', false, true)}{field('cwd', false)}</> : field('url')}
404
+ <div className="dsh-mcp-slider-group">
405
+ <div className="dsh-mcp-slider-header">
406
+ <label htmlFor={`${prefix}-reconnect`}>{t('reconnectInterval')}</label>
407
+ <span className="dsh-mcp-slider-badge">{reconnectInterval} {t('seconds')}</span>
408
+ </div>
409
+ <input
410
+ id={`${prefix}-reconnect`}
411
+ type="range"
412
+ min="3"
413
+ max="60"
414
+ step="1"
415
+ value={reconnectInterval}
416
+ onChange={event => setReconnectInterval(Number(event.target.value))}
417
+ />
418
+ <p className="dsh-mcp-slider-hint">{t('reconnectIntervalDesc')}</p>
419
+ </div>
386
420
  <p>{t('secret')}</p>
387
421
  </>}
388
422
  <div className="dsh-ext-actions"><button type="button" onClick={onCancel}>{t('cancel')}</button><button type="submit">{t(busy ? 'saving' : 'save')}</button></div>
@@ -532,3 +566,31 @@ export function CatalogSection({ t, useManager, request }) {
532
566
  </section>
533
567
  }
534
568
 
569
+
570
+
571
+ export function SkillMcpIcon({ size = 16 }) {
572
+ return <svg viewBox="0 0 16 16" width={size} height={size} fill="none" aria-hidden="true" style={{ display: "block" }}>
573
+ <path d="M5.5 2v3M10.5 2v3M3.5 5h9v3.8c0 2.2-1.8 4-4 4h-1c-2.2 0-4-1.8-4-4V5z" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" />
574
+ <path d="M8 12.8v1.7" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
575
+ </svg>
576
+ }
577
+
578
+ export function SkillMcpPage({ t, useManager, request }) {
579
+ const [activeTab, setActiveTab] = useState("skills")
580
+ return <div className="dsh-ext-page">
581
+ <header className="dsh-ext-page-head">
582
+ <h1 className="dsh-ext-page-title">{t("panelTitle")}</h1>
583
+ <p className="dsh-ext-page-intro">{t("panelIntro")}</p>
584
+ <div className="dsh-ext-page-tabs" role="tablist">
585
+ <button type="button" role="tab" aria-selected={activeTab === "skills"} className={activeTab === "skills" ? "is-active" : ""} onClick={() => setActiveTab("skills")}>{t("navSkill")}</button>
586
+ <button type="button" role="tab" aria-selected={activeTab === "mcp"} className={activeTab === "mcp" ? "is-active" : ""} onClick={() => setActiveTab("mcp")}>{t("navMcp")}</button>
587
+ <button type="button" role="tab" aria-selected={activeTab === "catalog"} className={activeTab === "catalog" ? "is-active" : ""} onClick={() => setActiveTab("catalog")}>{t("navCatalog")}</button>
588
+ </div>
589
+ </header>
590
+ <main className="dsh-ext-page-body">
591
+ {activeTab === "skills" && <SkillSection t={t} useManager={useManager} request={request} />}
592
+ {activeTab === "mcp" && <McpSection t={t} useManager={useManager} request={request} />}
593
+ {activeTab === "catalog" && <CatalogSection t={t} useManager={useManager} request={request} />}
594
+ </main>
595
+ </div>
596
+ }
@@ -1,5 +1,8 @@
1
1
  /** Both dictionaries use the same key vocabulary; checked by tests. */
2
2
  export const zh = {
3
+ panel: '技能与 MCP',
4
+ panelTitle: '技能与外部扩展',
5
+ panelIntro: '管理自定义 Skill 技能库、MCP 工具服务与系统工具权限。',
3
6
  navSkill: 'Skill 管理', navMcp: 'MCP 管理',
4
7
  skillIntro: '配置一个或多个 Skill 路径;每个路径下可以包含多个技能。', mcpIntro: '管理外部工具服务器,让它们在当前配置中可用。',
5
8
  skills: 'Skill 路径', mcp: 'MCP 服务器', refresh: '刷新', add: '添加路径', addMcp: '添加服务器', newSkill: '新建 Skill',
@@ -51,8 +54,14 @@ export const zh = {
51
54
  noCatalogResultsHint: '请尝试修改搜索词或选择其他插件分组。',
52
55
  toolProtected: '核心保留',
53
56
  toolTakeoverHint: '此工具受 skill-catalog 统一管理。',
57
+ reconnectInterval: '重连周期阈值',
58
+ reconnectIntervalDesc: '服务端断开后自动探测并恢复连接的最大等待时间(范围 3–60 秒,默认 7 秒)。',
59
+ seconds: '秒',
54
60
  }
55
61
  export const en = {
62
+ panel: 'Skills & MCP',
63
+ panelTitle: 'Skills & External Extensions',
64
+ panelIntro: 'Manage custom Skill repositories, MCP tool servers, and tool permissions.',
56
65
  navSkill: 'Skills', navMcp: 'MCP',
57
66
  skillIntro: 'Configure one or more Skill paths; each path can contain multiple skills.', mcpIntro: 'Manage external tool servers for this configuration.',
58
67
  skills: 'Skill paths', mcp: 'MCP servers', refresh: 'Refresh', add: 'Add path', addMcp: 'Add server', newSkill: 'New Skill',
@@ -104,4 +113,7 @@ export const en = {
104
113
  noCatalogResultsHint: 'Try adjusting your search terms or filter.',
105
114
  toolProtected: 'Protected',
106
115
  toolTakeoverHint: 'This tool is managed under skill-catalog.',
116
+ reconnectInterval: 'Reconnect Interval',
117
+ reconnectIntervalDesc: 'Maximum wait time before retrying dropped connections (range 3–60s, default 7s).',
118
+ seconds: 's',
107
119
  }
@@ -70,3 +70,25 @@
70
70
  .dsh-catalog-tool-details summary:hover{color:var(--dsw-alias-label-primary)}
71
71
  .dsh-catalog-tool-details p{margin:6px 0 2px;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.55;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l4)}
72
72
 
73
+ /* MCP Reconnect Slider */
74
+ .dsh-mcp-slider-group{grid-column:1/-1;display:flex;flex-direction:column;gap:8px;padding:12px 14px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l4)}
75
+ .dsh-mcp-slider-header{display:flex;justify-content:space-between;align-items:center}
76
+ .dsh-mcp-slider-header label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
77
+ .dsh-mcp-slider-badge{font-size:12px;font-weight:700;padding:2px 8px;border-radius:999px;color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}
78
+ .dsh-mcp-slider-group input[type=range]{width:100%;cursor:pointer;accent-color:var(--dsw-alias-brand-primary);margin:4px 0}
79
+ .dsh-mcp-slider-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.4}
80
+
81
+
82
+
83
+ /* Main Panel Page Layout */
84
+ .dsh-ext-page{box-sizing:border-box;height:100%;display:flex;flex-direction:column;align-items:center;overflow:auto;padding:0 clamp(24px,4vw,48px) 48px;color:var(--dsw-alias-label-primary)}
85
+ .dsh-ext-page>*{width:100%;max-width:960px}
86
+ .dsh-ext-page-head{box-sizing:border-box;padding-top:28px}
87
+ [data-platform=darwin] .dsh-ext-page-head{padding-top:calc(28px + var(--dsh-frame-top-clearance,0px))}
88
+ .dsh-ext-page-title{margin:0;font-size:20px;font-weight:500;line-height:28px}
89
+ .dsh-ext-page-intro{margin:4px 0 0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary)}
90
+ .dsh-ext-page-tabs{display:flex;gap:8px;margin-top:20px;border-bottom:1px solid var(--dsw-alias-border-l4);padding-bottom:12px}
91
+ .dsh-ext-page-tabs 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:var(--dsw-alias-label-secondary);padding:6px 14px;transition:all .15s ease}
92
+ .dsh-ext-page-tabs button:hover{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}
93
+ .dsh-ext-page-tabs button.is-active{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary);font-weight:500}
94
+ .dsh-ext-page-body{width:100%;max-width:960px}
package/src/mcp-sse.js CHANGED
@@ -18,12 +18,26 @@ export const Config = Schema.object({
18
18
  url: Schema.string().required(),
19
19
  toolCallTimeoutMs: Schema.number().default(60_000),
20
20
  failOnStartupError: Schema.boolean().default(false),
21
+ reconnect: Schema.object({
22
+ enabled: Schema.boolean().default(true),
23
+ initialDelayMs: Schema.number().default(500),
24
+ maxDelayMs: Schema.number().default(7000),
25
+ maxAttempts: Schema.number().default(Number.MAX_SAFE_INTEGER),
26
+ }).default({}),
21
27
  })
22
28
 
23
29
  export async function apply(ctx, config) {
24
- const client = new Client({ name: `dsh-sse-${config.serverName}`, version: '1.0.0' }, { capabilities: {} })
25
- const transport = new SSEClientTransport(new URL(config.url))
30
+ const reconnectPolicy = {
31
+ enabled: config.reconnect?.enabled ?? true,
32
+ initialDelayMs: config.reconnect?.initialDelayMs ?? 500,
33
+ maxDelayMs: config.reconnect?.maxDelayMs ?? 7000,
34
+ maxAttempts: config.reconnect?.maxAttempts ?? Number.MAX_SAFE_INTEGER,
35
+ }
36
+
26
37
  let disposed = false
38
+ let reconnectTimer
39
+ let failedAttempts = 0
40
+ let currentGeneration
27
41
  let registrations = new Map()
28
42
  let synchronization = Promise.resolve()
29
43
 
@@ -31,13 +45,25 @@ export async function apply(ctx, config) {
31
45
  for (const dispose of registrations.values()) dispose()
32
46
  registrations = new Map()
33
47
  }
34
- ctx.effect(() => () => {
35
- disposed = true
36
- clearTools()
37
- void client.close().catch(() => {})
38
- }, 'mcp-sse-client.connection')
39
48
 
40
- const synchronizeTools = async () => {
49
+ const scheduleReconnect = (cause) => {
50
+ if (disposed || !reconnectPolicy.enabled) return
51
+ if (reconnectTimer !== undefined) return
52
+ failedAttempts++
53
+ if (failedAttempts > reconnectPolicy.maxAttempts) {
54
+ ctx.logger.error(`mcp-sse-client(${config.serverName}): giving up after ${reconnectPolicy.maxAttempts} failed reconnect attempts`)
55
+ return
56
+ }
57
+ const delayMs = Math.min(reconnectPolicy.maxDelayMs, reconnectPolicy.initialDelayMs * (2 ** Math.min(failedAttempts - 1, 30)))
58
+ ctx.logger.warn(`mcp-sse-client(${config.serverName}): connection lost or failed (${messageOf(cause)}); reconnecting in ${delayMs}ms (attempt ${failedAttempts})`)
59
+ reconnectTimer = setTimeout(() => {
60
+ reconnectTimer = undefined
61
+ void connectGeneration(false)
62
+ }, delayMs)
63
+ reconnectTimer.unref?.()
64
+ }
65
+
66
+ const synchronizeTools = async (client, generation) => {
41
67
  const definitions = new Map()
42
68
  let cursor
43
69
  do {
@@ -52,7 +78,7 @@ export async function apply(ctx, config) {
52
78
  }
53
79
  cursor = response.nextCursor
54
80
  } while (cursor)
55
- if (disposed) return
81
+ if (disposed || generation.closed) return
56
82
  clearTools()
57
83
  const next = new Map()
58
84
  try {
@@ -64,21 +90,60 @@ export async function apply(ctx, config) {
64
90
  }
65
91
  }
66
92
 
67
- client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
68
- synchronization = synchronization.then(synchronizeTools).catch(error => {
69
- ctx.logger.warn(`mcp-sse-client(${config.serverName}): tool refresh failed: ${messageOf(error)}`)
93
+ async function connectGeneration(startup) {
94
+ if (disposed) return
95
+ const client = new Client({ name: `dsh-sse-${config.serverName}`, version: '1.0.0' }, { capabilities: {} })
96
+ const transport = new SSEClientTransport(new URL(config.url))
97
+ const generation = { client, transport, closed: false }
98
+ currentGeneration = generation
99
+
100
+ const onDisconnect = (err) => {
101
+ if (generation.closed || disposed) return
102
+ generation.closed = true
103
+ clearTools()
104
+ void client.close().catch(() => {})
105
+ if (currentGeneration === generation) {
106
+ scheduleReconnect(err ?? new Error('connection closed'))
107
+ }
108
+ }
109
+
110
+ client.onclose = () => onDisconnect()
111
+ client.onerror = (err) => onDisconnect(err)
112
+
113
+ client.setNotificationHandler(ToolListChangedNotificationSchema, () => {
114
+ if (generation.closed || disposed) return
115
+ synchronization = synchronization.then(() => synchronizeTools(client, generation)).catch(error => {
116
+ ctx.logger.warn(`mcp-sse-client(${config.serverName}): tool refresh failed: ${messageOf(error)}`)
117
+ })
70
118
  })
71
- })
72
119
 
73
- try {
74
- await client.connect(transport)
75
- await synchronizeTools()
76
- } catch (error) {
77
- clearTools()
78
- await client.close().catch(() => {})
79
- if (config.failOnStartupError) throw new Error(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`, { cause: error })
80
- ctx.logger.warn(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`)
120
+ try {
121
+ await client.connect(transport)
122
+ await synchronizeTools(client, generation)
123
+ failedAttempts = 0
124
+ ctx.logger.info(`mcp-sse-client(${config.serverName}): connected and synchronized tools`)
125
+ } catch (error) {
126
+ onDisconnect(error)
127
+ if (startup && config.failOnStartupError) {
128
+ throw new Error(`mcp-sse-client(${config.serverName}): initial connection failed: ${messageOf(error)}`, { cause: error })
129
+ }
130
+ }
81
131
  }
132
+
133
+ ctx.effect(() => () => {
134
+ disposed = true
135
+ if (reconnectTimer !== undefined) {
136
+ clearTimeout(reconnectTimer)
137
+ reconnectTimer = undefined
138
+ }
139
+ clearTools()
140
+ if (currentGeneration && !currentGeneration.closed) {
141
+ currentGeneration.closed = true
142
+ void currentGeneration.client.close().catch(() => {})
143
+ }
144
+ }, 'mcp-sse-client.connection')
145
+
146
+ await connectGeneration(true)
82
147
  }
83
148
 
84
149
  function createTool(client, publicName, tool, timeout) {
package/src/store.js CHANGED
@@ -39,12 +39,13 @@ function validateId(id) {
39
39
  }
40
40
 
41
41
  function parsePatch(value) {
42
- if (!Array.isArray(value) || value.length !== 1 || !Array.isArray(value[0]?.insert)
43
- || Object.keys(value[0]).join() !== 'insert') throw new Error('Expected one managed insert operation.')
42
+ if (!Array.isArray(value)) throw new Error('Expected patch array.')
43
+ const insertOp = value.find(row => row && typeof row === 'object' && Array.isArray(row.insert))
44
+ if (!insertOp) throw new Error('Expected one managed insert operation.')
44
45
  const ids = new Set()
45
46
  const managed = []
46
47
  const preserved = []
47
- for (const row of value[0].insert) {
48
+ for (const row of insertOp.insert) {
48
49
  if (row?.id === 'extension-manager' && row.name === managerModule) {
49
50
  if (ids.has(row.id)) throw new Error('Duplicate extension id.')
50
51
  ids.add(row.id)
@@ -69,7 +70,8 @@ function parsePatch(value) {
69
70
  } else throw new Error('Unknown managed plugin module.')
70
71
  managed.push(row)
71
72
  }
72
- return { managed, preserved }
73
+ const unmanagedRows = value.filter(row => row !== insertOp)
74
+ return { managed, preserved, unmanagedRows }
73
75
  }
74
76
 
75
77
  /** Validate the patch and return only extension rows owned by the manager. */
@@ -84,7 +86,8 @@ function markedRows(content) {
84
86
  if (start < 0 || end < start || content.indexOf(blockStart, start + blockStart.length) >= 0
85
87
  || content.indexOf(blockEnd, end + blockEnd.length) >= 0) throw new Error('Invalid dsh-skill-mcp managed block.')
86
88
  const bodyStart = start + blockStart.length
87
- return { start, end: end + blockEnd.length, rows: validatePatch(parse(content.slice(bodyStart, end))) }
89
+ const parsed = parsePatch(parse(content.slice(bodyStart, end)))
90
+ return { start, end: end + blockEnd.length, rows: parsed.managed, unmanagedRows: parsed.unmanagedRows }
88
91
  }
89
92
 
90
93
  function legacyPatch(content) {
@@ -99,7 +102,10 @@ function renderBlock(rows) {
99
102
  function writeManagedRows(content, rows) {
100
103
  const marked = markedRows(content)
101
104
  const block = renderBlock(rows)
102
- if (marked) return `${content.slice(0, marked.start)}${block}${content.slice(marked.end)}`
105
+ if (marked) {
106
+ const unmanaged = marked.unmanagedRows?.length ? `\n${stringify(marked.unmanagedRows).trimEnd()}\n` : ''
107
+ return `${content.slice(0, marked.start)}${block}${unmanaged}${content.slice(marked.end)}`
108
+ }
103
109
  const legacy = legacyPatch(content)
104
110
  if (legacy) return `${JSON.stringify([{ insert: [...legacy.preserved, ...rows] }], null, 2)}\n`
105
111
  if (/^[ \t]*\[\][ \t]*$/m.test(content)) return content.replace(/^[ \t]*\[\][ \t]*$/m, block)
@@ -121,10 +127,28 @@ export class ExtensionStore {
121
127
  /** Migrate managed MCP rows so an unavailable server cannot reject profile startup. */
122
128
  async ensureNonFatalMcpStartup() {
123
129
  const rows = await this.read()
124
- if (!rows.some(row => row.config.serverName && row.config.failOnStartupError !== false)) return
130
+ let needsUpdate = false
131
+ for (const row of rows) {
132
+ if (row.config?.serverName) {
133
+ if (row.config.failOnStartupError !== false || !row.config.reconnect || row.config.reconnect.maxAttempts !== Number.MAX_SAFE_INTEGER) {
134
+ needsUpdate = true
135
+ break
136
+ }
137
+ }
138
+ }
139
+ if (!needsUpdate) return
125
140
  await this.update(current => {
126
141
  for (const row of current) {
127
- if (row.config.serverName) row.config.failOnStartupError = false
142
+ if (row.config?.serverName) {
143
+ row.config.failOnStartupError = false
144
+ const maxDelay = row.config.reconnect?.maxDelayMs ?? 7000
145
+ row.config.reconnect = {
146
+ enabled: true,
147
+ initialDelayMs: 500,
148
+ maxDelayMs: Math.min(60_000, Math.max(3_000, maxDelay)),
149
+ maxAttempts: Number.MAX_SAFE_INTEGER,
150
+ }
151
+ }
128
152
  }
129
153
  })
130
154
  }
@@ -195,11 +219,23 @@ export class ExtensionStore {
195
219
  normalizeMcp(id, input) {
196
220
  // Authentication values belong in the host credential setup, not tool arguments or logs.
197
221
  if (input.env || input.headers) throw new Error('This version does not accept environment values or authentication headers.')
222
+ const reconnectIntervalSec = Math.min(60, Math.max(3, Math.round(Number(input.reconnectInterval ?? 7)) || 7))
223
+ const reconnect = {
224
+ enabled: true,
225
+ initialDelayMs: 500,
226
+ maxDelayMs: reconnectIntervalSec * 1000,
227
+ maxAttempts: Number.MAX_SAFE_INTEGER,
228
+ }
198
229
  const normalized = input.transport === 'stdio' ? {
199
230
  ...input,
200
231
  command: typeof input.command === 'string' ? normalizeWindowsPath(input.command) : input.command,
201
232
  cwd: typeof input.cwd === 'string' && input.cwd ? normalizeWindowsPath(input.cwd) : input.cwd,
202
- } : input
233
+ reconnect,
234
+ } : {
235
+ ...input,
236
+ reconnect,
237
+ }
238
+ delete normalized.reconnectInterval
203
239
  const config = input.transport === 'sse'
204
240
  ? SseMcpConfig({ ...normalized, serverName: id, failOnStartupError: false })
205
241
  : McpConfig({ ...normalized, serverName: id, failOnStartupError: false })
package/src/web.js CHANGED
@@ -55,8 +55,18 @@ export async function describeExtensions(store, ctx, options = {}) {
55
55
  : row.config.customSkillDirs[0],
56
56
  transport: row.config.transport ?? '',
57
57
  configuration: row.config.serverName ? (row.config.transport === 'stdio'
58
- ? { transport: row.config.transport, command: row.config.command, args: row.config.args ?? [], cwd: row.config.cwd ?? '' }
59
- : { transport: row.config.transport, url: row.config.url }) : undefined,
58
+ ? {
59
+ transport: row.config.transport,
60
+ command: row.config.command,
61
+ args: row.config.args ?? [],
62
+ cwd: row.config.cwd ?? '',
63
+ reconnectInterval: row.config.reconnect?.maxDelayMs ? Math.round(row.config.reconnect.maxDelayMs / 1000) : 7,
64
+ }
65
+ : {
66
+ transport: row.config.transport,
67
+ url: row.config.url,
68
+ reconnectInterval: row.config.reconnect?.maxDelayMs ? Math.round(row.config.reconnect.maxDelayMs / 1000) : 7,
69
+ }) : undefined,
60
70
  tools: row.config.serverName
61
71
  ? toolNames.filter(name => name.startsWith(`mcp__${row.id}__`)).sort() : [],
62
72
  })),