@mars-sea/dsh-commandcode-provider 0.11.8 → 0.11.10

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/lib/client.js CHANGED
@@ -36,6 +36,424 @@ window.__ModuleLoader__.load({
36
36
  };
37
37
  }
38
38
  //#endregion
39
+ //#region src/client/settings-scope.ts
40
+ const MEMORY_STATUS = "unavailable";
41
+ /**
42
+ * Backoff before re-reading a directory whose FIRST read failed, in millis.
43
+ *
44
+ * A first read can lose to a Host that is still coming up (the gateway's WS
45
+ * handshake, a settings service mid-start): the failure is transient, but the
46
+ * scope has nothing held, so it keeps its initial `{ status: 'loading',
47
+ * writable: false }` snapshot — and every control on the settings page renders
48
+ * disabled behind a "read-only" banner. Nothing else would ever re-read it:
49
+ * the forwarded `settings/document-updated` and `connection/reset` signals need
50
+ * a live Host to fire, and the namespace only re-mounts on a page reload. So a
51
+ * failure with NOTHING held retries on this bounded ladder; a failure with a
52
+ * held view keeps it (no retry — the page is already showing the last good
53
+ * document, and `tests/settings-scope.test.ts` pins that).
54
+ */
55
+ const SETTINGS_DESCRIBE_RETRY_MS = [
56
+ 1e3,
57
+ 2e3,
58
+ 4e3
59
+ ];
60
+ /** The production timer: a browser/node timeout, no polling. */
61
+ const REAL_SETTINGS_TIMER = {
62
+ set(callback, ms) {
63
+ const handle = setTimeout(callback, ms);
64
+ handle.unref?.();
65
+ return handle;
66
+ },
67
+ clear(handle) {
68
+ clearTimeout(handle);
69
+ }
70
+ };
71
+ /** Shallow equality over the snapshot fields, so an unchanged derive never churns subscribers. */
72
+ function sameSnapshot(a, b) {
73
+ return a.status === b.status && Object.is(a.value, b.value) && Object.is(a.base, b.base) && Object.is(a.user, b.user) && Object.is(a.revision, b.revision) && a.writable === b.writable && a.mode === b.mode;
74
+ }
75
+ /** Accept a decoded row value only when it is the object a settings section is. */
76
+ function decodeRow(value) {
77
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
78
+ return value;
79
+ }
80
+ /**
81
+ * One shared mirror of the Host `settings.describe()` view.
82
+ *
83
+ * The run loop is the harness's own: serialize behind `inFlight`, fold a
84
+ * concurrent invalidation into exactly one rerun, and hold the previous view
85
+ * across a failed read (status stays `ready` with an `error`, or returns to
86
+ * `idle` when nothing was ever held).
87
+ */
88
+ var SettingsDescribeMirror = class {
89
+ resolveRemote;
90
+ timer;
91
+ store;
92
+ persistence;
93
+ inFlight;
94
+ rerun = false;
95
+ generation = 0;
96
+ /** Pending retry handle and how many of {@link SETTINGS_DESCRIBE_RETRY_MS} are spent. */
97
+ retryHandle;
98
+ retries = 0;
99
+ disposed = false;
100
+ /**
101
+ * @param resolveRemote - Reads the inject-captured `remote.settings` namespace.
102
+ * @param persistence - Client-selected Host persistence; non-loopback pages may stay process-local.
103
+ * @param timer - Retry timer seam (see {@link SETTINGS_DESCRIBE_RETRY_MS}).
104
+ */
105
+ constructor(resolveRemote, persistence, timer = REAL_SETTINGS_TIMER) {
106
+ this.resolveRemote = resolveRemote;
107
+ this.timer = timer;
108
+ this.persistence = persistence;
109
+ this.store = createSnapshotStore({
110
+ status: persistence === "host" ? "idle" : MEMORY_STATUS,
111
+ view: null,
112
+ error: null
113
+ });
114
+ }
115
+ getSnapshot() {
116
+ return this.store.getSnapshot();
117
+ }
118
+ subscribe(listener) {
119
+ return this.store.subscribe(listener);
120
+ }
121
+ /** The cheap idempotent entry: kick a first read (a no-op on memory persistence). */
122
+ ensure() {
123
+ this.load();
124
+ }
125
+ /**
126
+ * Refresh from the Host. A call during an in-flight read marks one rerun
127
+ * after it settles instead of racing a second wire read.
128
+ * @returns settlement after this call's freshness is reflected.
129
+ */
130
+ load() {
131
+ if (this.disposed || this.persistence === "memory") return Promise.resolve();
132
+ this.clearRetry();
133
+ if (this.inFlight !== void 0) {
134
+ this.rerun = true;
135
+ return this.inFlight;
136
+ }
137
+ const run = Promise.resolve().then(() => this.run());
138
+ this.inFlight = run;
139
+ return run;
140
+ }
141
+ /**
142
+ * Fold one write answer's namespace ROW into the held view without a wire
143
+ * read, and invalidate any read still in flight: a read begun before the
144
+ * write committed must not publish the pre-write document over it. With no
145
+ * held document the answer is not published as a partial document — the
146
+ * next read (or the failed-write recovery) picks it up.
147
+ * @param row - The namespace view a settings write answered with.
148
+ */
149
+ acceptView(row) {
150
+ const before = this.store.getSnapshot();
151
+ this.generation += 1;
152
+ if (this.inFlight !== void 0) this.rerun = true;
153
+ if (before.view === null) return;
154
+ const held = before.view;
155
+ const namespaces = held.namespaces?.some((candidate) => candidate.ns === row.ns) ? held.namespaces.map((candidate) => candidate.ns === row.ns ? row : candidate) : [...held.namespaces ?? [], row];
156
+ this.store.set({
157
+ ...before,
158
+ view: {
159
+ ...held,
160
+ namespaces
161
+ }
162
+ });
163
+ }
164
+ async run() {
165
+ try {
166
+ for (;;) {
167
+ const before = this.store.getSnapshot();
168
+ if (before.status === "idle") this.store.set({
169
+ ...before,
170
+ status: "loading"
171
+ });
172
+ this.rerun = false;
173
+ const generation = ++this.generation;
174
+ let outcome;
175
+ try {
176
+ const namespace = this.resolveRemote();
177
+ if (namespace === void 0) throw new Error("the settings remote namespace is not mounted");
178
+ const response = await namespace.describe();
179
+ if (response.ok) {
180
+ const value = response.value;
181
+ outcome = typeof value === "object" && value !== null ? { view: value } : { failure: "settings describe answered no view" };
182
+ } else outcome = { failure: response.error?.message ?? "settings describe failed" };
183
+ } catch (error) {
184
+ outcome = { failure: error instanceof Error ? error.message : String(error) };
185
+ }
186
+ if (generation !== this.generation) continue;
187
+ if ("view" in outcome) {
188
+ this.retries = 0;
189
+ this.store.set({
190
+ status: "ready",
191
+ view: outcome.view,
192
+ error: null
193
+ });
194
+ } else {
195
+ const held = this.store.getSnapshot();
196
+ this.store.set({
197
+ status: held.view === null ? "idle" : "ready",
198
+ view: held.view,
199
+ error: outcome.failure
200
+ });
201
+ if (held.view === null) this.scheduleRetry();
202
+ }
203
+ if (!this.rerun) break;
204
+ }
205
+ } finally {
206
+ this.inFlight = void 0;
207
+ }
208
+ }
209
+ /**
210
+ * Schedule the next retry of a failed FIRST read ({@link SETTINGS_DESCRIBE_RETRY_MS}).
211
+ * Bounded: once the ladder is spent the scope keeps its degraded snapshot,
212
+ * exactly as before, until an invalidation or a namespace re-mount arrives.
213
+ */
214
+ scheduleRetry() {
215
+ if (this.disposed || this.retries >= SETTINGS_DESCRIBE_RETRY_MS.length) return;
216
+ const delay = SETTINGS_DESCRIBE_RETRY_MS[this.retries];
217
+ this.retries += 1;
218
+ this.retryHandle = this.timer.set(() => {
219
+ this.retryHandle = void 0;
220
+ this.load();
221
+ }, delay);
222
+ }
223
+ /** Cancel a pending retry (a live read is starting, or the scope is going away). */
224
+ clearRetry() {
225
+ if (this.retryHandle !== void 0) this.timer.clear(this.retryHandle);
226
+ this.retryHandle = void 0;
227
+ }
228
+ /** Stop the mirror: no further reads, and no retry may fire after disposal. */
229
+ dispose() {
230
+ this.disposed = true;
231
+ this.clearRetry();
232
+ }
233
+ };
234
+ /**
235
+ * One namespace's derived scope over the shared mirror, plus its serialized
236
+ * Host writes. This is the face `CommandCodeSettingsController` consumes
237
+ * (`getSnapshot` / `subscribe` / `set` / `unset`), so the controller itself
238
+ * needs no changes for either generation.
239
+ */
240
+ var RemoteSettingsScope = class {
241
+ resolveRemote;
242
+ store;
243
+ mirror;
244
+ namespace;
245
+ persistence;
246
+ tail = Promise.resolve();
247
+ writeGeneration = 0;
248
+ disposed = false;
249
+ unsubscribe;
250
+ /** Revision answered by a superseded write still ahead of the mirror. */
251
+ pendingRevision;
252
+ constructor(resolveRemote, mirror, namespace, persistence) {
253
+ this.resolveRemote = resolveRemote;
254
+ this.mirror = mirror;
255
+ this.namespace = namespace;
256
+ this.persistence = persistence;
257
+ this.store = createSnapshotStore({
258
+ status: persistence === "host" ? "loading" : "unavailable",
259
+ value: void 0,
260
+ base: void 0,
261
+ user: void 0,
262
+ revision: void 0,
263
+ writable: false,
264
+ mode: persistence
265
+ });
266
+ if (persistence === "host") {
267
+ this.unsubscribe = mirror.subscribe(() => {
268
+ this.derive();
269
+ });
270
+ this.derive();
271
+ }
272
+ }
273
+ getSnapshot() {
274
+ return this.store.getSnapshot();
275
+ }
276
+ subscribe(listener) {
277
+ return this.store.subscribe(listener);
278
+ }
279
+ /** Queue one field write (the single-path `set` op form). */
280
+ set(field, value) {
281
+ return this.mutate([{
282
+ op: "set",
283
+ path: [field],
284
+ value
285
+ }]);
286
+ }
287
+ /** Queue one field clear (the single-path `unset` op form). */
288
+ unset(field) {
289
+ return this.mutate([{
290
+ op: "unset",
291
+ path: [field]
292
+ }]);
293
+ }
294
+ /**
295
+ * Queue one atomic namespace mutation behind the single-flight fence.
296
+ * A rejected write (e.g. `SETTINGS_CONFLICT`) re-reads the Host so the
297
+ * snapshot reports what actually landed; an accepted one folds its answer
298
+ * in directly — unless a newer write already superseded it, in which case
299
+ * that answer's revision becomes the next write's fence.
300
+ */
301
+ mutate(ops, expectedRevision) {
302
+ const ownedOps = structuredClone(ops);
303
+ const generation = ++this.writeGeneration;
304
+ return this.enqueue(async () => {
305
+ const revision = expectedRevision ?? this.pendingRevision ?? this.getSnapshot().revision;
306
+ const response = await this.mutateRemote(ownedOps, revision);
307
+ if (!response.ok) {
308
+ await this.recover(generation);
309
+ return;
310
+ }
311
+ if (this.disposed) return;
312
+ const row = response.value;
313
+ if (generation === this.writeGeneration) {
314
+ this.pendingRevision = void 0;
315
+ this.mirror.acceptView(row);
316
+ } else this.pendingRevision = typeof row.revision === "number" ? row.revision : void 0;
317
+ });
318
+ }
319
+ async mutateRemote(ops, revision) {
320
+ try {
321
+ const namespace = this.resolveRemote();
322
+ if (namespace === void 0) return {
323
+ ok: false,
324
+ error: { message: "the settings remote namespace is not mounted" }
325
+ };
326
+ return await namespace.mutate(this.namespace, ops, revision);
327
+ } catch (error) {
328
+ return {
329
+ ok: false,
330
+ error: { message: error instanceof Error ? error.message : String(error) }
331
+ };
332
+ }
333
+ }
334
+ /** Reload Host state for the latest failed write; superseded failures leave recovery to it. */
335
+ async recover(generation) {
336
+ if (this.disposed || generation !== this.writeGeneration) return;
337
+ this.pendingRevision = void 0;
338
+ await this.mirror.load();
339
+ }
340
+ /** Derive this namespace's snapshot from the mirror's held view. */
341
+ derive() {
342
+ if (this.disposed) return;
343
+ const mirrored = this.mirror.getSnapshot();
344
+ if (mirrored.view === null || mirrored.view === void 0) return;
345
+ const writable = mirrored.view.writable === true;
346
+ const row = mirrored.view.namespaces?.find((candidate) => candidate.ns === this.namespace);
347
+ if (row === void 0) {
348
+ this.publish({
349
+ status: "unavailable",
350
+ writable
351
+ });
352
+ return;
353
+ }
354
+ const decoded = decodeRow(row.value);
355
+ if (decoded === void 0) return;
356
+ this.publish({
357
+ status: "ready",
358
+ writable,
359
+ value: decoded,
360
+ base: row.base,
361
+ user: row.user,
362
+ revision: typeof row.revision === "number" ? row.revision : void 0
363
+ });
364
+ }
365
+ /** Publish `next` over the current snapshot, skipping the write when nothing changed. */
366
+ publish(next) {
367
+ const current = this.store.getSnapshot();
368
+ const candidate = {
369
+ status: next.status,
370
+ value: "value" in next ? next.value : current.value,
371
+ base: "base" in next ? next.base : current.base,
372
+ user: "user" in next ? next.user : current.user,
373
+ revision: "revision" in next ? next.revision : current.revision,
374
+ writable: next.writable,
375
+ mode: current.mode
376
+ };
377
+ if (sameSnapshot(candidate, current)) return;
378
+ this.store.set(candidate);
379
+ }
380
+ /** Queue operations one at a time; memory persistence and disposal are no-ops. */
381
+ enqueue(operation) {
382
+ if (this.persistence === "memory" || this.disposed) return Promise.resolve();
383
+ const task = this.tail.then(async () => {
384
+ if (this.disposed) return;
385
+ await operation();
386
+ });
387
+ this.tail = task.catch(() => {});
388
+ return task;
389
+ }
390
+ /** Stop queued operations, stop deriving, and wait for the current wire call to settle. */
391
+ async dispose() {
392
+ this.disposed = true;
393
+ this.writeGeneration += 1;
394
+ this.unsubscribe?.();
395
+ await this.tail;
396
+ }
397
+ };
398
+ /**
399
+ * Build the plugin's settings scope for one namespace.
400
+ *
401
+ * Subscribes the two signals that can move the Host view — the forwarded
402
+ * `settings/document-updated` invalidation and (when the client exposes it)
403
+ * `connection/reset` — kicks the first describe read, and derives the
404
+ * namespace row on every mirror change.
405
+ *
406
+ * `resolveRemote` is the INJECT-CAPTURED namespace (see
407
+ * {@link SettingsRemoteResolver}): `remote.settings` is a Cordis service nested
408
+ * under `remote`, so a read through our own context throws and has to be
409
+ * replaced by an object captured inside `ctx.inject(['remote.settings'], …)`.
410
+ * The caller that owns that inject calls {@link ManagedSettingsScope.refresh}
411
+ * once the namespace lands, which is what turns the first, namespace-less read
412
+ * into a served one; a profile that never mounts the namespace leaves the page
413
+ * in its degraded (`unavailable`, read-only) state instead of throwing.
414
+ *
415
+ * @param context - The client context slice (only the declared `remote` service is read).
416
+ * @param namespace - Settings namespace (profile entry id) this scope binds.
417
+ * @param resolveRemote - Reads the inject-captured `remote.settings` namespace.
418
+ * @returns The scope plus the refresh/dispose handles the entry wires into its fiber.
419
+ */
420
+ function createSettingsScope(context, namespace, resolveRemote, timer = REAL_SETTINGS_TIMER) {
421
+ const loopback = context.remote.$host?.isLoopback === true;
422
+ const persistence = loopback ? "host" : "memory";
423
+ const mirror = new SettingsDescribeMirror(resolveRemote, persistence, timer);
424
+ const scope = new RemoteSettingsScope(resolveRemote, mirror, namespace, persistence);
425
+ const disposers = [];
426
+ if (loopback) {
427
+ if (typeof context.remote.$on === "function") {
428
+ const off = context.remote.$on("settings/document-updated", () => {
429
+ mirror.ensure();
430
+ });
431
+ if (typeof off === "function") disposers.push(off);
432
+ }
433
+ if (typeof context.on === "function") {
434
+ const off = context.on("connection/reset", () => {
435
+ mirror.load();
436
+ });
437
+ if (typeof off === "function") disposers.push(off);
438
+ }
439
+ mirror.ensure();
440
+ }
441
+ return {
442
+ getSnapshot: () => scope.getSnapshot(),
443
+ subscribe: (listener) => scope.subscribe(listener),
444
+ set: (field, value) => scope.set(field, value),
445
+ unset: (field) => scope.unset(field),
446
+ refresh: () => {
447
+ mirror.load();
448
+ },
449
+ dispose: async () => {
450
+ for (const dispose of disposers.splice(0)) dispose();
451
+ mirror.dispose();
452
+ await scope.dispose();
453
+ }
454
+ };
455
+ }
456
+ //#endregion
39
457
  //#region src/client/settings.ts
40
458
  /**
41
459
  * Browser controller for the "Command Code" settings page.
@@ -56,10 +474,14 @@ window.__ModuleLoader__.load({
56
474
  * - Working dir, request/stream timeouts -> the same namespace.
57
475
  *
58
476
  * The controller mirrors the plugin-card pattern from the harness's own
59
- * settings UI: it binds the `llm-commandcode` namespace through the
60
- * `settingsScope` service, keeps a staged draft of edits, and writes them on
61
- * save through `scope.set` / the credentials domain. The Host stays the
62
- * single fact source; the snapshot is republished after each accepted write.
477
+ * settings UI: it binds the `llm-commandcode` namespace through a
478
+ * `SettingsScope` — this plugin's own binding of `remote.settings` (see
479
+ * `./settings-scope.ts`; the harness's `settingsScope` wrapper existed only
480
+ * through 0.1.6 and was removed with the 0.1.7 settings rewrite, while the
481
+ * wire underneath spans every supported release) — keeps a staged draft of
482
+ * edits, and writes them on save through `scope.set` / the credentials
483
+ * domain. The Host stays the single fact source; the snapshot is republished
484
+ * after each accepted write.
63
485
  *
64
486
  * This module is deliberately free of JSX — it only produces the state face
65
487
  * the React component renders.
@@ -164,6 +586,16 @@ window.__ModuleLoader__.load({
164
586
  booleanField$1("filterModelsByPlan"),
165
587
  booleanField$1("webSearch"),
166
588
  booleanField$1("showSidebarQuota"),
589
+ booleanField$1("commandGuard"),
590
+ numberField$1("commandGuardThreshold", {
591
+ min: .5,
592
+ max: 1
593
+ }),
594
+ numberField$1("commandGuardTimeoutMs", {
595
+ min: 200,
596
+ max: 1e4
597
+ }),
598
+ booleanField$1("zdr"),
167
599
  textField("activeAccount")
168
600
  ];
169
601
  /** Whether two model-id lists are equal as sets (order-insensitive). */
@@ -318,6 +750,10 @@ window.__ModuleLoader__.load({
318
750
  filterModelsByPlan: this.field("filterModelsByPlan"),
319
751
  webSearch: this.field("webSearch"),
320
752
  showSidebarQuota: this.field("showSidebarQuota"),
753
+ commandGuard: this.field("commandGuard"),
754
+ commandGuardThreshold: this.field("commandGuardThreshold"),
755
+ commandGuardTimeoutMs: this.field("commandGuardTimeoutMs"),
756
+ zdr: this.field("zdr"),
321
757
  sidebarQuota: this.sectionValue("showSidebarQuota") === true,
322
758
  activeAccount: this.field("activeAccount"),
323
759
  accounts,
@@ -2157,7 +2593,7 @@ window.__ModuleLoader__.load({
2157
2593
  }
2158
2594
  //#endregion
2159
2595
  //#region package.json
2160
- var version = "0.11.8";
2596
+ var version = "0.11.10";
2161
2597
  var repository = {
2162
2598
  "type": "git",
2163
2599
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -2359,7 +2795,11 @@ window.__ModuleLoader__.load({
2359
2795
  "transportMaxRetries",
2360
2796
  "filterModelsByPlan",
2361
2797
  "webSearch",
2362
- "showSidebarQuota"
2798
+ "showSidebarQuota",
2799
+ "commandGuard",
2800
+ "commandGuardThreshold",
2801
+ "commandGuardTimeoutMs",
2802
+ "zdr"
2363
2803
  ];
2364
2804
  /** One labelled field row in the page body. */
2365
2805
  function Field({ id, label, hint, state, disabled, numeric, placeholder, onEdit, onReset, t }) {
@@ -2545,6 +2985,49 @@ window.__ModuleLoader__.load({
2545
2985
  onEdit: (text) => onEdit("showSidebarQuota", text),
2546
2986
  onReset: () => onReset("showSidebarQuota"),
2547
2987
  t
2988
+ }),
2989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
2990
+ id: "cc-command-guard",
2991
+ label: t("commandGuard"),
2992
+ hint: t("commandGuardHint"),
2993
+ state: state.commandGuard,
2994
+ disabled,
2995
+ defaultChecked: false,
2996
+ onEdit: (text) => onEdit("commandGuard", text),
2997
+ onReset: () => onReset("commandGuard"),
2998
+ t
2999
+ }),
3000
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3001
+ id: "cc-command-guard-threshold",
3002
+ label: t("commandGuardThreshold"),
3003
+ hint: t("commandGuardThresholdHint"),
3004
+ state: state.commandGuardThreshold,
3005
+ disabled,
3006
+ onEdit: (text) => onEdit("commandGuardThreshold", text),
3007
+ onReset: () => onReset("commandGuardThreshold"),
3008
+ t
3009
+ }),
3010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3011
+ id: "cc-command-guard-timeout",
3012
+ label: t("commandGuardTimeoutMs"),
3013
+ hint: t("commandGuardTimeoutMsHint"),
3014
+ state: state.commandGuardTimeoutMs,
3015
+ disabled,
3016
+ numeric: true,
3017
+ onEdit: (text) => onEdit("commandGuardTimeoutMs", text),
3018
+ onReset: () => onReset("commandGuardTimeoutMs"),
3019
+ t
3020
+ }),
3021
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3022
+ id: "cc-zdr",
3023
+ label: t("zdr"),
3024
+ hint: t("zdrHint"),
3025
+ state: state.zdr,
3026
+ disabled,
3027
+ defaultChecked: false,
3028
+ onEdit: (text) => onEdit("zdr", text),
3029
+ onReset: () => onReset("zdr"),
3030
+ t
2548
3031
  })
2549
3032
  ]
2550
3033
  }) : null,
@@ -2812,13 +3295,21 @@ window.__ModuleLoader__.load({
2812
3295
  report.blocked !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2813
3296
  className: "cc-usageBlocked",
2814
3297
  role: "alert",
2815
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2816
- className: "cc-usageBlockedTitle",
2817
- children: blockedTitle(report.blocked, t)
2818
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2819
- className: "cc-usageBlockedHint",
2820
- children: blockedHint(report.blocked, t)
2821
- })]
3298
+ children: [
3299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3300
+ className: "cc-usageBlockedTitle",
3301
+ children: blockedTitle(report.blocked, t)
3302
+ }),
3303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3304
+ className: "cc-usageBlockedHint",
3305
+ children: blockedHint(report.blocked, t)
3306
+ }),
3307
+ report.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3308
+ className: "cc-usageBlockedDetail",
3309
+ title: report.failures.join("; "),
3310
+ children: report.failures.join(" · ")
3311
+ }) : null
3312
+ ]
2822
3313
  }) : null,
2823
3314
  report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2824
3315
  className: "cc-usageStats",
@@ -4122,21 +4613,23 @@ window.__ModuleLoader__.load({
4122
4613
  }
4123
4614
  /** The report-level failure box, or undefined when the report is usable. */
4124
4615
  function failureView(state) {
4125
- const blocked = state.report?.accounts.find((entry) => entry.report.blocked !== void 0)?.report.blocked;
4616
+ const blockedEntry = state.report?.accounts.find((entry) => entry.report.blocked !== void 0);
4617
+ const blocked = blockedEntry?.report.blocked;
4618
+ const detail = (blockedEntry?.report.failures ?? []).join(" · ");
4126
4619
  if (blocked === "invalid-key") return {
4127
4620
  title: "errorInvalidKey",
4128
4621
  hint: "errorInvalidKeyHint",
4129
- detail: ""
4622
+ detail
4130
4623
  };
4131
4624
  if (blocked === "service-unavailable") return {
4132
4625
  title: "errorServiceUnavailable",
4133
4626
  hint: "errorServiceUnavailableHint",
4134
- detail: ""
4627
+ detail
4135
4628
  };
4136
4629
  if (blocked === "network") return {
4137
4630
  title: "errorNetwork",
4138
4631
  hint: "errorNetworkHint",
4139
- detail: ""
4632
+ detail
4140
4633
  };
4141
4634
  if (state.status === "error" && state.report === void 0) return {
4142
4635
  title: "errorGeneric",
@@ -5791,6 +6284,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
5791
6284
  .cc-usageBlocked{border:1px solid var(--dsw-alias-label-error);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}
5792
6285
  .cc-usageBlockedTitle{color:var(--dsw-alias-label-error);margin:0;font-size:13px;font-weight:600;line-height:1.5}
5793
6286
  .cc-usageBlockedHint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:1.5}
6287
+ .cc-usageBlockedDetail{color:var(--dsw-alias-label-secondary);margin:0;font-size:11px;line-height:1.5;word-break:break-word;opacity:.85}
5794
6288
  .cc-version{margin:4px 0 0;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
5795
6289
  /* The Models-page provider panel root: unstyled by design — the row card the
5796
6290
  * Models page owns supplies the surface, the panel only stacks its controls. */
@@ -6017,6 +6511,14 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6017
6511
  webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址),并优先于其他搜索后端;关闭则把选择权交还给之前的后端(如 modsearch),而不是强制回退到 DeepSeek 搜索。",
6018
6512
  showSidebarQuota: "在侧边栏显示额度卡片",
6019
6513
  showSidebarQuotaHint: "开启后,侧边栏底部(Settings 上方)显示 Command Code 套餐与配额卡片,点击可在中间栏打开完整仪表盘。默认关闭:左侧不显示任何额度信息,也不会为其后台刷新用量。保存后立即生效。",
6514
+ commandGuard: "AI 命令安全预判",
6515
+ commandGuardHint: "命令确认弹窗前,先由 typesafe/jev 判断它是否安全:安全就自动放行,其余照常弹窗。默认关闭。",
6516
+ commandGuardThreshold: "自动放行阈值(0.5–1)",
6517
+ commandGuardThresholdHint: "typesafe/jev 判定\"安全\"的概率达到该值即放行,默认 0.9。",
6518
+ commandGuardTimeoutMs: "判定超时(毫秒,200–10000)",
6519
+ commandGuardTimeoutMsHint: "typesafe/jev 超过该时间未返回就照常弹窗,默认 1500。",
6520
+ zdr: "零数据保留(ZDR)",
6521
+ zdrHint: "开启后,请求只经由不留存提示词与回复、也不用于训练的上游(等价于 CLI 的 CMD_ZDR=1)。没有可用 ZDR 上游的模型会返回错误,请换用其他模型或关闭此开关。ZDR 容量按上游实价透传,通常更贵;费用读数仍按常规目录价估算。默认关闭。",
6020
6522
  accountsTitle: "多账户轮换",
6021
6523
  accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
6022
6524
  accountAdd: "添加账户",
@@ -6153,6 +6655,14 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6153
6655
  webSearchHint: "When on, the model-facing web_search tool is backed by Command Code (same API key and base URL as chat), winning over other search backends. Off hands the selection back to the previous backend (e.g. modsearch) instead of forcing the shipped DeepSeek search.",
6154
6656
  showSidebarQuota: "Show the quota card in the sidebar",
6155
6657
  showSidebarQuotaHint: "When on, a Command Code plan & quota card sits at the bottom of the sidebar (above Settings) and opens the full dashboard in the centre column. Off by default: nothing is shown on the left, and no background usage poll runs for it. Applies on save.",
6658
+ commandGuard: "AI command guard",
6659
+ commandGuardHint: "Before a shell command asks for approval, typesafe/jev judges whether it is safe: safe commands run without a prompt, everything else asks as usual. Off by default.",
6660
+ commandGuardThreshold: "Auto-approve threshold (0.5–1)",
6661
+ commandGuardThresholdHint: "Approve when typesafe/jev rates the command \"safe\" at least this likely; default 0.9.",
6662
+ commandGuardTimeoutMs: "Judgement timeout (ms, 200–10000)",
6663
+ commandGuardTimeoutMsHint: "If typesafe/jev does not answer in time, the normal prompt appears; default 1500.",
6664
+ zdr: "Zero data retention (ZDR)",
6665
+ zdrHint: "Serve requests only through upstreams that keep no prompts or completions and never train on them (the same opt-in as the CLI's CMD_ZDR=1). A few models have no ZDR upstream; requests for those models fail rather than losing ZDR protection. ZDR capacity is billed at the upstream's pass-through rates and usually costs more; the cost readout keeps quoting the ordinary catalog rates. Off by default.",
6156
6666
  accountsTitle: "Account rotation",
6157
6667
  accountsHint: "When the active account hits its usage limit (429) or its key fails (401), requests switch to the next account; when every account is exhausted the error names the earliest window reset.",
6158
6668
  accountAdd: "Add account",
@@ -6314,9 +6824,11 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6314
6824
  const SESSION_COST_ID = "commandcode-session-cost";
6315
6825
  /**
6316
6826
  * Client plugin body. Gates on the services shared by both client generations
6317
- * (`slots`, `locale`, `connection`, `remote`, `settingsScope`). Current clients
6318
- * mount the page after `remote.credentials` appears; legacy clients mount it
6319
- * from the connection ApiProxy credential face.
6827
+ * (`slots`, `locale`, `connection`, `remote`) — never on `settingsScope`,
6828
+ * which 0.1.7 removed and which the settings scope here replaces for every
6829
+ * generation. Current clients mount the page after `remote.credentials`
6830
+ * appears; legacy clients mount it from the connection ApiProxy credential
6831
+ * face.
6320
6832
  */
6321
6833
  function apply(ctx) {
6322
6834
  injectPageCss();
@@ -6341,7 +6853,18 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6341
6853
  }
6342
6854
  /** Mount the one shared UI implementation over either credential transport. */
6343
6855
  function applyClientSurfaces(ctx, api, hostDescription) {
6344
- const scope = ctx.settingsScope.bind({ namespace: COMMANDCODE_NS });
6856
+ let settingsNamespace;
6857
+ const scope = createSettingsScope(ctx, COMMANDCODE_NS, () => settingsNamespace);
6858
+ ctx.effect(() => () => {
6859
+ scope.dispose();
6860
+ }, "dsh-commandcode-provider: settings scope");
6861
+ ctx.inject(["remote.settings"], (settingsCtx) => {
6862
+ settingsNamespace = settingsCtx.remote.settings;
6863
+ scope.refresh();
6864
+ settingsCtx.effect(() => () => {
6865
+ settingsNamespace = void 0;
6866
+ }, "dsh-commandcode-provider: settings namespace");
6867
+ });
6345
6868
  let modelsRemote;
6346
6869
  const controller = new CommandCodeSettingsController(scope, {
6347
6870
  ...api,
@@ -6359,6 +6882,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6359
6882
  let usageNamespace;
6360
6883
  let usageMountError;
6361
6884
  let pricesController;
6885
+ let refreshUsage;
6362
6886
  const contribution = {
6363
6887
  package: USAGE_REMOTE_CONTRIBUTION.package,
6364
6888
  descriptors: [
@@ -6381,6 +6905,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6381
6905
  usageNamespace = namespaceCtx.remote.commandcode;
6382
6906
  controller.refreshCatalog();
6383
6907
  pricesController?.reload();
6908
+ refreshUsage?.();
6384
6909
  namespaceCtx.effect(() => () => {
6385
6910
  usageNamespace = void 0;
6386
6911
  }, "dsh-commandcode-provider: usage namespace");
@@ -6430,6 +6955,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6430
6955
  };
6431
6956
  modelsRemote = () => usageRemote.models();
6432
6957
  const usageController = new CommandCodeUsageController(usageRemote);
6958
+ refreshUsage = () => void usageController.refresh();
6433
6959
  ctx.effect(() => () => usageController.dispose(), "dsh-commandcode-provider: usage controller");
6434
6960
  const usageStore = createSnapshotStore(usageController.state());
6435
6961
  usageController.subscribe(() => usageStore.set(usageController.state()));
@@ -6596,8 +7122,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
6596
7122
  "slots",
6597
7123
  "locale",
6598
7124
  "connection",
6599
- "remote",
6600
- "settingsScope"
7125
+ "remote"
6601
7126
  ];
6602
7127
  //#endregion
6603
7128
  exports.apply = apply;