dsh-plugin-effort-declare 0.1.2 → 0.1.4

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.
Files changed (48) hide show
  1. package/CONTRIBUTING.en.md +20 -9
  2. package/CONTRIBUTING.md +24 -13
  3. package/INSTALL.en.md +9 -9
  4. package/INSTALL.md +16 -16
  5. package/README.en.md +25 -20
  6. package/README.md +28 -23
  7. package/lib/client.js +354 -82
  8. package/lib/client.js.map +1 -1
  9. package/lib/types/client/EffortDeclareSection.d.ts +14 -4
  10. package/lib/types/client/EffortDeclareSection.d.ts.map +1 -1
  11. package/lib/types/client/build-info.d.ts +2 -0
  12. package/lib/types/client/build-info.d.ts.map +1 -0
  13. package/lib/types/client/index.d.ts.map +1 -1
  14. package/lib/types/client/load-drafts.d.ts +36 -6
  15. package/lib/types/client/load-drafts.d.ts.map +1 -1
  16. package/lib/types/client/locales.d.ts +1 -1
  17. package/lib/types/client/locales.d.ts.map +1 -1
  18. package/lib/types/core/attribution.d.ts +13 -0
  19. package/lib/types/core/attribution.d.ts.map +1 -0
  20. package/lib/types/core/catalog.d.ts +8 -0
  21. package/lib/types/core/catalog.d.ts.map +1 -1
  22. package/lib/types/core/drafts.d.ts +8 -4
  23. package/lib/types/core/drafts.d.ts.map +1 -1
  24. package/lib/types/core/efforts.d.ts.map +1 -1
  25. package/lib/types/core/input.d.ts +15 -0
  26. package/lib/types/core/input.d.ts.map +1 -0
  27. package/lib/types/core/validate.d.ts +3 -0
  28. package/lib/types/core/validate.d.ts.map +1 -1
  29. package/package.json +2 -2
  30. package/src/README.en.md +2 -2
  31. package/src/README.md +2 -2
  32. package/src/client/EffortDeclareSection.tsx +161 -47
  33. package/src/client/README.en.md +7 -5
  34. package/src/client/README.md +7 -5
  35. package/src/client/build-info.ts +12 -0
  36. package/src/client/effort-declare.module.css +8 -0
  37. package/src/client/globals.d.ts +5 -0
  38. package/src/client/index.ts +13 -11
  39. package/src/client/load-drafts.ts +124 -8
  40. package/src/client/locales.ts +13 -4
  41. package/src/core/README.en.md +10 -8
  42. package/src/core/README.md +10 -8
  43. package/src/core/attribution.ts +24 -0
  44. package/src/core/catalog.ts +11 -0
  45. package/src/core/drafts.ts +40 -26
  46. package/src/core/efforts.ts +4 -1
  47. package/src/core/input.ts +49 -0
  48. package/src/core/validate.ts +8 -0
package/lib/client.js CHANGED
@@ -36,6 +36,13 @@ window.__ModuleLoader__.load({
36
36
  const LLM_PI_AI_NS = "llm-pi-ai";
37
37
  /** Any route key walks a dict schema to the same profile node. */
38
38
  const SCHEMA_PROBE_ROUTE = "\0probe";
39
+ /** Request modalities llm-pi-ai accepts on `models[].input` / `defaultInput`. */
40
+ const INPUT_MODALITIES = ["text", "image"];
41
+ /**
42
+ * Canonical write for a hand-declared vision model.
43
+ * Must include `text`; image-only is not a serviceable OpenAI-completions route.
44
+ */
45
+ const IMAGE_CAPABLE_INPUT = ["text", "image"];
39
46
  //#endregion
40
47
  //#region src/core/paths.ts
41
48
  /**
@@ -231,25 +238,29 @@ window.__ModuleLoader__.load({
231
238
  }
232
239
  return map;
233
240
  }
234
- function effortsPresence(row) {
235
- if (row === void 0 || !Object.hasOwn(row, "reasoningEfforts")) return {
241
+ /** Model-row fields this page edits; other keys follow the Models page. */
242
+ const MODEL_OVERLAY_KEYS = ["reasoningEfforts", "input"];
243
+ function fieldPresence(row, key) {
244
+ if (row === void 0 || !Object.hasOwn(row, key)) return {
236
245
  present: false,
237
246
  value: void 0
238
247
  };
239
248
  return {
240
249
  present: true,
241
- value: row.reasoningEfforts
250
+ value: row[key]
242
251
  };
243
252
  }
244
- function effortsEqual(left, right) {
245
- if (left.present !== right.present) return false;
246
- if (!left.present) return true;
247
- return sliceEqual(left.value, right.value);
253
+ function fieldEqual(left, right, key) {
254
+ const a = fieldPresence(left, key);
255
+ const b = fieldPresence(right, key);
256
+ if (a.present !== b.present) return false;
257
+ if (!a.present) return true;
258
+ return sliceEqual(a.value, b.value);
248
259
  }
249
- function overlayLocalEfforts(incomingRow, prevRow) {
260
+ function overlayLocalFields(incomingRow, prevRow, keys) {
250
261
  const next = structuredClone(incomingRow);
251
- if (Object.hasOwn(prevRow, "reasoningEfforts")) next.reasoningEfforts = structuredClone(prevRow.reasoningEfforts);
252
- else delete next.reasoningEfforts;
262
+ for (const key of keys) if (Object.hasOwn(prevRow, key)) next[key] = structuredClone(prevRow[key]);
263
+ else delete next[key];
253
264
  return next;
254
265
  }
255
266
  function objectKeyChanged(left, right, key) {
@@ -260,7 +271,8 @@ window.__ModuleLoader__.load({
260
271
  }
261
272
  /**
262
273
  * Membership follows the latest user-layer models list (Models page add/delete).
263
- * Local unsaved `reasoningEfforts` (including a cleared key) overlay by id.
274
+ * Local unsaved overlay keys (including a cleared key) overlay by id; other
275
+ * fields on the row follow incoming.
264
276
  */
265
277
  function mergeModelsById(args) {
266
278
  const prevById = indexById(args.prevModels);
@@ -273,16 +285,19 @@ window.__ModuleLoader__.load({
273
285
  const prevRow = prevById.get(id);
274
286
  if (prevRow === void 0) return structuredClone(incomingRow);
275
287
  const prevOrig = prevOrigById.get(id);
276
- if (!!effortsEqual(effortsPresence(prevRow), effortsPresence(prevOrig))) return structuredClone(incomingRow);
288
+ const dirtyKeys = MODEL_OVERLAY_KEYS.filter((key) => !fieldEqual(prevRow, prevOrig, key));
289
+ if (dirtyKeys.length === 0) return structuredClone(incomingRow);
277
290
  const incomingOrig = incomingOrigById.get(id);
278
- if (!effortsEqual(effortsPresence(prevOrig), effortsPresence(incomingOrig))) conflicted = true;
279
- return overlayLocalEfforts(incomingRow, prevRow);
291
+ for (const key of dirtyKeys) if (!fieldEqual(prevOrig, incomingOrig, key)) conflicted = true;
292
+ return overlayLocalFields(incomingRow, prevRow, dirtyKeys);
280
293
  });
281
294
  for (const [id, prevRow] of prevById) {
282
295
  if (incomingIds.has(id)) continue;
283
296
  const prevOrig = prevOrigById.get(id);
284
- if (effortsEqual(effortsPresence(prevRow), effortsPresence(prevOrig))) continue;
285
- if (!effortsEqual(effortsPresence(prevOrig), effortsPresence(incomingOrigById.get(id)))) conflicted = true;
297
+ const dirtyKeys = MODEL_OVERLAY_KEYS.filter((key) => !fieldEqual(prevRow, prevOrig, key));
298
+ if (dirtyKeys.length === 0) continue;
299
+ const incomingOrig = incomingOrigById.get(id);
300
+ for (const key of dirtyKeys) if (!fieldEqual(prevOrig, incomingOrig, key)) conflicted = true;
286
301
  }
287
302
  return {
288
303
  models,
@@ -314,9 +329,9 @@ window.__ModuleLoader__.load({
314
329
  }
315
330
  /**
316
331
  * Apply a freshly loaded table. Membership and metadata follow incoming;
317
- * unsaved reasoningEfforts / dirty compat keys overlay by id. Conflict only
318
- * when a locally dirty field also changed in originals (revision-only bumps
319
- * and sibling-card saves do not warn).
332
+ * unsaved overlay keys (`reasoningEfforts`, `input`) / dirty compat keys
333
+ * overlay by id. Conflict only when a locally dirty field also changed in
334
+ * originals (revision-only bumps and sibling-card saves do not warn).
320
335
  */
321
336
  function mergeLoadedDrafts(current, incoming, options) {
322
337
  const currentByProvider = new Map(current.map((draft) => [draft.provider, draft]));
@@ -389,7 +404,10 @@ window.__ModuleLoader__.load({
389
404
  const next = { ...efforts };
390
405
  delete next.off;
391
406
  if (mode === "empty") next.off = null;
392
- else if (mode === "value") next.off = value.trim().length > 0 ? value : "none";
407
+ else if (mode === "value") {
408
+ const trimmed = value.trim();
409
+ next.off = trimmed.length > 0 ? trimmed : "none";
410
+ }
393
411
  return next;
394
412
  }
395
413
  /** Whether a thinking level (other than Off) is currently declared. */
@@ -521,27 +539,157 @@ window.__ModuleLoader__.load({
521
539
  return next;
522
540
  }
523
541
  //#endregion
542
+ //#region src/core/input.ts
543
+ /**
544
+ * Per-model `input` modalities as stored in llm-pi-ai settings.
545
+ * Absence falls through to route `defaultInput` (`[text]`). Empty list is
546
+ * treated as absent by the official resolver — do not write `[]`.
547
+ */
548
+ /** Whether the stored `input` list includes image. Absence is false. */
549
+ function readImageCapable(row) {
550
+ const value = row.input;
551
+ return Array.isArray(value) && value.includes("image");
552
+ }
553
+ /**
554
+ * Declare or drop image input on a spread row.
555
+ * Enabled writes `[text, image]`; disabled unsets the key (not `[]`, not `[text]`).
556
+ */
557
+ function writeImageCapable(row, enabled) {
558
+ const next = { ...row };
559
+ if (enabled) next.input = [...IMAGE_CAPABLE_INPUT];
560
+ else delete next.input;
561
+ return next;
562
+ }
563
+ /**
564
+ * Validate a stored `input` field. Returns an error code; never throws.
565
+ * Absence is valid. Empty list is rejected so a bad stamp cannot be re-saved.
566
+ */
567
+ function validateInput(input) {
568
+ if (input === void 0) return void 0;
569
+ if (!Array.isArray(input) || input.length === 0) return "bad-input";
570
+ const known = new Set(INPUT_MODALITIES);
571
+ const seen = /* @__PURE__ */ new Set();
572
+ for (const item of input) {
573
+ if (typeof item !== "string" || !known.has(item) || seen.has(item)) return "bad-input";
574
+ seen.add(item);
575
+ }
576
+ if (!seen.has("text")) return "bad-input";
577
+ }
578
+ /** Per-model client-side check used to show errors without throwing. */
579
+ function modelInputError(row) {
580
+ if (!("input" in row) || row.input === void 0) return void 0;
581
+ return validateInput(row.input);
582
+ }
583
+ //#endregion
524
584
  //#region src/core/validate.ts
525
585
  /** Per-model client-side check used to show errors without throwing. */
526
586
  function modelEffortError(row) {
527
587
  if (!("reasoningEfforts" in row) || row.reasoningEfforts === void 0) return void 0;
528
588
  return validateReasoningEfforts(row.reasoningEfforts);
529
589
  }
590
+ /** First blocking error on a model row (efforts, then input). */
591
+ function modelRowError(row) {
592
+ return modelEffortError(row) ?? modelInputError(row);
593
+ }
594
+ //#endregion
595
+ //#region src/core/attribution.ts
596
+ /**
597
+ * Plugin footer attribution. Version and end-year are frozen into the client
598
+ * bundle at pack time; this module only formats the line.
599
+ */
600
+ /** First publication year (LICENSE). Not the user's wall clock. */
601
+ const COPYRIGHT_FROM = 2026;
602
+ const COPYRIGHT_HOLDER = "Stardust";
603
+ /**
604
+ * `0.1.2 © 2026 Stardust` or `0.1.2 © 2026–2027 Stardust`.
605
+ * Throws if version is empty or `to < from` — a bad stamp must not render.
606
+ */
607
+ function formatAttribution(version, from, to) {
608
+ if (version.trim() === "") throw new Error("plugin version must be a non-empty string");
609
+ if (!Number.isInteger(from) || !Number.isInteger(to) || to < from) throw new Error(`invalid copyright range: ${String(from)}\u2013${String(to)}`);
610
+ return `${version} \u00a9 ${to === from ? String(from) : `${String(from)}\u2013${String(to)}`} ${COPYRIGHT_HOLDER}`;
611
+ }
612
+ //#endregion
613
+ //#region src/client/build-info.ts
614
+ /**
615
+ * Footer line frozen into the client bundle. Do not read the clock or
616
+ * package.json at DSH startup — host apply is empty and the settings page
617
+ * runs in the browser.
618
+ */
619
+ const PLUGIN_FOOTER_TEXT = formatAttribution("0.1.4", COPYRIGHT_FROM, 2026);
530
620
  //#endregion
531
621
  //#region src/client/load-drafts.ts
532
622
  function schemaDefaultString(node) {
533
623
  if (!isPlainObject(node) || !isPlainObject(node.meta)) return void 0;
534
624
  return typeof node.meta.default === "string" ? node.meta.default : void 0;
535
625
  }
626
+ /** Namespace revision on a describe snapshot, if that row exists. */
627
+ function namespaceRevision(snapshot, ns) {
628
+ return snapshot.view?.namespaces.find((view) => view.ns === ns)?.revision;
629
+ }
536
630
  /**
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`).
631
+ * True when `incoming` is the Host echo of a mutate this page already folded,
632
+ * or an older revision the snapshot has already passed. `echoed` is undefined
633
+ * until the first successful write.
634
+ */
635
+ function isOwnDocumentEcho(echoed, incoming) {
636
+ return echoed !== void 0 && incoming <= echoed;
637
+ }
638
+ /**
639
+ * After a preserve-dirty reload: conflicted cards get a conflict notice;
640
+ * live cards drop leftover conflict/error; saved notices stay; gone cards drop.
542
641
  */
543
- async function loadDrafts(api, describe, schema) {
544
- await describe.ensure();
642
+ function foldReloadNotices(current, args) {
643
+ const live = new Set(args.liveProviders);
644
+ const conflicted = new Set(args.conflicted);
645
+ const next = {};
646
+ for (const [provider, notice] of Object.entries(current)) {
647
+ if (!live.has(provider)) continue;
648
+ if (conflicted.has(provider)) continue;
649
+ if (notice.kind === "conflict" || notice.kind === "error") continue;
650
+ next[provider] = notice;
651
+ }
652
+ for (const provider of args.conflicted) next[provider] = args.conflictNotice;
653
+ return next;
654
+ }
655
+ function waitUntil(describe, predicate, signal) {
656
+ if (signal?.aborted) return Promise.resolve(false);
657
+ if (predicate()) return Promise.resolve(true);
658
+ return new Promise((resolve) => {
659
+ let settled = false;
660
+ const finish = (ok) => {
661
+ if (settled) return;
662
+ settled = true;
663
+ stop();
664
+ signal?.removeEventListener("abort", onAbort);
665
+ resolve(ok);
666
+ };
667
+ const onAbort = () => {
668
+ finish(false);
669
+ };
670
+ const stop = describe.subscribe(() => {
671
+ if (predicate()) finish(true);
672
+ });
673
+ signal?.addEventListener("abort", onAbort);
674
+ if (predicate()) finish(true);
675
+ else if (signal?.aborted) finish(false);
676
+ });
677
+ }
678
+ /** Resolve when the mirror's namespace revision is at least `revision`, or abort. */
679
+ async function waitForNamespaceRevision(describe, ns, revision, signal) {
680
+ return await waitUntil(describe, () => {
681
+ const current = namespaceRevision(describe.getSnapshot(), ns);
682
+ return current !== void 0 && current >= revision;
683
+ }, signal) ? "matched" : "aborted";
684
+ }
685
+ /** Resolve when the namespace revision differs from `previous`, or abort. */
686
+ async function waitForNamespaceRevisionChange(describe, ns, previous, signal) {
687
+ return await waitUntil(describe, () => {
688
+ const current = namespaceRevision(describe.getSnapshot(), ns);
689
+ return current !== void 0 && current !== previous;
690
+ }, signal) ? "changed" : "aborted";
691
+ }
692
+ async function assembleDrafts(api, describe, schema) {
545
693
  const mirrored = describe.getSnapshot();
546
694
  if (mirrored.view === void 0) return {
547
695
  writable: false,
@@ -592,6 +740,14 @@ window.__ModuleLoader__.load({
592
740
  drafts
593
741
  };
594
742
  }
743
+ /**
744
+ * `ensure`: first paint / idle recovery (official ensure only reads from idle).
745
+ * `snapshot`: refresh after the mirror revision already moved — do not ensure.
746
+ */
747
+ async function loadDrafts(api, describe, schema, mode = "ensure") {
748
+ if (mode === "ensure") await describe.ensure();
749
+ return assembleDrafts(api, describe, schema);
750
+ }
595
751
  //#endregion
596
752
  //#region src/client/schema-ops.ts
597
753
  /** Wrap a live settingsSchema service as plain callbacks. */
@@ -624,7 +780,7 @@ window.__ModuleLoader__.load({
624
780
  }
625
781
  //#endregion
626
782
  //#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}";
783
+ 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
784
  const cssTagId = "dsh-plugin-effort-declare/effort-declare.module.css";
629
785
  var effort_declare_module_css_default = {
630
786
  "actions": "hYEBUa_actions",
@@ -634,6 +790,7 @@ window.__ModuleLoader__.load({
634
790
  "compatSummary": "hYEBUa_compatSummary",
635
791
  "error": "hYEBUa_error",
636
792
  "fieldLabel": "hYEBUa_fieldLabel",
793
+ "footer": "hYEBUa_footer",
637
794
  "input": "hYEBUa_input",
638
795
  "intro": "hYEBUa_intro",
639
796
  "level": "hYEBUa_level",
@@ -663,8 +820,8 @@ window.__ModuleLoader__.load({
663
820
  //#endregion
664
821
  //#region src/client/EffortDeclareSection.tsx
665
822
  /**
666
- * Settings section: per-model reasoningEfforts + openai-completions compat
667
- * for hand-declared llm-pi-ai routes.
823
+ * Settings section: per-model reasoningEfforts, image input, and
824
+ * openai-completions compat for hand-declared llm-pi-ai routes.
668
825
  */
669
826
  function compatSummary(compat) {
670
827
  const parts = [];
@@ -677,6 +834,7 @@ window.__ModuleLoader__.load({
677
834
  if (code === "empty") return t("errorEmpty");
678
835
  if (code === "off-only") return t("errorOffOnly");
679
836
  if (code === "bad-wire") return t("errorBadWire");
837
+ if (code === "bad-input") return t("errorBadInput");
680
838
  }
681
839
  function ModelRowEditor(props) {
682
840
  const { row, disabled, t, onChange } = props;
@@ -712,6 +870,21 @@ window.__ModuleLoader__.load({
712
870
  })
713
871
  ]
714
872
  }),
873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
874
+ className: effort_declare_module_css_default.check,
875
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
876
+ type: "checkbox",
877
+ checked: readImageCapable(row),
878
+ disabled,
879
+ onChange: (event) => {
880
+ onChange(writeImageCapable(row, event.target.checked));
881
+ }
882
+ }), t("imageInput")]
883
+ }),
884
+ readImageCapable(row) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
885
+ className: effort_declare_module_css_default.notice,
886
+ children: t("imageInputHint")
887
+ }) : null,
715
888
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
716
889
  className: effort_declare_module_css_default.fieldLabel,
717
890
  children: t("levels")
@@ -795,7 +968,7 @@ window.__ModuleLoader__.load({
795
968
  const formats = thinkingFormatChoices(props.formats, draft.compat.thinkingFormat);
796
969
  const summary = compatSummary(draft.compat);
797
970
  const sameWire = draft.compat.supportsReasoningEffort === false;
798
- const clientError = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
971
+ const clientError = draft.models.map((row) => errorText(modelRowError(row), t)).find((text) => text !== void 0);
799
972
  const dirty = draftDirty(draft);
800
973
  const applyPreset = (id) => {
801
974
  const preset = PRESETS[id];
@@ -999,40 +1172,92 @@ window.__ModuleLoader__.load({
999
1172
  const [notices, setNotices] = (0, react.useState)({});
1000
1173
  const generationRef = (0, react.useRef)(0);
1001
1174
  const draftsRef = (0, react.useRef)(drafts);
1175
+ const echoedRevisionRef = (0, react.useRef)(void 0);
1176
+ const pendingRevisionRef = (0, react.useRef)(void 0);
1177
+ const busyRouteRef = (0, react.useRef)(null);
1178
+ const abortRef = (0, react.useRef)(null);
1002
1179
  draftsRef.current = drafts;
1003
- const reload = (0, react.useCallback)((preserveDirty) => {
1180
+ const applyDrafts = (next) => {
1181
+ const resolved = typeof next === "function" ? next(draftsRef.current) : next;
1182
+ draftsRef.current = resolved;
1183
+ setDrafts(resolved);
1184
+ };
1185
+ const snapshotMode = () => describe === void 0 || describe.getSnapshot().status === "idle" ? "ensure" : "snapshot";
1186
+ const beginGeneration = () => {
1187
+ abortRef.current?.abort();
1188
+ const abort = new AbortController();
1189
+ abortRef.current = abort;
1190
+ return {
1191
+ generation: nextGeneration(generationRef),
1192
+ signal: abort.signal
1193
+ };
1194
+ };
1195
+ const failGeneration = (generation, failure) => {
1196
+ if (!generationIsCurrent(generationRef, generation)) return;
1197
+ setStatus("error");
1198
+ setError(failure instanceof Error ? failure.message : t("loadError"));
1199
+ };
1200
+ const settleReload = (generation, preserveDirty, result) => {
1201
+ if (!generationIsCurrent(generationRef, generation)) return;
1202
+ setWritable(result.writable);
1203
+ setFormats(result.formats);
1204
+ if (result.error !== void 0) {
1205
+ setStatus("error");
1206
+ setError(result.error);
1207
+ return;
1208
+ }
1209
+ const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty });
1210
+ applyDrafts(merged.drafts);
1211
+ setNotices((current) => foldReloadNotices(current, {
1212
+ conflicted: merged.conflicted,
1213
+ conflictNotice: {
1214
+ kind: "conflict",
1215
+ text: t("dirtyConflict")
1216
+ },
1217
+ liveProviders: merged.drafts.map((draft) => draft.provider)
1218
+ }));
1219
+ setStatus("ready");
1220
+ };
1221
+ const loadSnapshotThenSettle = async (generation, preserveDirty) => {
1222
+ if (api === void 0 || describe === void 0 || schema === void 0) return;
1223
+ if (!generationIsCurrent(generationRef, generation)) return;
1224
+ try {
1225
+ const result = await loadDrafts(api, describe, schema, "snapshot");
1226
+ settleReload(generation, preserveDirty, result);
1227
+ } catch (failure) {
1228
+ failGeneration(generation, failure);
1229
+ }
1230
+ };
1231
+ const reload = (0, react.useCallback)((preserveDirty, mode = "ensure") => {
1004
1232
  if (api === void 0 || describe === void 0 || schema === void 0) {
1005
1233
  setStatus("error");
1006
1234
  setError(t("loadError"));
1007
1235
  return;
1008
1236
  }
1009
- const generation = nextGeneration(generationRef);
1237
+ const { generation } = beginGeneration();
1010
1238
  if (draftsRef.current.length === 0) setStatus("loading");
1011
1239
  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");
1240
+ loadDrafts(api, describe, schema, mode).then((result) => {
1241
+ settleReload(generation, preserveDirty, result);
1032
1242
  }, (failure) => {
1033
- if (!generationIsCurrent(generationRef, generation)) return;
1034
- setStatus("error");
1035
- setError(failure instanceof Error ? failure.message : t("loadError"));
1243
+ failGeneration(generation, failure);
1244
+ });
1245
+ }, [
1246
+ api,
1247
+ describe,
1248
+ schema,
1249
+ t
1250
+ ]);
1251
+ const refreshAtRevision = (0, react.useCallback)((revision, preserveDirty) => {
1252
+ if (api === void 0 || describe === void 0 || schema === void 0) return;
1253
+ const { generation, signal } = beginGeneration();
1254
+ if (draftsRef.current.length === 0) setStatus("loading");
1255
+ setError("");
1256
+ waitForNamespaceRevision(describe, LLM_PI_AI_NS, revision, signal).then((outcome) => {
1257
+ if (outcome === "aborted" || !generationIsCurrent(generationRef, generation)) return;
1258
+ return loadSnapshotThenSettle(generation, preserveDirty);
1259
+ }, (failure) => {
1260
+ failGeneration(generation, failure);
1036
1261
  });
1037
1262
  }, [
1038
1263
  api,
@@ -1040,22 +1265,44 @@ window.__ModuleLoader__.load({
1040
1265
  schema,
1041
1266
  t
1042
1267
  ]);
1268
+ const flushPendingSettings = (refresh) => {
1269
+ const pending = pendingRevisionRef.current;
1270
+ pendingRevisionRef.current = void 0;
1271
+ if (pending === void 0) return;
1272
+ if (isOwnDocumentEcho(echoedRevisionRef.current, pending)) return;
1273
+ refresh(pending, true);
1274
+ };
1043
1275
  (0, react.useEffect)(() => {
1044
- reload(false);
1276
+ reload(false, "ensure");
1045
1277
  }, [reload]);
1278
+ (0, react.useEffect)(() => () => {
1279
+ abortRef.current?.abort();
1280
+ nextGeneration(generationRef);
1281
+ }, []);
1046
1282
  (0, react.useEffect)(() => {
1047
1283
  if (props.subscribeInvalidate === void 0) return void 0;
1048
- return props.subscribeInvalidate((source) => {
1049
- if (source === "writable") {
1284
+ return props.subscribeInvalidate((event) => {
1285
+ if (event.source === "writable") {
1050
1286
  const view = describe?.getSnapshot().view;
1051
1287
  if (view !== void 0) setWritable(view.writable);
1052
1288
  return;
1053
1289
  }
1054
- if (source === "settings" || source === "directory") reload(true);
1290
+ if (event.source === "settings") {
1291
+ if (busyRouteRef.current !== null) {
1292
+ pendingRevisionRef.current = event.revision;
1293
+ return;
1294
+ }
1295
+ if (isOwnDocumentEcho(echoedRevisionRef.current, event.revision)) return;
1296
+ refreshAtRevision(event.revision, true);
1297
+ return;
1298
+ }
1299
+ if (event.source === "directory") reload(true, snapshotMode());
1300
+ if (event.source === "reset") reload(true, "ensure");
1055
1301
  });
1056
1302
  }, [
1057
1303
  describe,
1058
1304
  props.subscribeInvalidate,
1305
+ refreshAtRevision,
1059
1306
  reload
1060
1307
  ]);
1061
1308
  const patchNotice = (provider, notice) => {
@@ -1068,14 +1315,14 @@ window.__ModuleLoader__.load({
1068
1315
  };
1069
1316
  const save = async (draft) => {
1070
1317
  if (api === void 0 || describe === void 0 || schema === void 0) return;
1071
- if (status === "loading" || busyRoute !== null) {
1318
+ if (status === "loading" || busyRouteRef.current !== null) {
1072
1319
  patchNotice(draft.provider, {
1073
1320
  kind: "error",
1074
1321
  text: t("saveBusy")
1075
1322
  });
1076
1323
  return;
1077
1324
  }
1078
- const blocking = draft.models.map((row) => errorText(modelEffortError(row), t)).find((text) => text !== void 0);
1325
+ const blocking = draft.models.map((row) => errorText(modelRowError(row), t)).find((text) => text !== void 0);
1079
1326
  if (blocking !== void 0) {
1080
1327
  patchNotice(draft.provider, {
1081
1328
  kind: "error",
@@ -1083,6 +1330,7 @@ window.__ModuleLoader__.load({
1083
1330
  });
1084
1331
  return;
1085
1332
  }
1333
+ busyRouteRef.current = draft.provider;
1086
1334
  setBusyRoute(draft.provider);
1087
1335
  patchNotice(draft.provider, void 0);
1088
1336
  try {
@@ -1094,7 +1342,7 @@ window.__ModuleLoader__.load({
1094
1342
  afterCompat: draft.compat
1095
1343
  });
1096
1344
  if (ops.length === 0) {
1097
- setDrafts((current) => current.map((row) => row.provider === draft.provider ? alignDraft(row) : row));
1345
+ applyDrafts((current) => current.map((row) => row.provider === draft.provider ? alignDraft(row) : row));
1098
1346
  return;
1099
1347
  }
1100
1348
  const willWriteCompat = ops.some((op) => op.path.length > draft.settingsPath.length && op.path[draft.settingsPath.length] === "compat");
@@ -1128,12 +1376,21 @@ window.__ModuleLoader__.load({
1128
1376
  kind: conflict ? "conflict" : "error",
1129
1377
  text: conflict ? t("conflict") : response.result.error.message
1130
1378
  });
1131
- if (conflict) reload(true);
1379
+ if (conflict) {
1380
+ const { generation, signal } = beginGeneration();
1381
+ waitForNamespaceRevisionChange(describe, LLM_PI_AI_NS, draft.revision, signal).then((outcome) => {
1382
+ if (outcome === "aborted" || !generationIsCurrent(generationRef, generation)) return;
1383
+ return loadSnapshotThenSettle(generation, true);
1384
+ }, (failure) => {
1385
+ failGeneration(generation, failure);
1386
+ });
1387
+ }
1132
1388
  return;
1133
1389
  }
1134
1390
  const view = response.result.value;
1391
+ echoedRevisionRef.current = view.revision;
1135
1392
  describe.acceptView(view);
1136
- setDrafts((current) => applySaveSuccess(current, draft.provider, {
1393
+ applyDrafts(applySaveSuccess(draftsRef.current, draft.provider, {
1137
1394
  user: view.user ?? {},
1138
1395
  revision: view.revision
1139
1396
  }));
@@ -1147,7 +1404,9 @@ window.__ModuleLoader__.load({
1147
1404
  text: failure instanceof Error ? failure.message : t("loadError")
1148
1405
  });
1149
1406
  } finally {
1407
+ busyRouteRef.current = null;
1150
1408
  setBusyRoute(null);
1409
+ flushPendingSettings(refreshAtRevision);
1151
1410
  }
1152
1411
  };
1153
1412
  const showLoading = status === "loading" && drafts.length === 0;
@@ -1182,7 +1441,7 @@ window.__ModuleLoader__.load({
1182
1441
  type: "button",
1183
1442
  className: effort_declare_module_css_default.secondaryButton,
1184
1443
  onClick: () => {
1185
- reload(true);
1444
+ reload(true, snapshotMode());
1186
1445
  },
1187
1446
  children: t("reload")
1188
1447
  }) : null,
@@ -1205,21 +1464,25 @@ window.__ModuleLoader__.load({
1205
1464
  t,
1206
1465
  onChange: (next) => {
1207
1466
  patchNotice(next.provider, void 0);
1208
- setDrafts((current) => current.map((row) => row.provider === next.provider ? next : row));
1467
+ applyDrafts((current) => current.map((row) => row.provider === next.provider ? next : row));
1209
1468
  },
1210
1469
  onSave: (next) => {
1211
1470
  save(next);
1212
1471
  },
1213
1472
  onCancel: (next) => {
1214
1473
  patchNotice(next.provider, void 0);
1215
- setDrafts((current) => current.map((row) => row.provider === next.provider ? {
1474
+ applyDrafts((current) => current.map((row) => row.provider === next.provider ? {
1216
1475
  ...row,
1217
1476
  models: cloneModels(row.originalModels),
1218
1477
  compat: cloneObject(row.originalCompat)
1219
1478
  } : row));
1220
1479
  }
1221
1480
  }, draft.provider))
1222
- }) : null
1481
+ }) : null,
1482
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1483
+ className: effort_declare_module_css_default.footer,
1484
+ children: PLUGIN_FOOTER_TEXT
1485
+ })
1223
1486
  ]
1224
1487
  });
1225
1488
  }
@@ -1229,7 +1492,7 @@ window.__ModuleLoader__.load({
1229
1492
  const zh = {
1230
1493
  nav: "推理档位",
1231
1494
  title: "推理档位声明",
1232
- intro: "本页声明「这模型能选哪些推理档」。对话里的档位选择仍在输入框模型菜单。没声明 = 和现在一样没有 Effort 行。不会改密钥、目录或当次选择。",
1495
+ intro: "声明手工模型可提供的推理档位,以及是否接受图片输入。未声明则不显示 Effort 行,图片附件也会被拒绝。档位仍在输入框菜单中选择;密钥、目录与当前选中项不在本页修改。",
1233
1496
  empty: "没有可编辑的手工 openai-completions 路由。",
1234
1497
  emptyHint: "请先到「模型」页添加提供方(llm-pi-ai 手工路由)。官方 DeepSeek 与 catalog 路由不在本页编辑。需要 DSH ≥ 0.1.0-rc.8(providers.declared)。",
1235
1498
  loadError: "无法加载提供方或设置。",
@@ -1239,7 +1502,7 @@ window.__ModuleLoader__.load({
1239
1502
  saving: "保存中…",
1240
1503
  saveBusy: "另有路由正在保存,请稍候。",
1241
1504
  cancel: "取消",
1242
- saved: "已保存。对话选择器会按新的能力声明显示 Effort 行。",
1505
+ saved: "已保存。对话会按新的能力声明更新 Effort 行和是否允许贴图。",
1243
1506
  conflict: "设置已被其他地方改过,请重新加载后再保存。",
1244
1507
  dirtyConflict: "此路由有未保存修改,设置已在其他地方更新。保存会写到最新文档上,或先取消再改。",
1245
1508
  presets: "预设",
@@ -1267,12 +1530,15 @@ window.__ModuleLoader__.load({
1267
1530
  errorOffOnly: "不能只开 Off,必须再开一档思考档。",
1268
1531
  errorBadWire: "思考档必须填写线上拼写;只有 Off 可以留空。不能使用未知档位键。",
1269
1532
  noModels: "这条路由还没有 models 列表。请先到「模型」页添加模型。",
1270
- reload: "重新加载"
1533
+ reload: "重新加载",
1534
+ imageInput: "支持图片输入",
1535
+ imageInputHint: "只在这个模型确实能看图时勾选。勾错了图会进聊天记录,然后被接口打回来。",
1536
+ errorBadInput: "input 只能是 text / image,必须包含 text,不能重复或留空。不支持图片请清除该键,不要写成空列表。"
1271
1537
  };
1272
1538
  const en = {
1273
1539
  nav: "Reasoning efforts",
1274
1540
  title: "Reasoning effort declarations",
1275
- intro: "This page declares which reasoning levels a model can offer. The per-turn choice stays in the composer model menu. Undeclared models keep having no Effort row. Keys, catalogs, and the current selection are not edited here.",
1541
+ intro: "Declare reasoning levels and image input for hand-added models. Undeclared models omit the Effort row and refuse image attachments. Effort is still chosen in the composer menu; keys, catalogs, and the current selection are not modified here.",
1276
1542
  empty: "No editable hand-declared openai-completions routes.",
1277
1543
  emptyHint: "Add a provider on the Models page first (an llm-pi-ai hand-declared route). Official DeepSeek and catalog routes are not edited here. Requires DSH ≥ 0.1.0-rc.8 (providers.declared).",
1278
1544
  loadError: "Could not load providers or settings.",
@@ -1282,7 +1548,7 @@ window.__ModuleLoader__.load({
1282
1548
  saving: "Saving…",
1283
1549
  saveBusy: "Another route is saving. Wait, then save this card.",
1284
1550
  cancel: "Cancel",
1285
- saved: "Saved. The composer Effort row follows this capability declaration.",
1551
+ saved: "Saved. The composer Effort row and image admission follow this capability declaration.",
1286
1552
  conflict: "Settings changed elsewhere. Reload, then save again.",
1287
1553
  dirtyConflict: "This route has unsaved edits and settings changed elsewhere. Save writes onto the latest document, or cancel first.",
1288
1554
  presets: "Presets",
@@ -1310,7 +1576,10 @@ window.__ModuleLoader__.load({
1310
1576
  errorOffOnly: "Off alone is not enough; declare at least one thinking level.",
1311
1577
  errorBadWire: "Thinking levels need a wire spelling; only Off may be empty. Unknown effort keys are rejected.",
1312
1578
  noModels: "This route has no models list yet. Add models on the Models page first.",
1313
- reload: "Reload"
1579
+ reload: "Reload",
1580
+ imageInput: "Accepts image input",
1581
+ imageInputHint: "Check only if this model actually sees images. Over-claiming admits a picture into history, then the gateway rejects the turn.",
1582
+ errorBadInput: "input may only list text and image, must include text, and must not be empty or duplicated. To drop vision, unset the key; do not write []."
1314
1583
  };
1315
1584
  //#endregion
1316
1585
  //#region src/client/index.ts
@@ -1357,22 +1626,25 @@ window.__ModuleLoader__.load({
1357
1626
  const describe = ctx.settingsScope.describe();
1358
1627
  const invalidation = /* @__PURE__ */ new Set();
1359
1628
  ctx.effect(() => {
1360
- const emit = (source) => {
1361
- for (const listener of invalidation) listener(source);
1629
+ const emit = (event) => {
1630
+ for (const listener of invalidation) listener(event);
1362
1631
  };
1363
1632
  const disposers = [
1364
1633
  describe.subscribe(() => {
1365
- emit("writable");
1634
+ emit({ source: "writable" });
1366
1635
  }),
1367
- ctx.remote.$on("settings/document-updated", (ns) => {
1636
+ ctx.remote.$on("settings/document-updated", (ns, revision) => {
1368
1637
  if (ns !== "llm-pi-ai") return;
1369
- emit("settings");
1638
+ emit({
1639
+ source: "settings",
1640
+ revision
1641
+ });
1370
1642
  }),
1371
1643
  ctx.remote.$on("llm/adapters-updated", () => {
1372
- emit("directory");
1644
+ emit({ source: "directory" });
1373
1645
  }),
1374
1646
  ctx.on("connection/reset", () => {
1375
- emit("directory");
1647
+ emit({ source: "reset" });
1376
1648
  })
1377
1649
  ];
1378
1650
  return () => {