dsh-plugin-effort-declare 0.1.2 → 0.1.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.
@@ -52,6 +52,8 @@ pnpm pack # confirm the tarball contains lib/ and cordis.patch.yml
52
52
  pnpm publish
53
53
  ```
54
54
 
55
+ The settings footer version and copyright end year are frozen into the client bundle at `pnpm build` (`package.json` version plus the UTC year at that moment). Bump the version, then build / publish. Do not read the wall clock at DSH startup.
56
+
55
57
  `pnpm publish` fails on an unclean Git tree. Commit first (and preferably push). Do not pass `--no-git-checks`.
56
58
 
57
59
  After publish, users update with `dsh plugin --profile web update dsh-plugin-effort-declare` or `add dsh-plugin-effort-declare@latest`. Authors do not implement an update command; official `dsh plugin` forwards the rest of the line to pnpm.
package/CONTRIBUTING.md CHANGED
@@ -52,6 +52,8 @@ pnpm pack # 确认 tarball 含 lib/ 与 cordis.patch.yml
52
52
  pnpm publish
53
53
  ```
54
54
 
55
+ 页脚版本与版权结束年在 `pnpm build` 时写入 client bundle(`package.json` 的 version + 当时的 UTC 年)。先改 version 再 build / publish。不要在运行时读系统时钟。
56
+
55
57
  `pnpm publish` 在 Git 有未提交改动时会失败。应先 commit(建议再 push),不要用 `--no-git-checks` 绕过。
56
58
 
57
59
  发布后用户用 `dsh plugin --profile web update dsh-plugin-effort-declare` 或 `add dsh-plugin-effort-declare@latest` 更新。作者无需在插件里实现更新命令;官方 `dsh plugin` 会把参数转给 pnpm。
package/INSTALL.en.md CHANGED
@@ -83,7 +83,7 @@ dsh plugin --profile web add file:/absolute/path/to/dsh-plugin-effort-declare
83
83
 
84
84
  ## After install
85
85
 
86
- Restart DSH. Open **Settings → Reasoning efforts**. Add a custom provider on the Models page first; otherwise this page has nothing to edit.
86
+ Restart DSH. Open **Settings → Reasoning efforts**. The footer is the installed version (for example `0.1.3 © 2026 Stardust`); bumping `package.json` only shows up after you rebuild and reinstall. Add a custom provider on the Models page first; otherwise this page has nothing to edit.
87
87
 
88
88
  ## Uninstall
89
89
 
package/INSTALL.md CHANGED
@@ -83,7 +83,7 @@ dsh plugin --profile web add file:/绝对路径/dsh-plugin-effort-declare
83
83
 
84
84
  ## 装完之后
85
85
 
86
- 重启 DSH。打开 **设置 → 推理档位**。先在「模型」页添加第三方提供方,本页才会出现可编辑的条目。
86
+ 重启 DSH。打开 **设置 → 推理档位**。页脚是当前装上的版本(例如 `0.1.3 © 2026 Stardust`),改 `package.json` 后必须重新打包再安装才会变。先在「模型」页添加第三方提供方,本页才会出现可编辑的条目。
87
87
 
88
88
  ## 卸载
89
89
 
package/README.en.md CHANGED
@@ -34,6 +34,7 @@ Hand-added third-party providers on the Models page need a reasoning-level decla
34
34
  | Reasoning efforts page | Choose levels per model; change the gateway spelling if it differs |
35
35
  | Presets | DeepSeek compatible, OpenAI compatible, or on/off only. A preset **replaces** that dialect; it does not stack |
36
36
  | vs Models | Models owns the endpoint, keys, and model list; this page only declares which reasoning levels are offered |
37
+ | Version footer | Bottom of the page shows the plugin version and copyright years (frozen at pack time; opening DSH does not advance the year) |
37
38
 
38
39
  ## Install
39
40
 
@@ -51,7 +52,7 @@ dsh plugin --profile web update dsh-plugin-effort-declare
51
52
 
52
53
  Or `add dsh-plugin-effort-declare@latest`. Details: [install guide](./INSTALL.en.md) **Update**.
53
54
 
54
- Restart DSH, then open **Settings → Reasoning efforts**. Add a custom provider on the Models page first. GitHub, local folder, and uninstall: [install guide](./INSTALL.en.md).
55
+ Restart DSH, then open **Settings → Reasoning efforts**. The footer shows which version is installed. Add a custom provider on the Models page first. GitHub, local folder, and uninstall: [install guide](./INSTALL.en.md).
55
56
 
56
57
  ## Usage
57
58
 
package/README.md CHANGED
@@ -34,6 +34,7 @@
34
34
  | 推理档位页 | 按模型勾选可选档位;网关用的名字如果不同,可以改拼写 |
35
35
  | 预设 | DeepSeek 兼容、OpenAI 兼容、只开/关思考。点预设会换成那一套,不是叠加上去 |
36
36
  | 和「模型」页 | 「模型」管地址、密钥和模型名单;本页只管「能选哪些推理档」 |
37
+ | 版本页脚 | 页面底部显示当前插件版本与版权年(打包时写入;打开 DSH 不会跟着日历变) |
37
38
 
38
39
  ## 安装
39
40
 
@@ -51,7 +52,7 @@ dsh plugin --profile web update dsh-plugin-effort-declare
51
52
 
52
53
  或 `add dsh-plugin-effort-declare@latest`。详见 [安装说明](./INSTALL.md) 的「更新」。
53
54
 
54
- 装完后重启 DSH,打开 **设置 → 推理档位**。请先在「模型」页添加过第三方提供方。GitHub、本地文件夹和卸载也在 [安装说明](./INSTALL.md)。
55
+ 装完后重启 DSH,打开 **设置 → 推理档位**。页脚可核对装上的版本。请先在「模型」页添加过第三方提供方。GitHub、本地文件夹和卸载也在 [安装说明](./INSTALL.md)。
55
56
 
56
57
  ## 使用
57
58
 
package/lib/client.js CHANGED
@@ -389,7 +389,10 @@ window.__ModuleLoader__.load({
389
389
  const next = { ...efforts };
390
390
  delete next.off;
391
391
  if (mode === "empty") next.off = null;
392
- else if (mode === "value") next.off = value.trim().length > 0 ? value : "none";
392
+ else if (mode === "value") {
393
+ const trimmed = value.trim();
394
+ next.off = trimmed.length > 0 ? trimmed : "none";
395
+ }
393
396
  return next;
394
397
  }
395
398
  /** Whether a thinking level (other than Off) is currently declared. */
@@ -528,20 +531,104 @@ window.__ModuleLoader__.load({
528
531
  return validateReasoningEfforts(row.reasoningEfforts);
529
532
  }
530
533
  //#endregion
534
+ //#region src/core/attribution.ts
535
+ /**
536
+ * Plugin footer attribution. Version and end-year are frozen into the client
537
+ * bundle at pack time; this module only formats the line.
538
+ */
539
+ /** First publication year (LICENSE). Not the user's wall clock. */
540
+ const COPYRIGHT_FROM = 2026;
541
+ const COPYRIGHT_HOLDER = "Stardust";
542
+ /**
543
+ * `0.1.2 © 2026 Stardust` or `0.1.2 © 2026–2027 Stardust`.
544
+ * Throws if version is empty or `to < from` — a bad stamp must not render.
545
+ */
546
+ function formatAttribution(version, from, to) {
547
+ if (version.trim() === "") throw new Error("plugin version must be a non-empty string");
548
+ if (!Number.isInteger(from) || !Number.isInteger(to) || to < from) throw new Error(`invalid copyright range: ${String(from)}\u2013${String(to)}`);
549
+ return `${version} \u00a9 ${to === from ? String(from) : `${String(from)}\u2013${String(to)}`} ${COPYRIGHT_HOLDER}`;
550
+ }
551
+ //#endregion
552
+ //#region src/client/build-info.ts
553
+ /**
554
+ * Footer line frozen into the client bundle. Do not read the clock or
555
+ * package.json at DSH startup — host apply is empty and the settings page
556
+ * runs in the browser.
557
+ */
558
+ const PLUGIN_FOOTER_TEXT = formatAttribution("0.1.3", COPYRIGHT_FROM, 2026);
559
+ //#endregion
531
560
  //#region src/client/load-drafts.ts
532
561
  function schemaDefaultString(node) {
533
562
  if (!isPlainObject(node) || !isPlainObject(node.meta)) return void 0;
534
563
  return typeof node.meta.default === "string" ? node.meta.default : void 0;
535
564
  }
565
+ /** Namespace revision on a describe snapshot, if that row exists. */
566
+ function namespaceRevision(snapshot, ns) {
567
+ return snapshot.view?.namespaces.find((view) => view.ns === ns)?.revision;
568
+ }
536
569
  /**
537
- * First paint: `ensure()` (reads only from idle). Never treat ensure as refresh.
538
- * Callers that must not apply a stale settlement compare generation themselves.
539
- *
540
- * `formats` is the live schema union only. Empty means the dropdown has no
541
- * writable choices (stored values stay visible via `thinkingFormatChoices`).
570
+ * True when `incoming` is the Host echo of a mutate this page already folded,
571
+ * or an older revision the snapshot has already passed. `echoed` is undefined
572
+ * until the first successful write.
573
+ */
574
+ function isOwnDocumentEcho(echoed, incoming) {
575
+ return echoed !== void 0 && incoming <= echoed;
576
+ }
577
+ /**
578
+ * After a preserve-dirty reload: conflicted cards get a conflict notice;
579
+ * live cards drop leftover conflict/error; saved notices stay; gone cards drop.
542
580
  */
543
- async function loadDrafts(api, describe, schema) {
544
- await describe.ensure();
581
+ function foldReloadNotices(current, args) {
582
+ const live = new Set(args.liveProviders);
583
+ const conflicted = new Set(args.conflicted);
584
+ const next = {};
585
+ for (const [provider, notice] of Object.entries(current)) {
586
+ if (!live.has(provider)) continue;
587
+ if (conflicted.has(provider)) continue;
588
+ if (notice.kind === "conflict" || notice.kind === "error") continue;
589
+ next[provider] = notice;
590
+ }
591
+ for (const provider of args.conflicted) next[provider] = args.conflictNotice;
592
+ return next;
593
+ }
594
+ function waitUntil(describe, predicate, signal) {
595
+ if (signal?.aborted) return Promise.resolve(false);
596
+ if (predicate()) return Promise.resolve(true);
597
+ return new Promise((resolve) => {
598
+ let settled = false;
599
+ const finish = (ok) => {
600
+ if (settled) return;
601
+ settled = true;
602
+ stop();
603
+ signal?.removeEventListener("abort", onAbort);
604
+ resolve(ok);
605
+ };
606
+ const onAbort = () => {
607
+ finish(false);
608
+ };
609
+ const stop = describe.subscribe(() => {
610
+ if (predicate()) finish(true);
611
+ });
612
+ signal?.addEventListener("abort", onAbort);
613
+ if (predicate()) finish(true);
614
+ else if (signal?.aborted) finish(false);
615
+ });
616
+ }
617
+ /** Resolve when the mirror's namespace revision is at least `revision`, or abort. */
618
+ async function waitForNamespaceRevision(describe, ns, revision, signal) {
619
+ return await waitUntil(describe, () => {
620
+ const current = namespaceRevision(describe.getSnapshot(), ns);
621
+ return current !== void 0 && current >= revision;
622
+ }, signal) ? "matched" : "aborted";
623
+ }
624
+ /** Resolve when the namespace revision differs from `previous`, or abort. */
625
+ async function waitForNamespaceRevisionChange(describe, ns, previous, signal) {
626
+ return await waitUntil(describe, () => {
627
+ const current = namespaceRevision(describe.getSnapshot(), ns);
628
+ return current !== void 0 && current !== previous;
629
+ }, signal) ? "changed" : "aborted";
630
+ }
631
+ async function assembleDrafts(api, describe, schema) {
545
632
  const mirrored = describe.getSnapshot();
546
633
  if (mirrored.view === void 0) return {
547
634
  writable: false,
@@ -592,6 +679,14 @@ window.__ModuleLoader__.load({
592
679
  drafts
593
680
  };
594
681
  }
682
+ /**
683
+ * `ensure`: first paint / idle recovery (official ensure only reads from idle).
684
+ * `snapshot`: refresh after the mirror revision already moved — do not ensure.
685
+ */
686
+ async function loadDrafts(api, describe, schema, mode = "ensure") {
687
+ if (mode === "ensure") await describe.ensure();
688
+ return assembleDrafts(api, describe, schema);
689
+ }
595
690
  //#endregion
596
691
  //#region src/client/schema-ops.ts
597
692
  /** Wrap a live settingsSchema service as plain callbacks. */
@@ -624,7 +719,7 @@ window.__ModuleLoader__.load({
624
719
  }
625
720
  //#endregion
626
721
  //#region \0dsh-css:C:\Users\zimo\AppData\Roaming\io.github.hairyf.deepseek-harness-desktop\data\dsh\临时目录\dsh-plugin-effort-declare\src\client\effort-declare.module.css.mjs
627
- const cssText = ".hYEBUa_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.hYEBUa_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.hYEBUa_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.hYEBUa_notice{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}.hYEBUa_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.hYEBUa_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.hYEBUa_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.hYEBUa_rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.hYEBUa_rowHead{align-items:baseline;gap:8px;display:flex}.hYEBUa_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.hYEBUa_rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.hYEBUa_compatSummary{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.hYEBUa_presetRow,.hYEBUa_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.hYEBUa_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.hYEBUa_primaryButton,.hYEBUa_secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.hYEBUa_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border:none}.hYEBUa_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.hYEBUa_secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.hYEBUa_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.hYEBUa_primaryButton:disabled,.hYEBUa_secondaryButton:disabled,.hYEBUa_linkButton:disabled,.hYEBUa_input:disabled{opacity:.4;cursor:default}.hYEBUa_primaryButton:focus-visible,.hYEBUa_secondaryButton:focus-visible,.hYEBUa_linkButton:focus-visible,.hYEBUa_input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.hYEBUa_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.hYEBUa_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.hYEBUa_modelEntry{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:8px;padding:10px 0;display:flex}.hYEBUa_modelHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.hYEBUa_modelId{font-size:13px;font-weight:500;line-height:20px}.hYEBUa_modelName{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.hYEBUa_levels{flex-wrap:wrap;gap:10px 14px;display:flex}.hYEBUa_level{align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex}.hYEBUa_wireRow{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.hYEBUa_input,.hYEBUa_selectInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);height:32px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:0 10px;font-size:13px}.hYEBUa_wireInput{width:7em}.hYEBUa_offGroup{flex-direction:column;gap:6px;display:flex}.hYEBUa_advanced{border-top:1px solid var(--dsw-alias-border-l2);padding-top:8px}.hYEBUa_advanced summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.hYEBUa_advancedBody{flex-direction:column;gap:10px;padding-top:10px;display:flex}.hYEBUa_check{align-items:flex-start;gap:8px;font-size:12px;line-height:18px;display:flex}";
722
+ const cssText = ".hYEBUa_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}.hYEBUa_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}.hYEBUa_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}.hYEBUa_notice{color:var(--dsw-alias-state-warn-label);margin:0;font-size:12px;line-height:18px}.hYEBUa_savedNotice{color:var(--dsw-alias-state-success-primary);margin:0;font-size:12px;line-height:18px}.hYEBUa_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}.hYEBUa_rows{flex-direction:column;gap:8px;margin:12px 0 0;padding:0;list-style:none;display:flex}.hYEBUa_rowCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}.hYEBUa_rowHead{align-items:baseline;gap:8px;display:flex}.hYEBUa_rowName{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}.hYEBUa_rowTag{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:4px;flex:none;padding:1px 6px;font-size:11px;line-height:16px}.hYEBUa_compatSummary{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.hYEBUa_presetRow,.hYEBUa_actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.hYEBUa_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}.hYEBUa_primaryButton,.hYEBUa_secondaryButton{box-sizing:border-box;height:36px;font:inherit;cursor:pointer;border-radius:18px;justify-content:center;align-items:center;padding:0 14px;font-size:14px;line-height:22px;display:inline-flex}.hYEBUa_primaryButton{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border:none}.hYEBUa_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.hYEBUa_secondaryButton{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0}.hYEBUa_secondaryButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.hYEBUa_primaryButton:disabled,.hYEBUa_secondaryButton:disabled,.hYEBUa_linkButton:disabled,.hYEBUa_input:disabled{opacity:.4;cursor:default}.hYEBUa_primaryButton:focus-visible,.hYEBUa_secondaryButton:focus-visible,.hYEBUa_linkButton:focus-visible,.hYEBUa_input:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}.hYEBUa_linkButton{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:14px;align-items:center;padding:0 10px;font-size:12px;line-height:18px;display:inline-flex}.hYEBUa_linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.hYEBUa_modelEntry{border-top:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:8px;padding:10px 0;display:flex}.hYEBUa_modelHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.hYEBUa_modelId{font-size:13px;font-weight:500;line-height:20px}.hYEBUa_modelName{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.hYEBUa_levels{flex-wrap:wrap;gap:10px 14px;display:flex}.hYEBUa_level{align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex}.hYEBUa_wireRow{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.hYEBUa_input,.hYEBUa_selectInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);height:32px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:0 10px;font-size:13px}.hYEBUa_wireInput{width:7em}.hYEBUa_offGroup{flex-direction:column;gap:6px;display:flex}.hYEBUa_advanced{border-top:1px solid var(--dsw-alias-border-l2);padding-top:8px}.hYEBUa_advanced summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.hYEBUa_advancedBody{flex-direction:column;gap:10px;padding-top:10px;display:flex}.hYEBUa_check{align-items:flex-start;gap:8px;font-size:12px;line-height:18px;display:flex}.hYEBUa_footer{color:var(--dsw-alias-label-tertiary);opacity:.7;margin:16px 0 0;font-size:11px;line-height:16px}";
628
723
  const cssTagId = "dsh-plugin-effort-declare/effort-declare.module.css";
629
724
  var effort_declare_module_css_default = {
630
725
  "actions": "hYEBUa_actions",
@@ -634,6 +729,7 @@ window.__ModuleLoader__.load({
634
729
  "compatSummary": "hYEBUa_compatSummary",
635
730
  "error": "hYEBUa_error",
636
731
  "fieldLabel": "hYEBUa_fieldLabel",
732
+ "footer": "hYEBUa_footer",
637
733
  "input": "hYEBUa_input",
638
734
  "intro": "hYEBUa_intro",
639
735
  "level": "hYEBUa_level",
@@ -999,40 +1095,75 @@ window.__ModuleLoader__.load({
999
1095
  const [notices, setNotices] = (0, react.useState)({});
1000
1096
  const generationRef = (0, react.useRef)(0);
1001
1097
  const draftsRef = (0, react.useRef)(drafts);
1098
+ const echoedRevisionRef = (0, react.useRef)(void 0);
1099
+ const pendingRevisionRef = (0, react.useRef)(void 0);
1100
+ const busyRouteRef = (0, react.useRef)(null);
1101
+ const abortRef = (0, react.useRef)(null);
1002
1102
  draftsRef.current = drafts;
1003
- const reload = (0, react.useCallback)((preserveDirty) => {
1103
+ const applyDrafts = (next) => {
1104
+ const resolved = typeof next === "function" ? next(draftsRef.current) : next;
1105
+ draftsRef.current = resolved;
1106
+ setDrafts(resolved);
1107
+ };
1108
+ const snapshotMode = () => describe === void 0 || describe.getSnapshot().status === "idle" ? "ensure" : "snapshot";
1109
+ const beginGeneration = () => {
1110
+ abortRef.current?.abort();
1111
+ const abort = new AbortController();
1112
+ abortRef.current = abort;
1113
+ return {
1114
+ generation: nextGeneration(generationRef),
1115
+ signal: abort.signal
1116
+ };
1117
+ };
1118
+ const failGeneration = (generation, failure) => {
1119
+ if (!generationIsCurrent(generationRef, generation)) return;
1120
+ setStatus("error");
1121
+ setError(failure instanceof Error ? failure.message : t("loadError"));
1122
+ };
1123
+ const settleReload = (generation, preserveDirty, result) => {
1124
+ if (!generationIsCurrent(generationRef, generation)) return;
1125
+ setWritable(result.writable);
1126
+ setFormats(result.formats);
1127
+ if (result.error !== void 0) {
1128
+ setStatus("error");
1129
+ setError(result.error);
1130
+ return;
1131
+ }
1132
+ const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty });
1133
+ applyDrafts(merged.drafts);
1134
+ setNotices((current) => foldReloadNotices(current, {
1135
+ conflicted: merged.conflicted,
1136
+ conflictNotice: {
1137
+ kind: "conflict",
1138
+ text: t("dirtyConflict")
1139
+ },
1140
+ liveProviders: merged.drafts.map((draft) => draft.provider)
1141
+ }));
1142
+ setStatus("ready");
1143
+ };
1144
+ const loadSnapshotThenSettle = async (generation, preserveDirty) => {
1145
+ if (api === void 0 || describe === void 0 || schema === void 0) return;
1146
+ if (!generationIsCurrent(generationRef, generation)) return;
1147
+ try {
1148
+ const result = await loadDrafts(api, describe, schema, "snapshot");
1149
+ settleReload(generation, preserveDirty, result);
1150
+ } catch (failure) {
1151
+ failGeneration(generation, failure);
1152
+ }
1153
+ };
1154
+ const reload = (0, react.useCallback)((preserveDirty, mode = "ensure") => {
1004
1155
  if (api === void 0 || describe === void 0 || schema === void 0) {
1005
1156
  setStatus("error");
1006
1157
  setError(t("loadError"));
1007
1158
  return;
1008
1159
  }
1009
- const generation = nextGeneration(generationRef);
1160
+ const { generation } = beginGeneration();
1010
1161
  if (draftsRef.current.length === 0) setStatus("loading");
1011
1162
  setError("");
1012
- loadDrafts(api, describe, schema).then((result) => {
1013
- if (!generationIsCurrent(generationRef, generation)) return;
1014
- setWritable(result.writable);
1015
- setFormats(result.formats);
1016
- if (result.error !== void 0) {
1017
- setStatus("error");
1018
- setError(result.error);
1019
- return;
1020
- }
1021
- const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty });
1022
- setDrafts(merged.drafts);
1023
- if (merged.conflicted.length > 0) setNotices((current) => {
1024
- const next = { ...current };
1025
- for (const provider of merged.conflicted) next[provider] = {
1026
- kind: "conflict",
1027
- text: t("dirtyConflict")
1028
- };
1029
- return next;
1030
- });
1031
- setStatus("ready");
1163
+ loadDrafts(api, describe, schema, mode).then((result) => {
1164
+ settleReload(generation, preserveDirty, result);
1032
1165
  }, (failure) => {
1033
- if (!generationIsCurrent(generationRef, generation)) return;
1034
- setStatus("error");
1035
- setError(failure instanceof Error ? failure.message : t("loadError"));
1166
+ failGeneration(generation, failure);
1036
1167
  });
1037
1168
  }, [
1038
1169
  api,
@@ -1040,22 +1171,61 @@ window.__ModuleLoader__.load({
1040
1171
  schema,
1041
1172
  t
1042
1173
  ]);
1174
+ const refreshAtRevision = (0, react.useCallback)((revision, preserveDirty) => {
1175
+ if (api === void 0 || describe === void 0 || schema === void 0) return;
1176
+ const { generation, signal } = beginGeneration();
1177
+ if (draftsRef.current.length === 0) setStatus("loading");
1178
+ setError("");
1179
+ waitForNamespaceRevision(describe, LLM_PI_AI_NS, revision, signal).then((outcome) => {
1180
+ if (outcome === "aborted" || !generationIsCurrent(generationRef, generation)) return;
1181
+ return loadSnapshotThenSettle(generation, preserveDirty);
1182
+ }, (failure) => {
1183
+ failGeneration(generation, failure);
1184
+ });
1185
+ }, [
1186
+ api,
1187
+ describe,
1188
+ schema,
1189
+ t
1190
+ ]);
1191
+ const flushPendingSettings = (refresh) => {
1192
+ const pending = pendingRevisionRef.current;
1193
+ pendingRevisionRef.current = void 0;
1194
+ if (pending === void 0) return;
1195
+ if (isOwnDocumentEcho(echoedRevisionRef.current, pending)) return;
1196
+ refresh(pending, true);
1197
+ };
1043
1198
  (0, react.useEffect)(() => {
1044
- reload(false);
1199
+ reload(false, "ensure");
1045
1200
  }, [reload]);
1201
+ (0, react.useEffect)(() => () => {
1202
+ abortRef.current?.abort();
1203
+ nextGeneration(generationRef);
1204
+ }, []);
1046
1205
  (0, react.useEffect)(() => {
1047
1206
  if (props.subscribeInvalidate === void 0) return void 0;
1048
- return props.subscribeInvalidate((source) => {
1049
- if (source === "writable") {
1207
+ return props.subscribeInvalidate((event) => {
1208
+ if (event.source === "writable") {
1050
1209
  const view = describe?.getSnapshot().view;
1051
1210
  if (view !== void 0) setWritable(view.writable);
1052
1211
  return;
1053
1212
  }
1054
- if (source === "settings" || source === "directory") reload(true);
1213
+ if (event.source === "settings") {
1214
+ if (busyRouteRef.current !== null) {
1215
+ pendingRevisionRef.current = event.revision;
1216
+ return;
1217
+ }
1218
+ if (isOwnDocumentEcho(echoedRevisionRef.current, event.revision)) return;
1219
+ refreshAtRevision(event.revision, true);
1220
+ return;
1221
+ }
1222
+ if (event.source === "directory") reload(true, snapshotMode());
1223
+ if (event.source === "reset") reload(true, "ensure");
1055
1224
  });
1056
1225
  }, [
1057
1226
  describe,
1058
1227
  props.subscribeInvalidate,
1228
+ refreshAtRevision,
1059
1229
  reload
1060
1230
  ]);
1061
1231
  const patchNotice = (provider, notice) => {
@@ -1068,7 +1238,7 @@ window.__ModuleLoader__.load({
1068
1238
  };
1069
1239
  const save = async (draft) => {
1070
1240
  if (api === void 0 || describe === void 0 || schema === void 0) return;
1071
- if (status === "loading" || busyRoute !== null) {
1241
+ if (status === "loading" || busyRouteRef.current !== null) {
1072
1242
  patchNotice(draft.provider, {
1073
1243
  kind: "error",
1074
1244
  text: t("saveBusy")
@@ -1083,6 +1253,7 @@ window.__ModuleLoader__.load({
1083
1253
  });
1084
1254
  return;
1085
1255
  }
1256
+ busyRouteRef.current = draft.provider;
1086
1257
  setBusyRoute(draft.provider);
1087
1258
  patchNotice(draft.provider, void 0);
1088
1259
  try {
@@ -1094,7 +1265,7 @@ window.__ModuleLoader__.load({
1094
1265
  afterCompat: draft.compat
1095
1266
  });
1096
1267
  if (ops.length === 0) {
1097
- setDrafts((current) => current.map((row) => row.provider === draft.provider ? alignDraft(row) : row));
1268
+ applyDrafts((current) => current.map((row) => row.provider === draft.provider ? alignDraft(row) : row));
1098
1269
  return;
1099
1270
  }
1100
1271
  const willWriteCompat = ops.some((op) => op.path.length > draft.settingsPath.length && op.path[draft.settingsPath.length] === "compat");
@@ -1128,12 +1299,21 @@ window.__ModuleLoader__.load({
1128
1299
  kind: conflict ? "conflict" : "error",
1129
1300
  text: conflict ? t("conflict") : response.result.error.message
1130
1301
  });
1131
- if (conflict) reload(true);
1302
+ if (conflict) {
1303
+ const { generation, signal } = beginGeneration();
1304
+ waitForNamespaceRevisionChange(describe, LLM_PI_AI_NS, draft.revision, signal).then((outcome) => {
1305
+ if (outcome === "aborted" || !generationIsCurrent(generationRef, generation)) return;
1306
+ return loadSnapshotThenSettle(generation, true);
1307
+ }, (failure) => {
1308
+ failGeneration(generation, failure);
1309
+ });
1310
+ }
1132
1311
  return;
1133
1312
  }
1134
1313
  const view = response.result.value;
1314
+ echoedRevisionRef.current = view.revision;
1135
1315
  describe.acceptView(view);
1136
- setDrafts((current) => applySaveSuccess(current, draft.provider, {
1316
+ applyDrafts(applySaveSuccess(draftsRef.current, draft.provider, {
1137
1317
  user: view.user ?? {},
1138
1318
  revision: view.revision
1139
1319
  }));
@@ -1147,7 +1327,9 @@ window.__ModuleLoader__.load({
1147
1327
  text: failure instanceof Error ? failure.message : t("loadError")
1148
1328
  });
1149
1329
  } finally {
1330
+ busyRouteRef.current = null;
1150
1331
  setBusyRoute(null);
1332
+ flushPendingSettings(refreshAtRevision);
1151
1333
  }
1152
1334
  };
1153
1335
  const showLoading = status === "loading" && drafts.length === 0;
@@ -1182,7 +1364,7 @@ window.__ModuleLoader__.load({
1182
1364
  type: "button",
1183
1365
  className: effort_declare_module_css_default.secondaryButton,
1184
1366
  onClick: () => {
1185
- reload(true);
1367
+ reload(true, snapshotMode());
1186
1368
  },
1187
1369
  children: t("reload")
1188
1370
  }) : null,
@@ -1205,21 +1387,25 @@ window.__ModuleLoader__.load({
1205
1387
  t,
1206
1388
  onChange: (next) => {
1207
1389
  patchNotice(next.provider, void 0);
1208
- setDrafts((current) => current.map((row) => row.provider === next.provider ? next : row));
1390
+ applyDrafts((current) => current.map((row) => row.provider === next.provider ? next : row));
1209
1391
  },
1210
1392
  onSave: (next) => {
1211
1393
  save(next);
1212
1394
  },
1213
1395
  onCancel: (next) => {
1214
1396
  patchNotice(next.provider, void 0);
1215
- setDrafts((current) => current.map((row) => row.provider === next.provider ? {
1397
+ applyDrafts((current) => current.map((row) => row.provider === next.provider ? {
1216
1398
  ...row,
1217
1399
  models: cloneModels(row.originalModels),
1218
1400
  compat: cloneObject(row.originalCompat)
1219
1401
  } : row));
1220
1402
  }
1221
1403
  }, draft.provider))
1222
- }) : null
1404
+ }) : null,
1405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1406
+ className: effort_declare_module_css_default.footer,
1407
+ children: PLUGIN_FOOTER_TEXT
1408
+ })
1223
1409
  ]
1224
1410
  });
1225
1411
  }
@@ -1357,22 +1543,25 @@ window.__ModuleLoader__.load({
1357
1543
  const describe = ctx.settingsScope.describe();
1358
1544
  const invalidation = /* @__PURE__ */ new Set();
1359
1545
  ctx.effect(() => {
1360
- const emit = (source) => {
1361
- for (const listener of invalidation) listener(source);
1546
+ const emit = (event) => {
1547
+ for (const listener of invalidation) listener(event);
1362
1548
  };
1363
1549
  const disposers = [
1364
1550
  describe.subscribe(() => {
1365
- emit("writable");
1551
+ emit({ source: "writable" });
1366
1552
  }),
1367
- ctx.remote.$on("settings/document-updated", (ns) => {
1553
+ ctx.remote.$on("settings/document-updated", (ns, revision) => {
1368
1554
  if (ns !== "llm-pi-ai") return;
1369
- emit("settings");
1555
+ emit({
1556
+ source: "settings",
1557
+ revision
1558
+ });
1370
1559
  }),
1371
1560
  ctx.remote.$on("llm/adapters-updated", () => {
1372
- emit("directory");
1561
+ emit({ source: "directory" });
1373
1562
  }),
1374
1563
  ctx.on("connection/reset", () => {
1375
- emit("directory");
1564
+ emit({ source: "reset" });
1376
1565
  })
1377
1566
  ];
1378
1567
  return () => {