@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/CHANGELOG.md +55 -1
- package/README.md +35 -5
- package/README.zh-CN.md +35 -5
- package/icon.svg +1 -0
- package/lib/client.js +548 -23
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +182 -6
- package/lib/index.js +1705 -204
- package/lib/index.js.map +1 -1
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -38
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
|
|
60
|
-
* `
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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.
|
|
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: [
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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
|
|
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
|
|
6318
|
-
*
|
|
6319
|
-
*
|
|
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
|
-
|
|
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;
|