@morehao/dsh-skills-console 0.5.4 → 0.5.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/client.js CHANGED
@@ -69,6 +69,15 @@ async function apiCall(method, path, body, options = {}) {
69
69
  return json;
70
70
  }
71
71
  var GITHUB_URL = "https://github.com/morehao/dsh-skills-console";
72
+ function checkPluginUpdate({ fresh = false } = {}) {
73
+ return apiCall("GET", `/plugin-update${fresh ? "?fresh=1" : ""}`);
74
+ }
75
+ function applyPluginUpdate() {
76
+ return apiCall("POST", "/plugin-update/apply", {});
77
+ }
78
+ function fetchPluginRelease(version) {
79
+ return apiCall("GET", `/plugin-update/release?version=${encodeURIComponent(version || "")}`);
80
+ }
72
81
 
73
82
  // client/copy.js
74
83
  var DICT = {
@@ -117,10 +126,33 @@ var DICT = {
117
126
  "placement.off": "已关闭",
118
127
  "placement.hint": "开启后侧栏出现「技能控制台」入口;关闭则只在「设置 → 技能」显示。",
119
128
  "github.open": "打开 GitHub 仓库",
129
+ "pluginUpdate.check": "检查插件更新",
130
+ "pluginUpdate.title": "检查插件更新",
131
+ "pluginUpdate.current": "当前版本",
132
+ "pluginUpdate.latest": "最新版本",
133
+ "pluginUpdate.checking": "正在检查…",
134
+ "pluginUpdate.upToDate": "已是最新版本",
135
+ "pluginUpdate.available": "有新版本可用",
136
+ "pluginUpdate.checkFailed": "暂时无法获取最新版本,请稍后再试",
137
+ "pluginUpdate.recheck": "重新检查",
138
+ "pluginUpdate.update": "更新到 {version}",
139
+ "pluginUpdate.applying": "正在更新…",
140
+ "pluginUpdate.updated": "已更新到 {version},重启 DSH 后生效",
141
+ "pluginUpdate.restartHint": "更新已安装,请重启 DSH 使新版本生效。",
142
+ "pluginUpdate.manual": "当前环境不支持一键更新,请手动执行:",
143
+ "pluginUpdate.copy": "复制命令",
144
+ "pluginUpdate.copied": "已复制",
145
+ "pluginUpdate.close": "关闭",
146
+ "pluginUpdate.whatsNew": "v{version} 更新内容",
147
+ "pluginUpdate.releaseLoading": "正在加载更新内容…",
148
+ "pluginUpdate.viewReleaseNotes": "在 GitHub 查看发布说明",
149
+ "pluginUpdate.releaseUnavailable": "暂无该版本的发布说明。",
120
150
  "status.current": "最新",
121
151
  "status.update": "可更新",
122
- "status.local": "本地已改",
123
- "status.conflict": "本地已改 + 上游已变",
152
+ "status.local": "安装后有改动",
153
+ "status.localHint": "安装或更新后,这个技能的文件被改过;再次更新会覆盖这些改动,会先请你确认。可在「查看 → 信息」里核对",
154
+ "status.conflict": "有改动 · 可更新",
155
+ "status.conflictHint": "本地文件有改动,上游也有新提交;更新会覆盖你的改动(旧内容有备份,可回滚)",
124
156
  "status.deleted": "上游已移除",
125
157
  "status.broken": "异常:软链断裂",
126
158
  "status.unmanaged": "未纳入追踪",
@@ -212,6 +244,7 @@ var DICT = {
212
244
  "form.subpath": "子目录(可空)",
213
245
  "form.hint": "会立即下载并写入 ~/.agents/skills,并按「新安装时自动共享」的选择共享至对应 Agent。",
214
246
  "form.subHint": "只记录你关注这个仓库,不会安装任何技能。",
247
+ "form.submitFailed": "添加失败,请重试",
215
248
  "subscription.defaultRef": "默认分支",
216
249
  "confirm.deleteTitle": "删除技能",
217
250
  "confirm.deleteBody": "会从本机移除,断开本插件建立的其他 Agent 链接并清理记录;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。",
@@ -295,9 +328,9 @@ var DICT = {
295
328
  "viewer.info.updatedAt": "更新",
296
329
  "viewer.info.size": "大小",
297
330
  "viewer.info.local": "本地",
298
- "viewer.info.localChange": "本地改动",
331
+ "viewer.info.localChange": "安装后改动",
299
332
  "viewer.info.localChangeNone": "无(与安装时一致)",
300
- "viewer.info.localChangeYes": "有本地改动",
333
+ "viewer.info.localChangeYes": "有改动",
301
334
  "viewer.info.links": "共享",
302
335
  "viewer.info.exported": "已共享",
303
336
  "viewer.info.visibility": "可见性",
@@ -324,6 +357,9 @@ var DICT = {
324
357
  "intro.error.timeout": "生成超时,请重试",
325
358
  "intro.error.auth": "模型提供方认证失败",
326
359
  "intro.error.empty": "模型没有返回内容",
360
+ "intro.error.truncated": "简介被输出上限截断,请重试",
361
+ "intro.error.reasoningOnly": "模型只输出了思考过程,没有输出正文;请提高输出上限或换非推理模型",
362
+ "intro.error.noText": "模型没有输出文字简介,请重试",
327
363
  "intro.error.busy": "正在生成,请稍候",
328
364
  "intro.error.source": "读不到远端的 SKILL.md(仓库私有、已删除或网络问题)",
329
365
  "intro.error.noSource": "订阅快照里没有这个技能,请先点「检查更新」",
@@ -376,10 +412,33 @@ var DICT = {
376
412
  "placement.off": "off",
377
413
  "placement.hint": "On: a Skills Console entry appears in the sidebar. Off: it only shows under Settings → Skills.",
378
414
  "github.open": "Open GitHub repository",
415
+ "pluginUpdate.check": "Check for plugin updates",
416
+ "pluginUpdate.title": "Check for plugin updates",
417
+ "pluginUpdate.current": "Current version",
418
+ "pluginUpdate.latest": "Latest version",
419
+ "pluginUpdate.checking": "Checking…",
420
+ "pluginUpdate.upToDate": "You are on the latest version",
421
+ "pluginUpdate.available": "A new version is available",
422
+ "pluginUpdate.checkFailed": "Could not reach the registry; please try again later",
423
+ "pluginUpdate.recheck": "Check again",
424
+ "pluginUpdate.update": "Update to {version}",
425
+ "pluginUpdate.applying": "Updating…",
426
+ "pluginUpdate.updated": "Updated to {version}; restart DSH to apply",
427
+ "pluginUpdate.restartHint": "The update is installed. Restart DSH to use the new version.",
428
+ "pluginUpdate.manual": "One-click update is unavailable here; run this manually:",
429
+ "pluginUpdate.copy": "Copy command",
430
+ "pluginUpdate.copied": "Copied",
431
+ "pluginUpdate.close": "Close",
432
+ "pluginUpdate.whatsNew": "What's new in v{version}",
433
+ "pluginUpdate.releaseLoading": "Loading release notes…",
434
+ "pluginUpdate.viewReleaseNotes": "View release notes on GitHub",
435
+ "pluginUpdate.releaseUnavailable": "No release notes for this version.",
379
436
  "status.current": "Up to date",
380
437
  "status.update": "Update available",
381
- "status.local": "Locally modified",
382
- "status.conflict": "Local + upstream changed",
438
+ "status.local": "Changed since install",
439
+ "status.localHint": "Files were changed after install or update. Updating overwrites them and asks for confirmation first. Check “View → Info” to compare",
440
+ "status.conflict": "Changed · update available",
441
+ "status.conflictHint": "Local files changed and upstream has new commits; updating overwrites your edits (old content is backed up and can be rolled back)",
383
442
  "status.deleted": "Removed upstream",
384
443
  "status.broken": "Broken link",
385
444
  "status.unmanaged": "Untracked",
@@ -470,6 +529,7 @@ var DICT = {
470
529
  "form.subpath": "Subdirectory (optional)",
471
530
  "form.hint": "Downloads into ~/.agents/skills and shares it with agents per the auto-share setting.",
472
531
  "form.subHint": "Only records the repository; installs nothing.",
532
+ "form.submitFailed": "Failed; please try again",
473
533
  "subscription.defaultRef": "default branch",
474
534
  "confirm.deleteTitle": "Delete skill",
475
535
  "confirm.deleteBody": "Removes it locally, disconnects the agent links this plugin created, and clears the record. A snapshot stays in trash for 7 days. Your source repo is untouched.",
@@ -554,9 +614,9 @@ var DICT = {
554
614
  "viewer.info.updatedAt": "Updated",
555
615
  "viewer.info.size": "Size",
556
616
  "viewer.info.local": "Local",
557
- "viewer.info.localChange": "Local changes",
617
+ "viewer.info.localChange": "Changes since install",
558
618
  "viewer.info.localChangeNone": "None (matches install)",
559
- "viewer.info.localChangeYes": "Modified locally",
619
+ "viewer.info.localChangeYes": "Changed",
560
620
  "viewer.info.links": "Shared",
561
621
  "viewer.info.exported": "Shared",
562
622
  "viewer.info.visibility": "Visibility",
@@ -583,6 +643,9 @@ var DICT = {
583
643
  "intro.error.timeout": "Generation timed out; try again",
584
644
  "intro.error.auth": "The model provider rejected the credentials",
585
645
  "intro.error.empty": "The model returned no content",
646
+ "intro.error.truncated": "The intro hit the output limit and was truncated; try again",
647
+ "intro.error.reasoningOnly": "The model only produced reasoning, not intro text; raise the output limit or use a non-reasoning model",
648
+ "intro.error.noText": "The model returned no text intro; try again",
586
649
  "intro.error.busy": "Already generating; please wait",
587
650
  "intro.error.source": "Could not read the remote SKILL.md (private repo, deleted, or network)",
588
651
  "intro.error.noSource": "This skill is not in the subscription snapshot; run “Check for updates” first",
@@ -1040,7 +1103,7 @@ function FolderIcon({ size = 12 }) {
1040
1103
  }
1041
1104
 
1042
1105
  // client/section.js
1043
- var import_react9 = __toESM(require("react"), 1);
1106
+ var import_react10 = __toESM(require("react"), 1);
1044
1107
 
1045
1108
  // client/scroll.js
1046
1109
  function isScroller(node, tolerance = 4) {
@@ -1097,6 +1160,7 @@ var CSS = `
1097
1160
  /* 标题不参与收窄:窄面板里让身份胶囊先省略,标题本身不折行 */
1098
1161
  .dshsc-title { font-size: 17px; font-weight: 600; letter-spacing: -.01em; margin: 0; flex: none; white-space: nowrap; }
1099
1162
  .dshsc-sub { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); margin: 0; max-width: 64ch; }
1163
+ .dshsc-form-error { font-size: 12px; line-height: 1.55; margin: 8px 0 0; max-width: 64ch; color: var(--dsw-alias-state-error-primary); }
1100
1164
  .dshsc-spacer { flex: 1; }
1101
1165
  .dshsc-head-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; margin-left: auto; }
1102
1166
  /* 标题行恒为一行:身份胶囊只在面板极窄(连标题行都放不下)时才省略,工具组不被挤走。
@@ -1121,6 +1185,10 @@ var CSS = `
1121
1185
  .dshsc-tool:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1122
1186
  .dshsc-tool:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
1123
1187
  .dshsc-tool[aria-expanded="true"] { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
1188
+ /* 刷新按钮:检查到新版本时右上角点个品牌色圆点 */
1189
+ .dshsc-tool-dot { position: relative; color: var(--dsw-alias-brand-primary); }
1190
+ .dshsc-tool-dot::after { content: ""; position: absolute; top: 3px; right: 3px; width: 6px; height: 6px;
1191
+ border-radius: 50%; background: var(--dsw-alias-state-warning-primary, #e8b15a); box-shadow: 0 0 0 1.5px var(--dsw-alias-bg-base); }
1124
1192
  .dshsc-settings { position: relative; display: inline-flex; }
1125
1193
  /* 设置面板留在 Host 对话框内(焦点陷阱 / 层级都跟着走);开启时按左右余量选边和宽度 */
1126
1194
  .dshsc-pop { position: absolute; top: calc(100% + 8px); z-index: 30; box-sizing: border-box;
@@ -1483,6 +1551,10 @@ var CSS = `
1483
1551
  /* 64ch 是主页面长文的舒适行长;弹窗说明只有一两句,用满弹窗宽度,避免没必要的折行 */
1484
1552
  .dshsc-modal .dshsc-sub { max-width: none; }
1485
1553
  .dshsc-modal h3 { margin: 0; font-size: 15px; font-weight: 600; padding-right: 26px; }
1554
+ /* 插件更新弹窗的「当前 / 最新」键值对 */
1555
+ .dshsc-kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 12px; margin: 0; font-size: 12px; align-items: baseline; }
1556
+ .dshsc-kv dt { color: var(--dsw-alias-label-secondary); }
1557
+ .dshsc-kv dd { margin: 0; }
1486
1558
  .dshsc-modal-close { position: absolute; top: 10px; right: 10px; width: 26px; height: 26px; border: 0; border-radius: 7px;
1487
1559
  background: transparent; color: var(--dsw-alias-label-secondary); font-size: 18px; line-height: 1; cursor: pointer; }
1488
1560
  .dshsc-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
@@ -1532,6 +1604,14 @@ var CSS = `
1532
1604
  .dshsc-panel .dshsc-check.locked:hover { background: transparent; }
1533
1605
 
1534
1606
  .dshsc-modal-foot { display: flex; justify-content: flex-end; gap: 8px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); }
1607
+ /* 弹窗页脚按钮可能是「图标 + 文字」:统一成水平排列,避免 svg(display:block) 把文字挤到下一行 */
1608
+ .dshsc-modal-foot .dshsc-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; }
1609
+ .dshsc-modal-foot .dshsc-btn svg { display: block; }
1610
+ /* 插件更新弹窗里的「更新内容」(GitHub Release 正文):限高滚动,长说明不撑爆弹窗 */
1611
+ .dshsc-release-head { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); margin-bottom: 6px; }
1612
+ .dshsc-release-body { max-height: 260px; overflow: auto; padding: 10px 12px; border-radius: 10px;
1613
+ background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); }
1614
+ .dshsc-release-body .dshsc-md { font-size: 12.5px; }
1535
1615
  /* ---------- 查看器:抽屉(B1)/ 整页(A1)共用 ---------- */
1536
1616
  .dshsc-drawer-wrap { position: fixed; inset: 0; z-index: 55; }
1537
1617
  .dshsc-drawer-scrim { position: absolute; inset: 0; background: rgba(4,6,9,.55); }
@@ -1913,26 +1993,38 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
1913
1993
  return apiCall("POST", "/probe", { source: String(source || "").trim() });
1914
1994
  }
1915
1995
  async function installSkills({ source, skills }) {
1916
- setModal(null);
1917
- await run("install", async () => {
1918
- const result = await apiCall("POST", "/install", { source: String(source || "").trim(), skills });
1919
- const warnings = result.warnings?.length || 0;
1920
- setToast({
1921
- kind: warnings ? "err" : "ok",
1922
- text: `${result.installed?.length || 0} ${t("skillCount")}${warnings ? ` · ${warnings} 条提示` : ""}`
1923
- });
1924
- });
1996
+ try {
1997
+ await run("install", async () => {
1998
+ const result = await apiCall("POST", "/install", { source: String(source || "").trim(), skills });
1999
+ const warnings = result.warnings?.length || 0;
2000
+ setToast({
2001
+ kind: warnings ? "err" : "ok",
2002
+ text: `${result.installed?.length || 0} ${t("skillCount")}${warnings ? ` · ${warnings} 条提示` : ""}`
2003
+ });
2004
+ return result;
2005
+ }, { toastError: false });
2006
+ setModal(null);
2007
+ return { ok: true };
2008
+ } catch (err) {
2009
+ return { ok: false, error: err?.message || String(err) };
2010
+ }
1925
2011
  }
1926
2012
  async function addSubscription(form) {
1927
- setModal(null);
1928
- await run("subscribe", async () => {
1929
- const result = await apiCall("POST", "/subscriptions", {
1930
- source: form.source.trim(),
1931
- ref: form.ref.trim() || void 0,
1932
- subpath: form.subpath.trim() || void 0
1933
- });
1934
- setToast({ kind: "ok", text: result.subscription?.source || form.source });
1935
- });
2013
+ try {
2014
+ const result = await run("subscribe", async () => {
2015
+ const res = await apiCall("POST", "/subscriptions", {
2016
+ source: form.source.trim(),
2017
+ ref: form.ref.trim() || void 0,
2018
+ subpath: form.subpath.trim() || void 0
2019
+ });
2020
+ setToast({ kind: "ok", text: res.subscription?.source || form.source });
2021
+ return res;
2022
+ }, { toastError: false });
2023
+ setModal(null);
2024
+ return { ok: true, result };
2025
+ } catch (err) {
2026
+ return { ok: false, error: err?.message || String(err) };
2027
+ }
1936
2028
  }
1937
2029
  async function refreshSubscription(row) {
1938
2030
  await run(`refresh:${row.id}`, async () => {
@@ -1999,14 +2091,14 @@ function useSkillsData() {
1999
2091
  return () => clearTimeout(timer);
2000
2092
  }, [toast]);
2001
2093
  const run = import_react2.default.useCallback(
2002
- async (label, task) => {
2094
+ async (label, task, { toastError = true } = {}) => {
2003
2095
  setBusy(label);
2004
2096
  try {
2005
2097
  const result = await task();
2006
2098
  await reload();
2007
2099
  return result;
2008
2100
  } catch (err) {
2009
- setToast({ kind: "err", text: err.message });
2101
+ if (toastError) setToast({ kind: "err", text: err.message });
2010
2102
  throw err;
2011
2103
  } finally {
2012
2104
  setBusy(null);
@@ -2254,6 +2346,64 @@ function useSkillIntro({ name, lang, stamp, enabled = true, endpoint = null }) {
2254
2346
  }, [name, lang, base]);
2255
2347
  return { ...state, generating, genError, generate };
2256
2348
  }
2349
+ function usePluginUpdate() {
2350
+ const [status, setStatus] = import_react2.default.useState(null);
2351
+ const [checking, setChecking] = import_react2.default.useState(false);
2352
+ const [applying, setApplying] = import_react2.default.useState(false);
2353
+ const [checkError, setCheckError] = import_react2.default.useState(null);
2354
+ const [applyError, setApplyError] = import_react2.default.useState(null);
2355
+ const [release, setRelease] = import_react2.default.useState({ loading: false, data: null, version: null });
2356
+ const inFlight = import_react2.default.useRef(false);
2357
+ const check = import_react2.default.useCallback(async (fresh = false) => {
2358
+ if (inFlight.current) return null;
2359
+ inFlight.current = true;
2360
+ setChecking(true);
2361
+ setCheckError(null);
2362
+ try {
2363
+ const next = await checkPluginUpdate({ fresh });
2364
+ setStatus(next);
2365
+ return next;
2366
+ } catch (error) {
2367
+ setCheckError({ code: error?.code || null, message: error?.message || "检查失败" });
2368
+ return null;
2369
+ } finally {
2370
+ inFlight.current = false;
2371
+ setChecking(false);
2372
+ }
2373
+ }, []);
2374
+ const apply2 = import_react2.default.useCallback(async () => {
2375
+ setApplying(true);
2376
+ setApplyError(null);
2377
+ try {
2378
+ const next = await applyPluginUpdate();
2379
+ setStatus(next);
2380
+ return next;
2381
+ } catch (error) {
2382
+ setApplyError({
2383
+ code: error?.code || null,
2384
+ message: error?.message || "更新失败",
2385
+ detail: error?.detail || null
2386
+ });
2387
+ return null;
2388
+ } finally {
2389
+ setApplying(false);
2390
+ }
2391
+ }, []);
2392
+ const loadRelease = import_react2.default.useCallback(async (version) => {
2393
+ const target = String(version || "").trim();
2394
+ if (!target) return null;
2395
+ setRelease({ loading: true, data: null, version: target });
2396
+ try {
2397
+ const data = await fetchPluginRelease(target);
2398
+ setRelease({ loading: false, data: data || null, version: target });
2399
+ return data;
2400
+ } catch {
2401
+ setRelease({ loading: false, data: null, version: target });
2402
+ return null;
2403
+ }
2404
+ }, []);
2405
+ return { status, checking, applying, checkError, applyError, check, apply: apply2, release, loadRelease };
2406
+ }
2257
2407
 
2258
2408
  // client/views/header.js
2259
2409
  function installSpec(plugin) {
@@ -2275,6 +2425,8 @@ function Header({
2275
2425
  placement,
2276
2426
  placementBusy,
2277
2427
  readOnly,
2428
+ pluginUpdate,
2429
+ onCheckUpdate,
2278
2430
  onToggleSettings,
2279
2431
  onChangePlacement,
2280
2432
  onOpenAutoLink,
@@ -2282,9 +2434,18 @@ function Header({
2282
2434
  onAddSubscription
2283
2435
  }) {
2284
2436
  const spec = installSpec(plugin);
2437
+ const updateAvailable = Boolean(pluginUpdate?.status?.updateAvailable);
2285
2438
  const headTools = h(
2286
2439
  "div",
2287
2440
  { className: "dshsc-tools" },
2441
+ // 刷新按钮放在 GitHub 前面:点一下检查插件自身版本,有新版本时高亮提示
2442
+ h("button", {
2443
+ className: `dshsc-tool${updateAvailable ? " dshsc-tool-dot" : ""}`,
2444
+ type: "button",
2445
+ title: t("pluginUpdate.check"),
2446
+ "aria-label": t("pluginUpdate.check"),
2447
+ onClick: onCheckUpdate
2448
+ }, h(RefreshIcon, { size: 15 })),
2288
2449
  // 先源码、后设置:GitHub 紧挨包名,齿轮留在工具组最外侧
2289
2450
  h("a", {
2290
2451
  className: "dshsc-tool",
@@ -2450,12 +2611,13 @@ function Toolbar({
2450
2611
  var import_react4 = __toESM(require("react"), 1);
2451
2612
 
2452
2613
  // client/views/overlay.js
2453
- function Overlay({ t, title, children, footer, onCancel }) {
2454
- useOverlayLayer(true, onCancel);
2614
+ function Overlay({ t, title, children, footer, onCancel, dismissible = true }) {
2615
+ useOverlayLayer(true, dismissible ? onCancel : () => {
2616
+ });
2455
2617
  return portalize(h(
2456
2618
  "div",
2457
2619
  { className: "dshsc-overlay", onClick: (event) => {
2458
- if (event.target === event.currentTarget) onCancel?.();
2620
+ if (dismissible && event.target === event.currentTarget) onCancel?.();
2459
2621
  } },
2460
2622
  h(
2461
2623
  "div",
@@ -2463,9 +2625,10 @@ function Overlay({ t, title, children, footer, onCancel }) {
2463
2625
  h("button", {
2464
2626
  className: "dshsc-modal-close",
2465
2627
  "aria-label": t("action.cancel"),
2628
+ disabled: !dismissible,
2466
2629
  onClick: (event) => {
2467
2630
  event.stopPropagation();
2468
- onCancel?.();
2631
+ if (dismissible) onCancel?.();
2469
2632
  }
2470
2633
  }, "×"),
2471
2634
  h("h3", null, title),
@@ -2484,6 +2647,9 @@ var ERROR_KEY = {
2484
2647
  "error.skills.intro-timeout": "intro.error.timeout",
2485
2648
  "error.skills.intro-auth": "intro.error.auth",
2486
2649
  "error.skills.intro-empty": "intro.error.empty",
2650
+ "error.skills.intro-truncated": "intro.error.truncated",
2651
+ "error.skills.intro-reasoning-only": "intro.error.reasoningOnly",
2652
+ "error.skills.intro-no-text": "intro.error.noText",
2487
2653
  "error.skills.intro-busy": "intro.error.busy",
2488
2654
  "error.skills.intro-source": "intro.error.source",
2489
2655
  "error.skills.subscription-not-found": "intro.error.subscription",
@@ -2805,14 +2971,17 @@ function InstalledTab({
2805
2971
  const map = {
2806
2972
  current: ["status.current", "ok", true],
2807
2973
  "update-available": ["status.update", "warn"],
2808
- "locally-modified": ["status.local", "warn"],
2809
- conflict: ["status.conflict", "warn"],
2974
+ "locally-modified": ["status.local", "warn", false, "status.localHint"],
2975
+ conflict: ["status.conflict", "warn", false, "status.conflictHint"],
2810
2976
  "upstream-deleted": ["status.deleted", "warn"],
2811
2977
  "broken-link": ["status.broken", "err"],
2812
2978
  unmanaged: ["status.unmanaged", ""]
2813
2979
  };
2814
- const [key, cls, check] = map[row.status] || ["status.unknown", ""];
2815
- return h("span", { className: `dshsc-tag ${cls}` }, check ? `✓ ${t(key)}` : t(key));
2980
+ const [key, cls, check, hint] = map[row.status] || ["status.unknown", ""];
2981
+ return h("span", {
2982
+ className: `dshsc-tag ${cls}`,
2983
+ title: hint ? t(hint) : void 0
2984
+ }, check ? `✓ ${t(key)}` : t(key));
2816
2985
  }
2817
2986
  function sourceLine(row) {
2818
2987
  if (!row.source) {
@@ -3471,6 +3640,8 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3471
3640
  const [manual, setManual] = import_react6.default.useState("");
3472
3641
  const [skipped, setSkipped] = import_react6.default.useState(0);
3473
3642
  const [error, setError] = import_react6.default.useState(null);
3643
+ const [pending, setPending] = import_react6.default.useState(false);
3644
+ const [submitError, setSubmitError] = import_react6.default.useState(null);
3474
3645
  const reset = (value) => {
3475
3646
  setSource(value);
3476
3647
  setPhase("idle");
@@ -3478,12 +3649,14 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3478
3649
  setSelected(/* @__PURE__ */ new Set());
3479
3650
  setSkipped(0);
3480
3651
  setError(null);
3652
+ setSubmitError(null);
3481
3653
  };
3482
3654
  const probe = async () => {
3483
3655
  const value = source.trim();
3484
- if (!value || phase === "probing") return;
3656
+ if (!value || phase === "probing" || pending) return;
3485
3657
  setPhase("probing");
3486
3658
  setError(null);
3659
+ setSubmitError(null);
3487
3660
  try {
3488
3661
  const result = await onProbe(value);
3489
3662
  const found = Array.isArray(result?.skills) ? result.skills : [];
@@ -3496,20 +3669,33 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3496
3669
  setPhase("failed");
3497
3670
  }
3498
3671
  };
3499
- const toggle = (name) => setSelected((prev) => {
3500
- const next = new Set(prev);
3501
- if (next.has(name)) next.delete(name);
3502
- else next.add(name);
3503
- return next;
3504
- });
3672
+ const toggle = (name) => {
3673
+ setSubmitError(null);
3674
+ setSelected((prev) => {
3675
+ const next = new Set(prev);
3676
+ if (next.has(name)) next.delete(name);
3677
+ else next.add(name);
3678
+ return next;
3679
+ });
3680
+ };
3505
3681
  const names = manual.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
3506
3682
  const skippedText = skipped > 0 ? t("form.skipped").replace("{n}", String(skipped)) : null;
3507
3683
  const manualMode = phase === "empty" || phase === "failed";
3508
3684
  const skills = phase === "picked" ? [...selected] : names;
3509
3685
  const canSubmit = phase === "picked" ? selected.size > 0 : manualMode ? names.length > 0 : false;
3510
- const submit = () => {
3511
- if (!canSubmit) return;
3512
- onInstall({ source: source.trim(), skills });
3686
+ const submit = async () => {
3687
+ if (!canSubmit || pending) return;
3688
+ setPending(true);
3689
+ setSubmitError(null);
3690
+ let res;
3691
+ try {
3692
+ res = await onInstall({ source: source.trim(), skills });
3693
+ } catch (err) {
3694
+ res = { ok: false, error: err?.message || String(err) };
3695
+ }
3696
+ if (res?.ok) return;
3697
+ setPending(false);
3698
+ setSubmitError(res?.error || t("form.submitFailed"));
3513
3699
  };
3514
3700
  return h(
3515
3701
  Overlay,
@@ -3517,14 +3703,16 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3517
3703
  t,
3518
3704
  title: t("action.addSkill"),
3519
3705
  onCancel,
3706
+ // 提交安装进行中禁止关闭:请求不可取消,允许关闭只是假装取消
3707
+ dismissible: !pending,
3520
3708
  footer: [
3521
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
3709
+ h("button", { key: "c", className: "dshsc-btn ghost", disabled: pending, onClick: onCancel }, t("action.cancel")),
3522
3710
  h("button", {
3523
3711
  key: "s",
3524
3712
  className: "dshsc-btn primary",
3525
- disabled: !canSubmit,
3526
- onClick: submit
3527
- }, t("action.save"))
3713
+ disabled: !canSubmit || pending,
3714
+ onClick: () => void submit()
3715
+ }, pending ? t("busy.working") : t("action.save"))
3528
3716
  ]
3529
3717
  },
3530
3718
  h("p", { className: "dshsc-sub" }, t("form.pickHint")),
@@ -3538,18 +3726,20 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3538
3726
  h("input", {
3539
3727
  value: source,
3540
3728
  placeholder: t("form.sourceHint"),
3729
+ disabled: pending,
3541
3730
  onChange: (event) => reset(event.target.value),
3542
3731
  onKeyDown: (event) => {
3543
- if (event.key === "Enter") void probe();
3732
+ if (event.key === "Enter" && !pending) void probe();
3544
3733
  }
3545
3734
  }),
3546
3735
  h("button", {
3547
3736
  className: "dshsc-btn",
3548
- disabled: !source.trim() || phase === "probing",
3737
+ disabled: !source.trim() || phase === "probing" || pending,
3549
3738
  onClick: () => void probe()
3550
3739
  }, phase === "probing" ? t("form.probing") : t("form.probe"))
3551
3740
  )
3552
3741
  ),
3742
+ submitError ? h("p", { className: "dshsc-form-error", role: "alert" }, submitError) : null,
3553
3743
  phase === "picked" ? h(
3554
3744
  "div",
3555
3745
  { key: "picked" },
@@ -3559,11 +3749,19 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3559
3749
  { style: { display: "flex", gap: 6, marginBottom: 6 } },
3560
3750
  h("button", {
3561
3751
  className: "dshsc-btn ghost",
3562
- onClick: () => setSelected(new Set(options.map((item) => item.name)))
3752
+ disabled: pending,
3753
+ onClick: () => {
3754
+ setSubmitError(null);
3755
+ setSelected(new Set(options.map((item) => item.name)));
3756
+ }
3563
3757
  }, t("form.selectAll")),
3564
3758
  h("button", {
3565
3759
  className: "dshsc-btn ghost",
3566
- onClick: () => setSelected(/* @__PURE__ */ new Set())
3760
+ disabled: pending,
3761
+ onClick: () => {
3762
+ setSubmitError(null);
3763
+ setSelected(/* @__PURE__ */ new Set());
3764
+ }
3567
3765
  }, t("form.selectNone"))
3568
3766
  ),
3569
3767
  h("div", { className: "dshsc-panel" }, options.map((option) => h(
@@ -3572,6 +3770,7 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3572
3770
  h("input", {
3573
3771
  type: "checkbox",
3574
3772
  checked: selected.has(option.name),
3773
+ disabled: pending,
3575
3774
  onChange: () => toggle(option.name)
3576
3775
  }),
3577
3776
  h(
@@ -3596,7 +3795,11 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3596
3795
  h("input", {
3597
3796
  value: manual,
3598
3797
  placeholder: "git-kit, handoff",
3599
- onChange: (event) => setManual(event.target.value)
3798
+ disabled: pending,
3799
+ onChange: (event) => {
3800
+ setManual(event.target.value);
3801
+ if (submitError) setSubmitError(null);
3802
+ }
3600
3803
  })
3601
3804
  )
3602
3805
  ) : null,
@@ -3607,23 +3810,41 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
3607
3810
  // client/views/modals.js
3608
3811
  function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
3609
3812
  const [form, setForm] = import_react7.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
3813
+ const [pending, setPending] = import_react7.default.useState(false);
3814
+ const [error, setError] = import_react7.default.useState(null);
3610
3815
  const incomplete = fields.some(
3611
3816
  (field) => field.required !== false && !String(form[field.key] || "").trim()
3612
3817
  );
3818
+ const submit = async () => {
3819
+ if (incomplete || pending) return;
3820
+ setPending(true);
3821
+ setError(null);
3822
+ let res;
3823
+ try {
3824
+ res = await onSubmit(form);
3825
+ } catch (err) {
3826
+ res = { ok: false, error: err?.message || String(err) };
3827
+ }
3828
+ if (res?.ok) return;
3829
+ setPending(false);
3830
+ setError(res?.error || t("form.submitFailed"));
3831
+ };
3613
3832
  return h(
3614
3833
  Overlay,
3615
3834
  {
3616
3835
  t,
3617
3836
  title,
3618
3837
  onCancel,
3838
+ // 提交进行中禁止关闭:请求不可取消,允许关闭只是假装取消
3839
+ dismissible: !pending,
3619
3840
  footer: [
3620
- h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
3841
+ h("button", { key: "c", className: "dshsc-btn ghost", disabled: pending, onClick: onCancel }, t("action.cancel")),
3621
3842
  h("button", {
3622
3843
  key: "s",
3623
3844
  className: "dshsc-btn primary",
3624
- disabled: incomplete,
3625
- onClick: () => onSubmit(form)
3626
- }, t("action.save"))
3845
+ disabled: incomplete || pending,
3846
+ onClick: () => void submit()
3847
+ }, pending ? t("busy.working") : t("action.save"))
3627
3848
  ]
3628
3849
  },
3629
3850
  hint ? h("p", { className: "dshsc-sub" }, hint) : null,
@@ -3634,9 +3855,14 @@ function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
3634
3855
  h("input", {
3635
3856
  value: form[field.key],
3636
3857
  placeholder: field.placeholder,
3637
- onChange: (event) => setForm((prev) => ({ ...prev, [field.key]: event.target.value }))
3858
+ disabled: pending,
3859
+ onChange: (event) => {
3860
+ setForm((prev) => ({ ...prev, [field.key]: event.target.value }));
3861
+ if (error) setError(null);
3862
+ }
3638
3863
  })
3639
- ))
3864
+ )),
3865
+ error ? h("p", { className: "dshsc-form-error", role: "alert" }, error) : null
3640
3866
  );
3641
3867
  }
3642
3868
  function ConfirmModal({ t, title, body, onCancel, onConfirm, danger, confirmLabel, hideCancel = false }) {
@@ -3751,7 +3977,7 @@ function Modals({ t, modal, data, autoLinkAgents, actions, onClose, onSaveAutoLi
3751
3977
  { key: "subpath", label: t("form.subpath"), placeholder: "skills", required: false }
3752
3978
  ],
3753
3979
  onCancel: onClose,
3754
- onSubmit: (form) => void actions.addSubscription(form)
3980
+ onSubmit: (form) => actions.addSubscription(form)
3755
3981
  });
3756
3982
  }
3757
3983
  if (modal.type === "exports") {
@@ -4281,6 +4507,10 @@ var STATUS_KEY = {
4281
4507
  "broken-link": "status.broken",
4282
4508
  unmanaged: "status.unmanaged"
4283
4509
  };
4510
+ var STATUS_HINT = {
4511
+ "locally-modified": "status.localHint",
4512
+ conflict: "status.conflictHint"
4513
+ };
4284
4514
  function TabButton({ id, label, count, active, onSelect }) {
4285
4515
  return h("button", {
4286
4516
  type: "button",
@@ -4474,7 +4704,10 @@ function SkillReader({
4474
4704
  "div",
4475
4705
  { className: "dshsc-reader-title" },
4476
4706
  h("h2", { id: "dshsc-reader-name", tabIndex: -1 }, name || t("viewer.title")),
4477
- row?.status && STATUS_KEY[row.status] ? h("span", { className: `dshsc-tag ${row.status === "current" ? "ok" : "warn"}` }, t(STATUS_KEY[row.status])) : null,
4707
+ row?.status && STATUS_KEY[row.status] ? h("span", {
4708
+ className: `dshsc-tag ${row.status === "current" ? "ok" : "warn"}`,
4709
+ title: STATUS_HINT[row.status] ? t(STATUS_HINT[row.status]) : void 0
4710
+ }, t(STATUS_KEY[row.status])) : null,
4478
4711
  row?.enabled === false ? h("span", { className: "dshsc-tag off" }, t("status.off")) : null,
4479
4712
  variant === "drawer" ? h("button", {
4480
4713
  type: "button",
@@ -4848,25 +5081,122 @@ function SkillReader({
4848
5081
  ), overlayHost());
4849
5082
  }
4850
5083
 
5084
+ // client/views/plugin-update-modal.js
5085
+ var import_react9 = __toESM(require("react"), 1);
5086
+ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
5087
+ const { status, checking, applying, checkError, applyError, check, apply: apply2, release, loadRelease } = update;
5088
+ const [copied, setCopied] = import_react9.default.useState(false);
5089
+ import_react9.default.useEffect(() => {
5090
+ void check();
5091
+ }, [check]);
5092
+ const latest = status?.latestVersion || null;
5093
+ const current = status?.currentVersion || "—";
5094
+ const available = Boolean(status?.updateAvailable);
5095
+ const canAuto = status?.canAutoUpdate !== false;
5096
+ const manual = status?.manualCommand || "";
5097
+ const restartRequired = status?.restartRequired === true;
5098
+ const updatedVersion = status?.updatedVersion || null;
5099
+ import_react9.default.useEffect(() => {
5100
+ if (available && latest) void loadRelease(latest);
5101
+ }, [available, latest, loadRelease]);
5102
+ const releaseData = release?.version === latest ? release.data : null;
5103
+ const releaseLoading = release?.version === latest && release.loading;
5104
+ const releaseBody = typeof releaseData?.body === "string" ? releaseData.body.trim() : "";
5105
+ const releaseUrl = releaseData?.htmlUrl || (repoUrl ? `${repoUrl}/releases` : null);
5106
+ const copy = () => {
5107
+ if (!manual || typeof navigator === "undefined" || !navigator.clipboard?.writeText) return;
5108
+ Promise.resolve(navigator.clipboard.writeText(manual)).then(() => {
5109
+ setCopied(true);
5110
+ setTimeout(() => setCopied(false), 1500);
5111
+ }).catch(() => {
5112
+ });
5113
+ };
5114
+ let stateLine;
5115
+ if (checking && !status) stateLine = t("pluginUpdate.checking");
5116
+ else if (checkError || status?.checkFailed) stateLine = t("pluginUpdate.checkFailed");
5117
+ else if (restartRequired) stateLine = t("pluginUpdate.updated", { version: updatedVersion || latest });
5118
+ else if (available) stateLine = t("pluginUpdate.available");
5119
+ else if (status) stateLine = t("pluginUpdate.upToDate");
5120
+ else stateLine = t("pluginUpdate.checking");
5121
+ const stateTone = checkError || status?.checkFailed ? "err" : available ? "update" : "ok";
5122
+ const footer = [
5123
+ h("button", {
5124
+ key: "check",
5125
+ className: "dshsc-btn ghost",
5126
+ disabled: checking || applying,
5127
+ onClick: () => void check(true)
5128
+ }, h(RefreshIcon, { size: 13 }), t("pluginUpdate.recheck"))
5129
+ ];
5130
+ if (available && canAuto) {
5131
+ footer.push(h("button", {
5132
+ key: "apply",
5133
+ className: "dshsc-btn primary",
5134
+ disabled: applying,
5135
+ onClick: () => void apply2()
5136
+ }, applying ? t("pluginUpdate.applying") : t("pluginUpdate.update", { version: latest })));
5137
+ }
5138
+ footer.push(h("button", { key: "close", className: "dshsc-btn ghost", onClick: onClose }, t("pluginUpdate.close")));
5139
+ return h(
5140
+ Overlay,
5141
+ { t, title: t("pluginUpdate.title"), onCancel: onClose, footer },
5142
+ h(
5143
+ "dl",
5144
+ { className: "dshsc-kv" },
5145
+ h("dt", null, t("pluginUpdate.current")),
5146
+ h("dd", null, h("span", { className: "dshsc-code" }, current)),
5147
+ h("dt", null, t("pluginUpdate.latest")),
5148
+ h("dd", null, h("span", { className: "dshsc-code" }, latest || "—"))
5149
+ ),
5150
+ h("p", { className: "dshsc-sub", "data-kind": stateTone, role: "status", style: stateTone === "err" ? { color: "var(--dsw-alias-state-error-primary)" } : null }, stateLine),
5151
+ // 有新版本:展示该版本的发布说明(拉了 GitHub Release 正文就渲染,拉不到给外链)
5152
+ available ? h(
5153
+ "div",
5154
+ { className: "dshsc-release" },
5155
+ h("div", { className: "dshsc-release-head" }, t("pluginUpdate.whatsNew", { version: latest })),
5156
+ releaseLoading ? h("p", { className: "dshsc-sub" }, t("pluginUpdate.releaseLoading")) : releaseBody ? h("div", { className: "dshsc-release-body" }, h(Markdown, { text: releaseBody })) : h(
5157
+ "p",
5158
+ { className: "dshsc-sub" },
5159
+ releaseUrl ? h("a", { className: "dshsc-md-link", href: releaseUrl, target: "_blank", rel: "noopener noreferrer" }, t("pluginUpdate.viewReleaseNotes")) : t("pluginUpdate.releaseUnavailable")
5160
+ )
5161
+ ) : null,
5162
+ applyError ? h("p", { className: "dshsc-sub", role: "alert", style: { color: "var(--dsw-alias-state-error-primary)" } }, applyError.message) : null,
5163
+ restartRequired ? h("p", { className: "dshsc-sub" }, t("pluginUpdate.restartHint")) : null,
5164
+ // 一键更新不可用(或本次安装失败后想手动兜底):给命令 + 复制
5165
+ !canAuto && manual ? h(
5166
+ "div",
5167
+ { className: "dshsc-field" },
5168
+ h("span", null, t("pluginUpdate.manual")),
5169
+ h(
5170
+ "div",
5171
+ { className: "dshsc-inline", style: { display: "flex", gap: 8, alignItems: "center" } },
5172
+ h("code", { className: "dshsc-code", style: { flex: 1, wordBreak: "break-all" } }, manual),
5173
+ h("button", { className: "dshsc-btn ghost", onClick: copy }, copied ? t("pluginUpdate.copied") : t("pluginUpdate.copy"))
5174
+ )
5175
+ ) : null
5176
+ );
5177
+ }
5178
+
4851
5179
  // client/section.js
4852
5180
  var UPDATABLE2 = /* @__PURE__ */ new Set(["update-available", "locally-modified", "conflict"]);
4853
5181
  function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4854
5182
  const { data, error, busy, toast, setToast, setBusy, reload, run } = useSkillsData();
4855
- const [tab, setTab] = import_react9.default.useState("installed");
4856
- const [view, setView] = import_react9.default.useState("flat");
4857
- const [modal, setModal] = import_react9.default.useState(null);
4858
- const [expanded, setExpanded] = import_react9.default.useState({});
4859
- const [placementBusy, setPlacementBusy] = import_react9.default.useState(null);
4860
- const [placementOverride, setPlacementOverride] = import_react9.default.useState(null);
4861
- const [query, setQuery] = import_react9.default.useState("");
4862
- const [viewer, setViewer] = import_react9.default.useState(null);
4863
- const rootRef = import_react9.default.useRef(null);
4864
- const scrollPos = import_react9.default.useRef(null);
4865
- const restoreScroll = import_react9.default.useRef(null);
4866
- const [filter, setFilter] = import_react9.default.useState("all");
4867
- const [builtinOpen, setBuiltinOpen] = import_react9.default.useState(false);
5183
+ const [tab, setTab] = import_react10.default.useState("installed");
5184
+ const [view, setView] = import_react10.default.useState("flat");
5185
+ const [modal, setModal] = import_react10.default.useState(null);
5186
+ const [expanded, setExpanded] = import_react10.default.useState({});
5187
+ const [placementBusy, setPlacementBusy] = import_react10.default.useState(null);
5188
+ const [placementOverride, setPlacementOverride] = import_react10.default.useState(null);
5189
+ const [query, setQuery] = import_react10.default.useState("");
5190
+ const [viewer, setViewer] = import_react10.default.useState(null);
5191
+ const rootRef = import_react10.default.useRef(null);
5192
+ const scrollPos = import_react10.default.useRef(null);
5193
+ const restoreScroll = import_react10.default.useRef(null);
5194
+ const [filter, setFilter] = import_react10.default.useState("all");
5195
+ const [builtinOpen, setBuiltinOpen] = import_react10.default.useState(false);
4868
5196
  const { settingsOpen, settingsRef, settingsPlacement, closeSettings, toggleSettings } = useSettingsPopover();
4869
5197
  const { agentTip, agentTipPos, agentTipRef, openAgentTip, closeAgentTip } = useAgentTip();
5198
+ const pluginUpdate = usePluginUpdate();
5199
+ const [updateOpen, setUpdateOpen] = import_react10.default.useState(false);
4870
5200
  const rows = data?.installs || [];
4871
5201
  const actions = useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal });
4872
5202
  const writable = Boolean(data) && !data?.readOnly && data?.cli?.usable;
@@ -4887,7 +5217,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4887
5217
  const agentLabels = row.exports.map((item) => item.label);
4888
5218
  return [row.name, row.source || "", ...agentLabels].join(" ").toLowerCase().includes(queryText);
4889
5219
  });
4890
- import_react9.default.useEffect(() => {
5220
+ import_react10.default.useEffect(() => {
4891
5221
  if (!viewer || !data) return;
4892
5222
  if (rows.some((row) => row.name === viewer.name)) return;
4893
5223
  setViewer(null);
@@ -4921,7 +5251,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4921
5251
  if (viewer?.mode === "page") restoreScroll.current = scrollPos.current;
4922
5252
  setViewer(null);
4923
5253
  }
4924
- import_react9.default.useLayoutEffect(() => {
5254
+ import_react10.default.useLayoutEffect(() => {
4925
5255
  if (restoreScroll.current === null) return;
4926
5256
  const target = restoreScroll.current;
4927
5257
  restoreScroll.current = null;
@@ -4990,7 +5320,7 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
4990
5320
  onToast: (next) => setToast(next)
4991
5321
  }) : null;
4992
5322
  const listNode = h(
4993
- import_react9.default.Fragment,
5323
+ import_react10.default.Fragment,
4994
5324
  null,
4995
5325
  h(Header, {
4996
5326
  t,
@@ -5006,6 +5336,8 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
5006
5336
  placement,
5007
5337
  placementBusy,
5008
5338
  readOnly: Boolean(data?.readOnly),
5339
+ pluginUpdate,
5340
+ onCheckUpdate: () => setUpdateOpen(true),
5009
5341
  onToggleSettings: toggleSettings,
5010
5342
  onChangePlacement: (next) => void changePlacement(next),
5011
5343
  onOpenAutoLink: () => setModal({ type: "autolink" }),
@@ -5075,7 +5407,8 @@ function Section({ t, page = false, pickDirectory, onPlacementChange }) {
5075
5407
  onClose: () => setModal(null),
5076
5408
  onSaveAutoLink: saveAutoLink
5077
5409
  }) : null,
5078
- agentTip ? h(AgentTip, { t, agentTip, agentTipPos, nodeRef: agentTipRef }) : null
5410
+ agentTip ? h(AgentTip, { t, agentTip, agentTipPos, nodeRef: agentTipRef }) : null,
5411
+ updateOpen ? h(PluginUpdateModal, { t, update: pluginUpdate, repoUrl: GITHUB_URL, onClose: () => setUpdateOpen(false) }) : null
5079
5412
  );
5080
5413
  return page ? h("div", { className: "dshsc-page" }, root) : root;
5081
5414
  }