dsh-tool-vision 0.7.1 → 0.8.0

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
@@ -40,6 +40,10 @@ window.__ModuleLoader__.load({
40
40
  ".__tv_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}" +
41
41
  ".__tv_btn:disabled{opacity:.5;cursor:default}" +
42
42
  ".__tv_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #3964fe);background:var(--dsw-alias-state-business-primary, #3964fe);color:#fff}" +
43
+ ".__tv_btnDanger{border-color:var(--dsw-alias-state-error-primary, #e5484d);color:var(--dsw-alias-state-error-primary, #e5484d)}" +
44
+ ".__tv_btnDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary, #e5484d);color:#fff}" +
45
+ ".__tv_master{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;margin:2px 0 6px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}" +
46
+ ".__tv_masterLabel{font-size:13px;color:var(--dsw-alias-label-primary)}" +
43
47
  ".__tv_status{font-size:12px;color:var(--dsw-alias-label-tertiary)}" +
44
48
  ".__tv_error{font-size:12px;color:var(--dsw-alias-state-error-primary)}" +
45
49
  ".__tv_unavailable{font-size:13px;color:var(--dsw-alias-label-tertiary)}";
@@ -97,7 +101,14 @@ window.__ModuleLoader__.load({
97
101
  error: "保存失败",
98
102
  unavailable: "设置命名空间不可用(服务端未注册 tool-vision 命名空间?)",
99
103
  overridden: "已覆盖",
100
- loading: "加载中…"
104
+ loading: "加载中…",
105
+ fieldEnabled: "启用插件(关闭后所有视觉工具下线)",
106
+ masterSwitch: "总开关",
107
+ turnOff: "一键关闭",
108
+ turnOn: "重新启用",
109
+ enabledNotice: "已启用:视觉工具已上线",
110
+ disabledNotice: "已关闭:所有视觉工具已下线",
111
+ notApplied: "写入未生效"
101
112
  };
102
113
  var en = {
103
114
  nav: "Vision Model",
@@ -141,11 +152,19 @@ window.__ModuleLoader__.load({
141
152
  error: "Save failed",
142
153
  unavailable: "Settings namespace unavailable (tool-vision namespace not registered server-side?)",
143
154
  overridden: "overridden",
144
- loading: "Loading…"
155
+ loading: "Loading…",
156
+ fieldEnabled: "Enable plugin (off takes every vision tool offline)",
157
+ masterSwitch: "Master switch",
158
+ turnOff: "Disable all",
159
+ turnOn: "Re-enable",
160
+ enabledNotice: "Enabled: vision tools are live",
161
+ disabledNotice: "Disabled: every vision tool is offline",
162
+ notApplied: "Write did not take effect"
145
163
  };
146
164
 
147
165
  // ── field spec ────────────────────────────────────────────────────────
148
166
  var FIELDS = [
167
+ { key: "enabled", label: "fieldEnabled", type: "checkbox" },
149
168
  { key: "baseURL", label: "fieldBaseUrl", type: "text", placeholder: "https://api.openai.com/v1" },
150
169
  { key: "apiKey", label: "fieldApiKey", type: "password", secret: true },
151
170
  { key: "apiKeyEnv", label: "fieldApiKeyEnv", type: "text" },
@@ -196,7 +215,10 @@ window.__ModuleLoader__.load({
196
215
  var [error, setError] = react.useState(null);
197
216
 
198
217
  react.useEffect(function () {
199
- if (typeof scope.load === "function") scope.load();
218
+ // No refresh call: the scope's public seam has no load() — reads ride the
219
+ // shared describe mirror, which re-reads on every Host
220
+ // `settings/document-updated`. The guarded scope.load() that used to sit
221
+ // here was dead code that read like a refresh that never happened.
200
222
  var alive = true;
201
223
  var sync = function () { if (alive) setSnapshot(scope.getSnapshot()); };
202
224
  var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
@@ -239,66 +261,154 @@ window.__ModuleLoader__.load({
239
261
  setError(null);
240
262
  }
241
263
 
242
- function onSave() {
243
- setBusy(true); setNotice(null); setError(null);
244
- var writes = FIELDS.map(function (f) {
264
+ // One atomic mutation carrying ONE revision fence.
265
+ //
266
+ // The fields used to be issued as parallel scope.set()/unset() calls. Every
267
+ // write is queued and re-reads the namespace revision, but a write whose
268
+ // fence is behind the Host document is REFUSED with settings/conflict — and
269
+ // a refused write still resolves (the scope's contract is "settle after any
270
+ // recovery read", not "throw on refusal"). The section therefore reported
271
+ // "Saved" while the edits silently reverted. Batching every change into a
272
+ // single mutate() gives it one fence and one persistence decision.
273
+ function buildOps() {
274
+ var ops = [];
275
+ FIELDS.forEach(function (f) {
245
276
  var d = fieldDraft(f);
246
277
  if (f.type === "csv") {
247
278
  var arr = String(d).split(",").map(function (s) { return s.trim(); }).filter(Boolean);
248
279
  var cur = value[f.key] || [];
249
- if (arr.length === cur.length && arr.every(function (x, i) { return x === cur[i]; })) return Promise.resolve();
250
- return scope.set(f.key, arr);
280
+ if (arr.length === cur.length && arr.every(function (x, i) { return x === cur[i]; })) return;
281
+ ops.push({ op: "set", path: [f.key], value: arr });
282
+ return;
251
283
  }
252
284
  if (f.type === "checkbox") {
253
- if (Boolean(d) === Boolean(value[f.key])) return Promise.resolve();
254
- return Boolean(d) ? scope.set(f.key, true) : scope.unset(f.key);
285
+ if (Boolean(d) === Boolean(value[f.key])) return;
286
+ ops.push(Boolean(d) ? { op: "set", path: [f.key], value: true }
287
+ : { op: "unset", path: [f.key] });
288
+ return;
255
289
  }
256
290
  if (f.type === "password") {
257
- if (!d) return Promise.resolve(); // blank keeps the current key
258
- if (d === String(value[f.key] ?? "")) return Promise.resolve();
259
- return scope.set(f.key, d);
291
+ if (!d) return; // blank keeps the current key
292
+ if (d === String(value[f.key] ?? "")) return;
293
+ ops.push({ op: "set", path: [f.key], value: d });
294
+ return;
260
295
  }
261
- if (String(d) === String(value[f.key] ?? "")) return Promise.resolve();
262
- if (String(d).trim() === "" && !(f.key in user)) return Promise.resolve();
263
- return String(d).trim() === "" ? scope.unset(f.key) : scope.set(f.key, f.type === "number" ? Number(d) : d);
296
+ if (String(d) === String(value[f.key] ?? "")) return;
297
+ if (String(d).trim() === "" && !(f.key in user)) return;
298
+ ops.push(String(d).trim() === ""
299
+ ? { op: "unset", path: [f.key] }
300
+ : { op: "set", path: [f.key], value: f.type === "number" ? Number(d) : d });
301
+ });
302
+ return ops;
303
+ }
304
+
305
+ /** Whether the namespace section now reflects every queued operation. */
306
+ function opsApplied(ops, snap) {
307
+ if (snap.status !== "ready" || snap.value === void 0) return false;
308
+ return ops.every(function (op) {
309
+ var key = op.path[0];
310
+ if (op.op === "unset") return !(snap.user && key in snap.user);
311
+ return JSON.stringify(snap.value[key]) === JSON.stringify(op.value);
312
+ });
313
+ }
314
+
315
+ /**
316
+ * Settle one namespace change and report what actually happened.
317
+ *
318
+ * Reads settle after the write and any recovery read, so the section is
319
+ * inspected afterwards instead of trusting the promise: that check is the
320
+ * only way to tell a committed change from a refused one. Hosts without
321
+ * mutate() apply the same ops in order — each waits for its predecessor, so
322
+ * the revision chain still holds.
323
+ */
324
+ function commit(ops) {
325
+ var run;
326
+ if (typeof scope.mutate === "function") {
327
+ run = scope.mutate(ops, scope.getSnapshot().revision);
328
+ } else {
329
+ run = ops.reduce(function (chain, op) {
330
+ return chain.then(function () {
331
+ return op.op === "unset" ? scope.unset(op.path[0]) : scope.set(op.path[0], op.value);
332
+ });
333
+ }, Promise.resolve());
334
+ }
335
+ return Promise.resolve(run).then(function () {
336
+ return opsApplied(ops, scope.getSnapshot());
337
+ });
338
+ }
339
+
340
+ function reportOutcome(ok, okMessage) {
341
+ setBusy(false);
342
+ if (ok) {
343
+ setNotice(okMessage || t("saved"));
344
+ return;
345
+ }
346
+ setError(t("error") + ":" + t("notApplied"));
347
+ reseedDraft();
348
+ }
349
+
350
+ function onSave() {
351
+ setBusy(true); setNotice(null); setError(null);
352
+ var ops = buildOps();
353
+ if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
354
+ commit(ops).then(function (ok) {
355
+ reportOutcome(ok);
356
+ }).catch(function (e) {
357
+ setBusy(false); setError(t("error") + ":" + String(e && e.message || e));
264
358
  });
265
- Promise.all(writes).then(function () {
266
- setBusy(false); setNotice(t("saved"));
267
- if (typeof scope.load === "function") scope.load();
359
+ }
360
+
361
+ // One-click master switch: the same field the checkbox edits, written on its
362
+ // own so taking the plugin offline never depends on the rest of the form.
363
+ function onToggleEnabled() {
364
+ setBusy(true); setNotice(null); setError(null);
365
+ var next = value.enabled === false;
366
+ commit([{ op: "set", path: ["enabled"], value: next }]).then(function (ok) {
367
+ reportOutcome(ok, next ? t("enabledNotice") : t("disabledNotice"));
268
368
  }).catch(function (e) {
269
- setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
369
+ setBusy(false); setError(t("error") + ":" + String(e && e.message || e));
270
370
  });
271
371
  }
272
372
 
273
373
  function reseedDraft() {
274
- if (typeof scope.load === "function") {
275
- var p = scope.load();
276
- if (p && typeof p.then === "function") {
277
- p.then(function () {
278
- var fresh = scope.getSnapshot();
279
- if (fresh.status === "ready" && fresh.value !== void 0) setDraft(Object.assign({}, valueToDraft(fresh.value)));
280
- }).catch(function () {});
281
- return;
282
- }
374
+ // The mirror folds a write's answer in before the promise settles, so the
375
+ // snapshot is already current; the timeout only covers a host that
376
+ // answers late.
377
+ var fresh = scope.getSnapshot();
378
+ if (fresh.status === "ready" && fresh.value !== void 0) {
379
+ setDraft(Object.assign({}, valueToDraft(fresh.value)));
380
+ return;
283
381
  }
284
382
  setTimeout(function () {
285
- var fresh = scope.getSnapshot();
286
- if (fresh.status === "ready" && fresh.value !== void 0) setDraft(Object.assign({}, valueToDraft(fresh.value)));
383
+ var later = scope.getSnapshot();
384
+ if (later.status === "ready" && later.value !== void 0) setDraft(Object.assign({}, valueToDraft(later.value)));
287
385
  }, 120);
288
386
  }
289
387
 
290
388
  function onReset() {
291
389
  setBusy(true); setNotice(null); setError(null);
292
- Promise.all(FIELDS.map(function (f) { return scope.unset(f.key); })).then(function () {
293
- setBusy(false); setNotice(t("saved"));
294
- reseedDraft();
390
+ var ops = FIELDS
391
+ .filter(function (f) { return f.key in user; })
392
+ .map(function (f) { return { op: "unset", path: [f.key] }; });
393
+ if (ops.length === 0) { setBusy(false); setNotice(t("saved")); return; }
394
+ commit(ops).then(function (ok) {
395
+ reportOutcome(ok);
295
396
  }).catch(function (e) {
296
- setBusy(false); setError(t("error") + ": " + String(e && e.message || e));
397
+ setBusy(false); setError(t("error") + ":" + String(e && e.message || e));
297
398
  });
298
399
  }
299
400
 
300
401
  return h("div", { className: "__tv_root" },
301
402
  h("p", { className: "__tv_hint", style: { margin: "0 0 4px" } }, t("intro")),
403
+ h("div", { className: "__tv_master" },
404
+ h("span", { className: "__tv_masterLabel" },
405
+ t("masterSwitch") + (value.enabled === false ? ":" + t("disabledNotice") : "")),
406
+ h("button", {
407
+ type: "button",
408
+ className: "__tv_btn " + (value.enabled === false ? "__tv_btnPrimary" : "__tv_btnDanger"),
409
+ onClick: onToggleEnabled,
410
+ disabled: busy || !snapshot.writable
411
+ }, value.enabled === false ? t("turnOn") : t("turnOff"))),
302
412
  FIELDS.map(function (f) {
303
413
  var overridden = f.key in user;
304
414
  if (f.type === "checkbox") {
@@ -500,7 +610,6 @@ window.__ModuleLoader__.load({
500
610
  var t = ctx.locale.bind(NS);
501
611
  ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-tool-vision: dictionaries");
502
612
  var scope = ctx.settingsScope.bind({ namespace: "tool-vision" });
503
- if (typeof scope.load === "function") scope.load();
504
613
  attachBridgePreview(ctx, scope);
505
614
  ctx.slots.inject("settings.section", function () {
506
615
  return ctx.slots.register({