@mars-sea/dsh-commandcode-provider 0.11.16 → 0.12.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/lib/client.js CHANGED
@@ -40,16 +40,12 @@ window.__ModuleLoader__.load({
40
40
  /**
41
41
  * Backoff before re-reading a directory whose FIRST read failed, in millis.
42
42
  *
43
- * A first read can lose to a Host that is still coming up (the gateway's WS
44
- * handshake, a settings service mid-start): the failure is transient, but the
45
- * scope has nothing held, so it keeps its initial `{ status: 'loading',
46
- * writable: false }` snapshot — and every control on the settings page renders
47
- * disabled behind a "read-only" banner. Nothing else would ever re-read it:
48
- * the forwarded `settings/document-updated` and `connection/reset` signals need
49
- * a live Host to fire, and the namespace only re-mounts on a page reload. So a
50
- * failure with NOTHING held retries on this bounded ladder; a failure with a
51
- * held view keeps it (no retry — the page is already showing the last good
52
- * document, and `tests/settings-scope.test.ts` pins that).
43
+ * Only a failure with NOTHING held retries. With no view the scope keeps its
44
+ * initial `{ status: 'loading', writable: false }` snapshot and the settings
45
+ * page renders fully disabled, while the forwarded invalidation signals need a
46
+ * live Host to fire and the namespace only re-mounts on a reload. A failure
47
+ * with a held view retries nothing — the page already shows the last good
48
+ * document (`tests/settings-scope.test.ts` pins both).
53
49
  */
54
50
  const SETTINGS_DESCRIBE_RETRY_MS = [
55
51
  1e3,
@@ -95,10 +91,6 @@ window.__ModuleLoader__.load({
95
91
  retryHandle;
96
92
  retries = 0;
97
93
  disposed = false;
98
- /**
99
- * @param resolveRemote - Reads the inject-captured `remote.settings` namespace.
100
- * @param timer - Retry timer seam (see {@link SETTINGS_DESCRIBE_RETRY_MS}).
101
- */
102
94
  constructor(resolveRemote, timer = REAL_SETTINGS_TIMER) {
103
95
  this.resolveRemote = resolveRemote;
104
96
  this.timer = timer;
@@ -229,9 +221,8 @@ window.__ModuleLoader__.load({
229
221
  };
230
222
  /**
231
223
  * One namespace's derived scope over the shared mirror, plus its serialized
232
- * Host writes. This is the face `CommandCodeSettingsController` consumes
233
- * (`getSnapshot` / `subscribe` / `set` / `unset`), so the controller itself
234
- * needs no changes for either generation.
224
+ * Host writes — the face `CommandCodeSettingsController` consumes
225
+ * (`getSnapshot` / `subscribe` / `set` / `unset`).
235
226
  */
236
227
  var RemoteSettingsScope = class {
237
228
  resolveRemote;
@@ -392,22 +383,17 @@ window.__ModuleLoader__.load({
392
383
  *
393
384
  * Subscribes the two signals that can move the Host view — the forwarded
394
385
  * `settings/document-updated` invalidation and (when the client exposes it)
395
- * `connection/reset` — kicks the first describe read, and derives the
396
- * namespace row on every mirror change.
386
+ * `connection/reset` — kicks the first describe read, and derives the namespace
387
+ * row on every mirror change.
397
388
  *
398
- * `resolveRemote` is the INJECT-CAPTURED namespace (see
399
- * {@link SettingsRemoteResolver}): `remote.settings` is a Cordis service nested
400
- * under `remote`, so a read through our own context throws and has to be
401
- * replaced by an object captured inside `ctx.inject(['remote.settings'], …)`.
402
- * The caller that owns that inject calls {@link ManagedSettingsScope.refresh}
403
- * once the namespace lands, which is what turns the first, namespace-less read
404
- * into a served one; a profile that never mounts the namespace leaves the page
405
- * in its degraded (`unavailable`, read-only) state instead of throwing.
389
+ * `resolveRemote` must be the INJECT-CAPTURED namespace (see
390
+ * {@link SettingsRemoteResolver}), and the caller that owns that inject calls
391
+ * {@link ManagedSettingsScope.refresh} once the namespace lands — that is what
392
+ * turns the first, namespace-less read into a served one. A profile that never
393
+ * mounts the namespace leaves the page in its degraded (`unavailable`,
394
+ * read-only) state instead of throwing.
406
395
  *
407
- * @param context - The client context slice (only the declared `remote` service is read).
408
396
  * @param namespace - Settings namespace (profile entry id) this scope binds.
409
- * @param resolveRemote - Reads the inject-captured `remote.settings` namespace.
410
- * @returns The scope plus the refresh/dispose handles the entry wires into its fiber.
411
397
  */
412
398
  function createSettingsScope(context, namespace, resolveRemote, timer = REAL_SETTINGS_TIMER) {
413
399
  const mirror = new SettingsDescribeMirror(resolveRemote, timer);
@@ -444,38 +430,22 @@ window.__ModuleLoader__.load({
444
430
  //#endregion
445
431
  //#region src/client/settings.ts
446
432
  /**
447
- * Browser controller for the "Command Code" settings page.
448
- *
449
- * The page lives at the same settings-nav level as General / Models / Plugins
450
- * (a `settings.section` entry, id `commandcode`). It exists because the
451
- * Models page renders an unknown-adapter-family card for the `commandcode`
452
- * provider and deliberately disables its submit — the API key cannot be
453
- * configured there. This page owns the connection facts the plugin resolves
454
- * per request:
433
+ * Browser controller for the "Command Code" settings page (a `settings.section`
434
+ * entry, id `commandcode`). React-free: it only produces the state face
435
+ * `section.tsx` renders, so node tests can drive it.
455
436
  *
456
- * - API key -> written through the credentials domain under the reference
457
- * the plugin resolves (`apiKeyEnv`, default
458
- * `COMMANDCODE_API_KEY`). The literal never rides a response,
459
- * so the control only reports whether one is configured.
460
- * - API base -> the `llm-commandcode` settings namespace (`apiBase`), same
461
- * namespace the Models page card addresses.
462
- * - Timeouts, behavior switches, model visibility -> the same namespace,
463
- * staged and written on save.
464
- * - Accounts -> `accounts`, `activeAccount`, `modelAccountRules` and the
465
- * per-account credentials, committed immediately.
466
- *
467
- * The controller mirrors the plugin-card pattern from the harness's own
468
- * settings UI: it binds the `llm-commandcode` namespace through a
469
- * `SettingsScope` — this plugin's own binding of `remote.settings` (see
470
- * `./settings-scope.ts`; the harness's `settingsScope` wrapper existed only
471
- * through 0.1.6 and was removed with the 0.1.7 settings rewrite, while the
472
- * wire underneath spans every supported release) — keeps a staged draft of
473
- * edits, and writes them on save through `scope.set` / the credentials
474
- * domain. The Host stays the single fact source; the snapshot is republished
475
- * after each accepted write.
437
+ * Two write paths with different contracts:
438
+ * - The staged form (connection facts, behavior switches, model visibility,
439
+ * plus the Models-page card's default key draft) lands on `save()`.
440
+ * - Account management commits IMMEDIATELY and serially.
476
441
  *
477
- * This module is deliberately free of JSX — it only produces the state face
478
- * the React component renders.
442
+ * The API key is written through the CREDENTIALS domain under the reference the
443
+ * plugin resolves (`apiKeyEnv`), never through the settings document, so the
444
+ * literal cannot leak into a document; the control only reports whether one is
445
+ * configured. Everything else rides the `llm-commandcode` namespace through this
446
+ * plugin's own `SettingsScope` over `remote.settings` (see `./settings-scope.ts`).
447
+ * The Host stays the single fact source; the snapshot is republished after each
448
+ * accepted write.
479
449
  */
480
450
  /** The settings namespace the plugin registers (host half, src/index.ts). */
481
451
  const COMMANDCODE_NS = "llm-commandcode";
@@ -500,8 +470,7 @@ window.__ModuleLoader__.load({
500
470
  * save, and an optional inclusive `bounds` range rejects out-of-range values
501
471
  * with a specific reason (the Host schema would reject them at save time with
502
472
  * only a generic failure — catching it here names the problem while typing).
503
- * Decimals pass: the Host schema is `z.number()` too, and a fractional
504
- * millisecond value is harmless even if pointless.
473
+ * Decimals pass, like the Host schema's `z.number()`.
505
474
  */
506
475
  function numberField$1(field, bounds) {
507
476
  return {
@@ -530,24 +499,6 @@ window.__ModuleLoader__.load({
530
499
  }
531
500
  };
532
501
  }
533
- /** A field limited to fixed string choices; an empty draft clears it. */
534
- function choiceField(field, choices) {
535
- return {
536
- field,
537
- format: (value) => typeof value === "string" && choices.includes(value) ? value : "",
538
- parse: (text) => {
539
- const trimmed = text.trim();
540
- if (trimmed === "") return { kind: "clear" };
541
- return choices.includes(trimmed) ? {
542
- kind: "set",
543
- value: trimmed
544
- } : {
545
- kind: "invalid",
546
- reason: "format"
547
- };
548
- }
549
- };
550
- }
551
502
  /**
552
503
  * A boolean field, staged as the strings `'true'`/`'false'` (an empty draft
553
504
  * clears it). The component renders a toggle and only ever stages these two
@@ -575,35 +526,65 @@ window.__ModuleLoader__.load({
575
526
  }
576
527
  };
577
528
  }
578
- const MAX_TIMEOUT_MS = 2147483647;
579
- /** The command guard's auto-approve levels, strictest first. */
580
- const COMMAND_GUARD_LEVEL_CHOICES = [
581
- "high",
582
- "medium",
583
- "low"
584
- ];
585
- /** The level an unset `commandGuardLevel` reads as on the Host. */
586
- const COMMAND_GUARD_DEFAULT_LEVEL_CHOICE = "medium";
529
+ const MAX_TIMEOUT_SECONDS = 3600;
530
+ /**
531
+ * A duration field the user edits in WHOLE SECONDS.
532
+ *
533
+ * Milliseconds are the wrong unit to show: the defaults are 300000 and the
534
+ * ceiling is 2147483647, so the page asked users to do arithmetic to answer
535
+ * "how long is a reasonable wait". `format` divides, `parse` multiplies, and
536
+ * the stored value never leaves milliseconds.
537
+ *
538
+ * Decimals are refused rather than rounded. "1.5" is a format the control does
539
+ * not accept, and silently storing 1000 or 2000 for it would make the saved
540
+ * profile disagree with the box it was typed in.
541
+ */
542
+ function secondsField(field, bounds) {
543
+ return {
544
+ field,
545
+ format: (value) => typeof value === "number" ? String(Math.round(value / 1e3)) : "",
546
+ parse: (text) => {
547
+ const trimmed = text.trim();
548
+ if (trimmed === "") return { kind: "clear" };
549
+ const parsed = Number(trimmed);
550
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) return {
551
+ kind: "invalid",
552
+ reason: "format"
553
+ };
554
+ if (parsed < bounds.min) return {
555
+ kind: "invalid",
556
+ reason: "tooSmall"
557
+ };
558
+ if (parsed > bounds.max) return {
559
+ kind: "invalid",
560
+ reason: "tooLarge"
561
+ };
562
+ return {
563
+ kind: "set",
564
+ value: parsed * 1e3
565
+ };
566
+ }
567
+ };
568
+ }
587
569
  /** The fields this page edits inside the `llm-commandcode` namespace. */
588
570
  const SECTION_FIELDS = [
589
571
  textField("apiBase"),
590
- numberField$1("requestTimeoutMs", {
572
+ secondsField("requestTimeoutMs", {
591
573
  min: 1,
592
- max: MAX_TIMEOUT_MS
574
+ max: MAX_TIMEOUT_SECONDS
593
575
  }),
594
- numberField$1("streamIdleTimeoutMs", {
576
+ secondsField("streamIdleTimeoutMs", {
595
577
  min: 1,
596
- max: MAX_TIMEOUT_MS
578
+ max: MAX_TIMEOUT_SECONDS
597
579
  }),
598
580
  numberField$1("transportMaxRetries", {
599
581
  min: 0,
600
582
  max: 50
601
583
  }),
584
+ booleanField$1("offloadSeenImagesForCache"),
602
585
  booleanField$1("filterModelsByPlan"),
603
586
  booleanField$1("webSearch"),
604
587
  booleanField$1("showSidebarQuota"),
605
- booleanField$1("commandGuard"),
606
- choiceField("commandGuardLevel", COMMAND_GUARD_LEVEL_CHOICES),
607
588
  booleanField$1("zdr")
608
589
  ];
609
590
  /** Whether two model-id lists are equal as sets (order-insensitive). */
@@ -643,17 +624,12 @@ window.__ModuleLoader__.load({
643
624
  }
644
625
  /**
645
626
  * Controller bridging the `llm-commandcode` scope and the credentials domain
646
- * onto the page.
627
+ * onto the page. See the module header for the two write paths.
647
628
  *
648
- * Two write paths with different contracts:
649
- * - The staged form (connection, behavior and model-visibility fields, plus
650
- * the Models-page card's default key draft) lands on `save()`.
651
- * - Account management (create, rename, remove, key replacement, pinning and
652
- * per-account models) commits IMMEDIATELY and serially. A new account had to
653
- * be saved before browser sign-in could target it (the Host refuses a login
654
- * for a reference the stored `accounts` list does not name), which turned
655
- * "add an account" into edit → save → sign in; committing each operation
656
- * removes that dance and keeps unrelated staged edits out of it.
629
+ * Account ops commit immediately, so a new account can be created, signed into
630
+ * and pinned in one gesture — the Host refuses a login for a reference the
631
+ * stored `accounts` list does not name, so a staged row could not be signed
632
+ * into at all.
657
633
  */
658
634
  var CommandCodeSettingsController = class {
659
635
  scope;
@@ -663,13 +639,11 @@ window.__ModuleLoader__.load({
663
639
  listeners = /* @__PURE__ */ new Set();
664
640
  disposers = [];
665
641
  disposed = false;
666
- /** The credential reference the default account resolves. */
667
642
  credentialRef = DEFAULT_API_KEY_REF;
668
643
  /** Host-reported configured/writable state per credential reference. */
669
644
  credentialStates = /* @__PURE__ */ new Map();
670
645
  /** Staged visible-model allowlist (undefined = no draft). */
671
646
  visibleModelsDraft = void 0;
672
- /** The catalog the model editors offer (Host-side). */
673
647
  catalogModels = [];
674
648
  catalogFailed = false;
675
649
  saving = false;
@@ -679,10 +653,6 @@ window.__ModuleLoader__.load({
679
653
  accountQueue = Promise.resolve();
680
654
  accountPending = 0;
681
655
  accountFailed = void 0;
682
- /**
683
- * @param scope - bound scope for the `llm-commandcode` namespace.
684
- * @param api - credentials wire face.
685
- */
686
656
  constructor(scope, api) {
687
657
  this.scope = scope;
688
658
  this.api = api;
@@ -745,11 +715,10 @@ window.__ModuleLoader__.load({
745
715
  requestTimeoutMs: this.field("requestTimeoutMs"),
746
716
  streamIdleTimeoutMs: this.field("streamIdleTimeoutMs"),
747
717
  transportMaxRetries: this.field("transportMaxRetries"),
718
+ offloadSeenImagesForCache: this.field("offloadSeenImagesForCache"),
748
719
  filterModelsByPlan: this.field("filterModelsByPlan"),
749
720
  webSearch: this.field("webSearch"),
750
721
  showSidebarQuota: this.field("showSidebarQuota"),
751
- commandGuard: this.field("commandGuard"),
752
- commandGuardLevel: this.field("commandGuardLevel"),
753
722
  zdr: this.field("zdr"),
754
723
  sidebarQuota: this.sectionValue("showSidebarQuota") === true,
755
724
  activeAccount: typeof active === "string" ? active : "",
@@ -973,9 +942,8 @@ window.__ModuleLoader__.load({
973
942
  return target === "default" ? this.credentialRef : target;
974
943
  }
975
944
  /**
976
- * The first free `<credentialRef>_<n>` reference. Derived from the default
977
- * reference's name, so a renamed `apiKeyEnv` yields `MY_KEY_2`-style refs
978
- * consistent with the default slot.
945
+ * The first free `<credentialRef>_<n>` reference, so a renamed `apiKeyEnv`
946
+ * yields `MY_KEY_2`-style refs consistent with the default slot.
979
947
  */
980
948
  nextAccountRef() {
981
949
  const used = /* @__PURE__ */ new Set([this.credentialRef, ...this.storedExtras().map((extra) => extra.ref)]);
@@ -1133,10 +1101,10 @@ window.__ModuleLoader__.load({
1133
1101
  if (changed) this.publish();
1134
1102
  }
1135
1103
  /**
1136
- * Fetch the model catalog for the settings page's model editors through
1137
- * the Host Remote. Runs once at construction; call again (e.g. from the
1138
- * client entry once the Remote mount lands) to (re)try — a later success
1139
- * clears a prior failure flag so the editors recover without a page reload.
1104
+ * Fetch the model catalog through the Host Remote. Runs once at construction;
1105
+ * call again (e.g. from the client entry once the Remote mount lands) to
1106
+ * (re)try — a later success clears a prior failure flag, so the editors
1107
+ * recover without a page reload.
1140
1108
  */
1141
1109
  refreshCatalog() {
1142
1110
  const models = this.api.models;
@@ -1155,7 +1123,8 @@ window.__ModuleLoader__.load({
1155
1123
  shaped.push({
1156
1124
  id: entry.id,
1157
1125
  name: entry.name,
1158
- ...typeof entry.tier === "string" ? { tier: entry.tier } : {}
1126
+ ...typeof entry.tier === "string" ? { tier: entry.tier } : {},
1127
+ ...typeof entry.allowance === "number" && Number.isFinite(entry.allowance) ? { allowance: entry.allowance } : {}
1159
1128
  });
1160
1129
  }
1161
1130
  this.catalogModels = shaped;
@@ -1257,6 +1226,34 @@ window.__ModuleLoader__.load({
1257
1226
  }
1258
1227
  };
1259
1228
  //#endregion
1229
+ //#region src/display-format.ts
1230
+ /** Shared, dependency-free formatting for Host commands and browser surfaces. */
1231
+ function formatMoney(value) {
1232
+ return `$${value.toFixed(2)}`;
1233
+ }
1234
+ function formatMoneyExact(value) {
1235
+ return `$${value.toFixed(4)}`;
1236
+ }
1237
+ function formatTokensCompact(value) {
1238
+ if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
1239
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
1240
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
1241
+ return String(value);
1242
+ }
1243
+ /** The input is already in percent units; callers add the percent sign. */
1244
+ function formatSuccessRate(value) {
1245
+ return String(Number(value.toFixed(2)));
1246
+ }
1247
+ /** One window's fill ratio in [0, 1]; zero when uncapped. */
1248
+ function windowRatio(used, cap) {
1249
+ if (cap <= 0) return 0;
1250
+ return Math.max(0, Math.min(1, used / cap));
1251
+ }
1252
+ /** Local date-time, with the empty state chosen by the caller. */
1253
+ function formatResetAt(ms) {
1254
+ return ms <= 0 ? "" : new Date(ms).toLocaleString();
1255
+ }
1256
+ //#endregion
1260
1257
  //#region src/client/usage.ts
1261
1258
  const IDLE$1 = {
1262
1259
  status: "idle",
@@ -1339,41 +1336,6 @@ window.__ModuleLoader__.load({
1339
1336
  for (const listener of this.listeners) listener();
1340
1337
  }
1341
1338
  };
1342
- /** Format a dollar amount compactly (2 decimals). */
1343
- function formatMoney(value) {
1344
- return `$${value.toFixed(2)}`;
1345
- }
1346
- /** Format a dollar amount precisely (4 decimals) for small totals. */
1347
- function formatMoneyExact(value) {
1348
- return `$${value.toFixed(4)}`;
1349
- }
1350
- /** Format a large token count compactly (1.9M style). */
1351
- function formatTokensCompact(value) {
1352
- if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
1353
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
1354
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
1355
- return String(value);
1356
- }
1357
- /**
1358
- * Format a success-rate percentage (already in percent units, e.g. 99.96):
1359
- * at most two decimals, trailing zeros trimmed — `100` stays `100`, not
1360
- * `100.00`, and the raw upstream float `99.965552876334` becomes `99.97`.
1361
- * The `%` suffix is appended by the caller (the card and the dashboard line
1362
- * both compose it).
1363
- */
1364
- function formatSuccessRate(value) {
1365
- return String(Number(value.toFixed(2)));
1366
- }
1367
- /** One window's fill ratio in [0, 1]; 0 when uncapped. */
1368
- function windowRatio(used, cap) {
1369
- if (cap <= 0) return 0;
1370
- return Math.max(0, Math.min(1, used / cap));
1371
- }
1372
- /** Format a millis timestamp as a local short date-time; empty when unset. */
1373
- function formatResetAt(ms) {
1374
- if (ms <= 0) return "";
1375
- return new Date(ms).toLocaleString();
1376
- }
1377
1339
  /** Host facts drive the usage card, including keys invisible to browser credentials. */
1378
1340
  function usageCardState(usage) {
1379
1341
  const accounts = usage.report?.accounts ?? [];
@@ -1402,15 +1364,12 @@ window.__ModuleLoader__.load({
1402
1364
  };
1403
1365
  /**
1404
1366
  * Cache over the `commandcode/prices` Remote, with a bounded transient-retry
1405
- * budget. Public API mirrors {@link CommandCodeUsageController}: `state()`,
1406
- * `subscribe`, and `ensure()`; `reload()` additionally drops the cache and
1407
- * restarts the budget when the Host namespace rebinds.
1408
- *
1409
- * Not a one-shot: the last fetch is cached until `reload()`, a transient
1410
- * failure is retried three times at 1/2/4 s (the delays are bounded, not a
1411
- * poll), and a MANUAL `ensure()` still fetches after that budget is spent. A
1412
- * PERMANENT failure — the Host serving no such endpoint — is terminal for the
1413
- * binding and is only cleared by `reload()`.
1367
+ * budget (three retries at 1/2/4 s — bounded, not a poll). Public API mirrors
1368
+ * {@link CommandCodeUsageController}: `state()`, `subscribe` and `ensure()`;
1369
+ * `reload()` additionally drops the cache and restarts the budget when the
1370
+ * Host namespace rebinds. A MANUAL `ensure()` still fetches after the budget
1371
+ * is spent, and a PERMANENT failure — the Host serving no such endpoint — is
1372
+ * terminal for the binding until `reload()`.
1414
1373
  */
1415
1374
  var CommandCodePricesController = class {
1416
1375
  remote;
@@ -1444,12 +1403,9 @@ window.__ModuleLoader__.load({
1444
1403
  }
1445
1404
  /**
1446
1405
  * Fetch the table unless it is already loaded, permanent, or in flight — and
1447
- * unless a bounded retry already owns the next attempt.
1448
- *
1449
- * The only callers are the client entry: `reload()` when the Remote namespace
1450
- * lands or rebinds, and a manual refresh. Nothing in the composer calls it, so
1451
- * a mounted readout does not trigger a fetch of its own — it renders whatever
1452
- * the table's state currently is.
1406
+ * unless a bounded retry already owns the next attempt. Only the client
1407
+ * entry calls this (on rebind and on a manual refresh), so a mounted readout
1408
+ * renders the current state instead of triggering a fetch of its own.
1453
1409
  */
1454
1410
  ensure() {
1455
1411
  if (this.disposed || this.permanent || this.inFlight || this.current.status === "ready") return;
@@ -1575,12 +1531,11 @@ window.__ModuleLoader__.load({
1575
1531
  this.remote = remote;
1576
1532
  this.pollMs = pollMs;
1577
1533
  }
1578
- /** Subscribe to state projections. @returns the disposer. */
1534
+ /** Subscribe to state projections. */
1579
1535
  subscribe(listener) {
1580
1536
  this.listeners.add(listener);
1581
1537
  return () => this.listeners.delete(listener);
1582
1538
  }
1583
- /** Build the current panel state face. */
1584
1539
  state() {
1585
1540
  return {
1586
1541
  phase: this.phase,
@@ -1773,9 +1728,9 @@ window.__ModuleLoader__.load({
1773
1728
  return new Promise((resolve) => setTimeout(resolve, ms));
1774
1729
  }
1775
1730
  /**
1776
- * The per-reason copy for a failed login attempt. Shared by the settings
1777
- * page's login panel and the Models-page card's login row — the same reasons
1778
- * can surface from either surface.
1731
+ * The per-reason copy for a failed attempt. Shared by the settings page's login
1732
+ * panel and the Models-page card's login row — the same reason can surface from
1733
+ * either.
1779
1734
  */
1780
1735
  function loginFailureCopy(reason, t) {
1781
1736
  if (reason === "denied") return t("loginDenied");
@@ -1788,8 +1743,8 @@ window.__ModuleLoader__.load({
1788
1743
  }
1789
1744
  /**
1790
1745
  * The hint text + class for one login panel state, shared by the settings
1791
- * page's `LoginPanel` and the Models-page card's login row so both surfaces
1792
- * can never drift apart. Pure: no timers, no state — the components render it.
1746
+ * page's login panel and the Models-page card's login row so both surfaces
1747
+ * can never drift apart. Pure: no timers, no state.
1793
1748
  */
1794
1749
  function loginHint(state, t) {
1795
1750
  if (state.phase === "starting" || state.phase === "waiting") return {
@@ -1830,16 +1785,16 @@ window.__ModuleLoader__.load({
1830
1785
  /** The wire namespace all Command Code endpoints share. */
1831
1786
  const REMOTE_NAMESPACE = "commandcode";
1832
1787
  /**
1833
- * Build the validator helpers one endpoint uses. `prefix` names the
1834
- * endpoint in the rejection message (e.g. `commandcode/report result:`), so
1835
- * each wire file keeps its own diagnostic phrasing while sharing the helper
1836
- * bodies.
1788
+ * Build the validator helpers one endpoint uses. `prefix` names the endpoint
1789
+ * in the rejection message (e.g. `commandcode/report result:`), so each wire
1790
+ * file keeps its own diagnostic phrasing while sharing the helper bodies.
1837
1791
  *
1838
1792
  * The helpers return the reject call directly in the failure branch: since
1839
1793
  * `reject` is typed `never`, the ternary's union collapses to the success type
1840
1794
  * without relying on TypeScript's control-flow analysis of a never-returning
1841
1795
  * call (which only recognizes function declarations, not the destructured
1842
- * arrow `reject` callers receive from this factory).
1796
+ * arrow `reject` callers receive from this factory). Callers that need the
1797
+ * narrowing in an `if` must therefore assign inside the positive branch.
1843
1798
  */
1844
1799
  function makeBoundaryValidator(prefix) {
1845
1800
  const reject = (field) => {
@@ -1867,10 +1822,8 @@ window.__ModuleLoader__.load({
1867
1822
  }
1868
1823
  /**
1869
1824
  * Build one strict invocation descriptor. Every Command Code Remote shares the
1870
- * `commandcode` namespace, the `commandcodeUsage` service, and a strict
1871
- * `mode: 'strict'` result — only the endpoint, method, result type symbol, and
1872
- * schema differ — so the boilerplate lives here once and each endpoint supplies
1873
- * only its own facts.
1825
+ * `commandcode` namespace, the `commandcodeUsage` service, and a strict result
1826
+ * — only the endpoint, method, result type symbol, and schema differ.
1874
1827
  */
1875
1828
  function makeRemoteDescriptor(endpoint, method, typeSymbol, schema) {
1876
1829
  return {
@@ -1894,7 +1847,6 @@ window.__ModuleLoader__.load({
1894
1847
  * so rejection messages name the offending boundary.
1895
1848
  */
1896
1849
  const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
1897
- /** Validate one window-limit block (`fiveHour` / `weekly`). */
1898
1850
  /**
1899
1851
  * Parse one quota window, or undefined when the frame carries no such window.
1900
1852
  *
@@ -2010,7 +1962,7 @@ window.__ModuleLoader__.load({
2010
1962
  * separate instance so catalog boundary errors name `commandcode/models`,
2011
1963
  * not the report endpoint.
2012
1964
  */
2013
- const { record: catalogRecord, stringField: catalogString } = makeBoundaryValidator("commandcode/models result:");
1965
+ const { record: catalogRecord, stringField: catalogString, numberField: catalogNumber } = makeBoundaryValidator("commandcode/models result:");
2014
1966
  /** Parse one untrusted boundary value into a {@link CommandCodeCatalogModel}. */
2015
1967
  function parseCatalogModel(value) {
2016
1968
  const source = catalogRecord(value, "model");
@@ -2019,6 +1971,7 @@ window.__ModuleLoader__.load({
2019
1971
  name: catalogString(source, "name", "model.name")
2020
1972
  };
2021
1973
  if (source.tier !== void 0) model.tier = catalogString(source, "tier", "model.tier");
1974
+ if (source.allowance !== void 0) model.allowance = catalogNumber(source, "allowance", "model.allowance");
2022
1975
  return model;
2023
1976
  }
2024
1977
  /** Parse the wire result into a {@link CommandCodeCatalog}. */
@@ -2170,38 +2123,40 @@ window.__ModuleLoader__.load({
2170
2123
  ]
2171
2124
  };
2172
2125
  //#endregion
2126
+ //#region src/plan-tiers.ts
2127
+ /** Shared plan-tier presentation facts; safe to inline into the browser bundle. */
2128
+ const PLAN_TIER_ORDER = [
2129
+ "go",
2130
+ "goat",
2131
+ "pro",
2132
+ "provider",
2133
+ "max"
2134
+ ];
2135
+ const PLAN_LABELS = {
2136
+ go: "Go",
2137
+ goat: "GOAT",
2138
+ pro: "Pro",
2139
+ provider: "Provider",
2140
+ max: "Max"
2141
+ };
2142
+ Object.fromEntries(PLAN_TIER_ORDER.map((tier, index) => [tier, index]));
2143
+ //#endregion
2173
2144
  //#region src/client/model-select.ts
2174
2145
  /**
2175
2146
  * Model-select helpers for the settings page's model editors (browser half).
2176
2147
  *
2177
- * The routing-rule editor and the visible-models filter both pick catalog
2178
- * models through the same checkbox multi-select dropdown (`ModelMultiSelect`
2179
- * in section.tsx). The dropdown's data shaping — search filtering, stale-id
2148
+ * The routing-rule editor and the visible-models filter both pick catalog models
2149
+ * through the same checkbox multi-select dropdown (`ModelMultiSelect` in
2150
+ * section.tsx). The dropdown's data shaping — search filtering, stale-id
2180
2151
  * detection, tier grouping — lives here, React-free, so node tests can drive
2181
2152
  * it directly.
2182
2153
  *
2183
- * Dependency-free by design: the client bundle may only import platform/seed
2184
- * modules, so the plan snapshot below is a deliberately small vendored copy
2185
- * (tier key → heading label) rather than an import of src/capabilities.ts.
2186
- * When upstream adds a plan tier, extend BOTH tables.
2154
+ * The shared tier module is dependency-free and inlined into the client bundle;
2155
+ * importing the Host's full capability snapshot here would bloat that bundle.
2187
2156
  *
2188
2157
  * @module dsh-commandcode-provider/model-select
2189
2158
  */
2190
2159
  /**
2191
- * Minimum plan tier → dropdown section heading. Mirrors the Host-side
2192
- * `KNOWN_PLANS` values + `PLAN_LABELS` in src/capabilities.ts (kept as a
2193
- * vendored copy because the client bundle cannot import host modules).
2194
- * Covers every tier key `KNOWN_PLANS` uses today; an unknown tier key falls
2195
- * back to the raw key rather than vanishing the row.
2196
- */
2197
- const TIER_HEADINGS = {
2198
- go: "Go",
2199
- goat: "GOAT",
2200
- pro: "Pro",
2201
- provider: "Provider",
2202
- max: "Max"
2203
- };
2204
- /**
2205
2160
  * The dropdown section heading for a catalog model id, or undefined for
2206
2161
  * models outside the known plan tiers (unmapped models and stale ids render
2207
2162
  * unheaded). `knownPlans` is the Host-side `KNOWN_PLANS` table, threaded in
@@ -2210,7 +2165,7 @@ window.__ModuleLoader__.load({
2210
2165
  function tierHeadingFor(modelId, knownPlans) {
2211
2166
  const tier = knownPlans[modelId];
2212
2167
  if (tier === void 0) return void 0;
2213
- return TIER_HEADINGS[tier] ?? tier;
2168
+ return PLAN_LABELS[tier] ?? tier;
2214
2169
  }
2215
2170
  /**
2216
2171
  * Whether `text` matches `query` as a case-insensitive substring over the
@@ -2222,21 +2177,19 @@ window.__ModuleLoader__.load({
2222
2177
  return model.id.toLowerCase().includes(needle) || model.name.toLowerCase().includes(needle);
2223
2178
  }
2224
2179
  /**
2225
- * Build the dropdown options: the catalog (already in picker order) plus
2226
- * any selected ids the catalog no longer carries, flagged stale so the UI
2227
- * can mark them — a saved selection never silently loses an entry, and the
2228
- * user can see which ones went stale upstream.
2229
- *
2230
- * When `query` is non-blank, catalog rows are filtered by
2231
- * {@link matchesModelQuery}; stale rows are kept only while they match too,
2232
- * so a search for a live model does not surface unrelated stale ids.
2180
+ * Build the dropdown options: the catalog (already in picker order) plus any
2181
+ * selected ids the catalog no longer carries, flagged stale so a saved
2182
+ * selection never silently loses an entry. Stale rows survive a non-blank
2183
+ * {@link matchesModelQuery} search only while they match too, so a search for
2184
+ * a live model does not surface unrelated stale ids.
2233
2185
  */
2234
2186
  function buildModelSelectOptions(catalog, selected, query = "") {
2235
2187
  const catalogIds = new Set(catalog.map((model) => model.id));
2236
2188
  const options = catalog.filter((model) => matchesModelQuery(model, query)).map((model) => ({
2237
2189
  value: model.id,
2238
2190
  label: model.name,
2239
- stale: false
2191
+ stale: false,
2192
+ ...model.allowance === void 0 ? {} : { allowance: model.allowance }
2240
2193
  }));
2241
2194
  const seen = new Set(catalogIds);
2242
2195
  for (const id of selected) {
@@ -2256,12 +2209,10 @@ window.__ModuleLoader__.load({
2256
2209
  return options;
2257
2210
  }
2258
2211
  /**
2259
- * Group dropdown options under plan-tier headings (`tierOf` maps a model id
2260
- * to its tier heading, or undefined for unmapped models — see
2261
- * {@link tierHeadingFor}). Live rows keep their relative order; stale ids
2262
- * and unmapped live rows share one trailing unheaded group. Groups merge
2263
- * repeats, so a catalog interleaving two tiers still renders one section
2264
- * per tier.
2212
+ * Group dropdown options under plan-tier headings (`tierOf` maps a model id to
2213
+ * its heading, or undefined for unmapped models — see {@link tierHeadingFor}).
2214
+ * Live rows keep their relative order and repeats merge, so a catalog
2215
+ * interleaving two tiers still renders one section per tier.
2265
2216
  */
2266
2217
  function groupModelSelectOptions(options, tierOf) {
2267
2218
  const groups = [];
@@ -2291,13 +2242,11 @@ window.__ModuleLoader__.load({
2291
2242
  }
2292
2243
  /**
2293
2244
  * Whether the catalog is trustworthy enough to call an unlisted selection
2294
- * "retired". FALSE while the first fetch is still in flight and after a
2295
- * failure, because the catalog is empty then and every selected id would look
2296
- * stale — which turns the one-click stale cleanup into a button that silently
2297
- * empties the allowlist. A successfully loaded but empty catalog is treated as
2298
- * untrustworthy too: an empty list is far more likely a Host problem than every
2299
- * model being retired at once, and the explicit "show all" entry covers the
2300
- * user who really wants to clear the list.
2245
+ * "retired". FALSE while the first fetch is in flight, after a failure, and
2246
+ * for a loaded-but-empty catalog — an empty list makes every selected id look
2247
+ * stale, which would turn the one-click stale cleanup into a button that
2248
+ * silently empties the allowlist. Users who really want it cleared have the
2249
+ * explicit "show all" entry.
2301
2250
  */
2302
2251
  function catalogIsReady(readiness) {
2303
2252
  return readiness.catalogIds.length > 0 && !readiness.catalogFailed;
@@ -2313,7 +2262,7 @@ window.__ModuleLoader__.load({
2313
2262
  }
2314
2263
  //#endregion
2315
2264
  //#region package.json
2316
- var version = "0.11.16";
2265
+ var version = "0.12.0";
2317
2266
  var repository = {
2318
2267
  "type": "git",
2319
2268
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -2321,17 +2270,15 @@ window.__ModuleLoader__.load({
2321
2270
  //#endregion
2322
2271
  //#region src/client/version.ts
2323
2272
  /**
2324
- * The plugin's own version, read from package.json at build time.
2325
- *
2326
- * The client bundle inlines the JSON import (rolldown resolves it during the
2327
- * tsdown build; node tests read it through tsx), so the rendered value always
2328
- * matches the published package version with no second constant to keep in
2329
- * sync. Rendered as a muted footer line on the settings page so a user can
2330
- * report the exact build they run.
2273
+ * The plugin's own version, read from package.json at build time. The client
2274
+ * bundle inlines the JSON import (rolldown resolves it during the tsdown
2275
+ * build; node tests read it through tsx), so the rendered value always matches
2276
+ * the published package version with no second constant to keep in sync —
2277
+ * rendered as a muted footer line so a user can report the exact build.
2331
2278
  *
2332
2279
  * @module dsh-commandcode-provider/client/version
2333
2280
  */
2334
- /** The published package version (e.g. `'0.6.0'`). */
2281
+ /** The published package version. */
2335
2282
  const PLUGIN_VERSION = version;
2336
2283
  /**
2337
2284
  * This package's GitHub releases page, derived from the repository field so
@@ -2351,14 +2298,12 @@ window.__ModuleLoader__.load({
2351
2298
  */
2352
2299
  const NPM_LATEST_URL = "https://registry.npmjs.org/@mars-sea%2Fdsh-commandcode-provider/latest";
2353
2300
  /**
2354
- * Compare two version strings (`major.minor.patch[-pre]`). Returns a negative
2355
- * number when `a` sorts before `b`, positive when after, zero when equal.
2301
+ * Compare two version strings (`major.minor.patch[-pre]`): negative when `a`
2302
+ * sorts before `b`, positive when after, zero when equal.
2356
2303
  *
2357
- * Tolerant by design: a leading `v` is stripped, unparsable numeric parts
2358
- * count as `0`, and semver prerelease rules apply (release > prerelease;
2359
- * numeric identifiers compare numerically, everything else lexically, a
2360
- * shorter identifier list sorts first). Enough for release tags; not a full
2361
- * semver validator.
2304
+ * Tolerant by design — a leading `v` is stripped, unparsable numeric parts
2305
+ * count as `0`, and semver prerelease rules apply. Enough for release tags,
2306
+ * not a full semver validator.
2362
2307
  */
2363
2308
  function compareVersions(a, b) {
2364
2309
  const left = splitVersion(a);
@@ -2494,21 +2439,17 @@ window.__ModuleLoader__.load({
2494
2439
  * React component for the "Command Code" settings page (browser half).
2495
2440
  *
2496
2441
  * Renders as a `settings.section` entry — a page at the same settings-nav
2497
- * level as General / Models / Plugins. The shell supplies the nav row and
2498
- * renders this body inside the content column. All copy comes from the
2442
+ * level as General / Models / Plugins. All copy comes from the
2499
2443
  * `settings.commandcode` locale namespace; all state comes from the
2500
2444
  * `CommandCodeSettingsController` injected by the slot registration.
2501
2445
  *
2502
- * Layout, top to bottom:
2503
- * 1. Accounts — one card per account with its status, quota, actions and
2504
- * dedicated models. Every account operation commits immediately.
2505
- * 2. Models — the plan filter and the visible-model allowlist.
2506
- * 3. Privacy & security — zero data retention and the command guard.
2507
- * 4. Integrations & display — web search and the sidebar quota card.
2508
- * 5. Advanced (collapsed) — API base and the network limits.
2509
- * Sections 2–5 are flat groups of rows (title and description left, control
2510
- * right, as on the harness's own settings pages), a staged form written by
2511
- * the floating save bar.
2446
+ * Layout, top to bottom: Accounts (one card per account, status, quota,
2447
+ * actions and dedicated models — every operation commits immediately), Models
2448
+ * (plan filter + visible-model allowlist), Privacy & security (ZDR),
2449
+ * Integrations & display (web search, sidebar quota card), and a collapsed
2450
+ * Advanced card (API base + network limits). Everything below Accounts is a
2451
+ * flat group of rows (title/description left, control right) inside the staged
2452
+ * form the floating save bar writes.
2512
2453
  *
2513
2454
  * Styles are injected once by the client entry (see src/client/index.ts) and
2514
2455
  * class-prefixed `cc-` to stay local.
@@ -2517,7 +2458,8 @@ window.__ModuleLoader__.load({
2517
2458
  "apiBase",
2518
2459
  "requestTimeoutMs",
2519
2460
  "streamIdleTimeoutMs",
2520
- "transportMaxRetries"
2461
+ "transportMaxRetries",
2462
+ "offloadSeenImagesForCache"
2521
2463
  ];
2522
2464
  /** The "customized" tag and reset link a staged field shows once it differs from the default. */
2523
2465
  function FieldOverride({ label, state, disabled, t, onReset }) {
@@ -2645,53 +2587,6 @@ window.__ModuleLoader__.load({
2645
2587
  })] })
2646
2588
  });
2647
2589
  }
2648
- /**
2649
- * One fixed-choice field rendered as a segmented control. The staged text is
2650
- * one of `options`' values or `''` (unset → `defaultValue`); picking the
2651
- * default while unset stages nothing new.
2652
- */
2653
- function SegmentedField({ label, hint, state, disabled, options, defaultValue, className, onEdit, onReset, t }) {
2654
- const current = state.text === "" ? defaultValue : state.text;
2655
- const index = Math.max(0, options.findIndex((option) => option.value === current));
2656
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingRow, {
2657
- title: label,
2658
- tag: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OverrideTag, {
2659
- state,
2660
- t
2661
- }),
2662
- description: hint,
2663
- ...className === void 0 ? {} : { className },
2664
- control: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldOverride, {
2665
- label,
2666
- state,
2667
- disabled,
2668
- t,
2669
- onReset
2670
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2671
- className: "cc-segmented",
2672
- role: "radiogroup",
2673
- "aria-label": label,
2674
- style: {
2675
- "--cc-segment-count": options.length,
2676
- "--cc-segment-index": index
2677
- },
2678
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2679
- className: "cc-segmentIndicator",
2680
- "aria-hidden": "true"
2681
- }), options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2682
- type: "button",
2683
- role: "radio",
2684
- "aria-checked": current === option.value,
2685
- className: "cc-segment",
2686
- disabled,
2687
- onClick: () => {
2688
- if (current !== option.value) onEdit(option.value);
2689
- },
2690
- children: option.label
2691
- }, option.value))]
2692
- })] })
2693
- });
2694
- }
2695
2590
  /** One stat tile in the account card's summary grid. */
2696
2591
  function UsageStat({ label, value, sub }) {
2697
2592
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2760,11 +2655,8 @@ window.__ModuleLoader__.load({
2760
2655
  ]
2761
2656
  });
2762
2657
  }
2763
- /**
2764
- * One pool account's facts (identity, totals, credits, window limits)
2765
- * rendered inside an account row's details.
2766
- */
2767
- function AccountReport({ entry, fetchedAt, t, summaryOnly = false, headless = false }) {
2658
+ /** One pool account's facts (identity, totals, credits, window limits). */
2659
+ function AccountReport({ entry, fetchedAt, t, headless = false }) {
2768
2660
  const report = entry.report;
2769
2661
  const account = report.account;
2770
2662
  const accountName = account === void 0 ? "" : account.userName || account.name;
@@ -2821,7 +2713,7 @@ window.__ModuleLoader__.load({
2821
2713
  }) : null
2822
2714
  ]
2823
2715
  }) : null,
2824
- !summaryOnly && report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2716
+ report.usage !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2825
2717
  className: "cc-usageStats cc-usageStatsActivity",
2826
2718
  children: [
2827
2719
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
@@ -2845,7 +2737,7 @@ window.__ModuleLoader__.load({
2845
2737
  })
2846
2738
  ]
2847
2739
  }) : null,
2848
- !summaryOnly && credits !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2740
+ credits !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2849
2741
  className: "cc-usageStats cc-usageStatsBalance",
2850
2742
  children: [
2851
2743
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
@@ -2919,16 +2811,12 @@ window.__ModuleLoader__.load({
2919
2811
  return t("usageNetworkHint");
2920
2812
  }
2921
2813
  /**
2922
- * A checkbox multi-select dropdown for picking catalog models (the
2923
- * routing-rule rows and the visible-models filter share it). The trigger
2924
- * shows the selection count; the Menu lists every catalog model with a
2925
- * checkbox, toggled by clicking the row. A search box under the trigger
2926
- * (inside the Menu anchor, so focusing it never trips the outside-click
2927
- * close) filters the list by id/display-name substring, and items group
2928
- * under plan-tier headings in picker order. Selected ids the catalog no
2929
- * longer carries still render — flagged stale — so a saved selection never
2930
- * silently loses an entry, and the VisibleModelsCard offers a one-click
2931
- * cleanup.
2814
+ * A checkbox multi-select dropdown for picking catalog models (the routing-rule
2815
+ * rows and the visible-models filter share it). The trigger shows the selection
2816
+ * count; the Menu lists every catalog model, grouping under plan-tier headings
2817
+ * and flagging selected ids the catalog no longer carries as stale, so a saved
2818
+ * selection never silently loses an entry (the VisibleModelsCard offers a
2819
+ * one-click cleanup).
2932
2820
  *
2933
2821
  * With `deferCommit` the toggles collect in a local draft that is handed to
2934
2822
  * `onSelect` once, when the menu closes: the per-account picker writes
@@ -2975,6 +2863,11 @@ window.__ModuleLoader__.load({
2975
2863
  className: "cc-badge",
2976
2864
  children: t("modelStale")
2977
2865
  }) : null,
2866
+ option.allowance !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2867
+ className: "cc-badgeMuted",
2868
+ title: t("modelAllowanceTitle"),
2869
+ children: t("modelAllowance", { amount: option.allowance })
2870
+ }) : null,
2978
2871
  !selected.includes(option.value) && ownerOf?.(option.value) !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2979
2872
  className: "cc-badgeMuted",
2980
2873
  children: t("accountModelOwner", { name: ownerOf(option.value) })
@@ -3073,11 +2966,13 @@ window.__ModuleLoader__.load({
3073
2966
  }
3074
2967
  /**
3075
2968
  * One titled group of rows. No card surface: like the harness's own settings
3076
- * pages, a group is a heading over hairline-separated rows.
2969
+ * pages, a group is a heading over its rows. The hairline between sections is
2970
+ * `cc-groupDivided`, so it belongs to every group but the first one on the page
2971
+ * (accounts opens it) and never to the rows inside a group.
3077
2972
  */
3078
- function SettingsGroup({ title, action, children }) {
2973
+ function SettingsGroup({ title, action, divided = false, children }) {
3079
2974
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3080
- className: "cc-group",
2975
+ className: divided ? "cc-group cc-groupDivided" : "cc-group",
3081
2976
  "aria-label": title,
3082
2977
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3083
2978
  className: "cc-groupHead",
@@ -3098,6 +2993,7 @@ window.__ModuleLoader__.load({
3098
2993
  function ModelsCard({ state, disabled, t, onEdit, onReset, onSelect, onClear }) {
3099
2994
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsGroup, {
3100
2995
  title: t("modelsTitle"),
2996
+ divided: true,
3101
2997
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3102
2998
  id: "cc-filter-models-by-plan",
3103
2999
  label: t("filterModelsByPlan"),
@@ -3118,70 +3014,31 @@ window.__ModuleLoader__.load({
3118
3014
  });
3119
3015
  }
3120
3016
  /**
3121
- * Privacy & security: the two opt-ins that change where data goes. ZDR
3122
- * restricts which upstream serves a request; the command guard sends command
3123
- * text to a decision model with no ZDR upstream and can skip an approval the
3124
- * user would otherwise have seen, so both start off.
3017
+ * Privacy & security: the opt-in that changes where data goes. ZDR restricts
3018
+ * which upstream serves a request, so it starts off.
3125
3019
  */
3126
3020
  function PrivacyCard({ state, disabled, t, onEdit, onReset }) {
3127
- const showGuardLevel = state.commandGuard.text === "true" || state.commandGuardLevel.overridden || state.commandGuardLevel.clear;
3128
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsGroup, {
3021
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsGroup, {
3129
3022
  title: t("privacyTitle"),
3130
- children: [
3131
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3132
- id: "cc-zdr",
3133
- label: t("zdr"),
3134
- hint: t("zdrHint"),
3135
- state: state.zdr,
3136
- disabled,
3137
- defaultChecked: false,
3138
- onEdit: (text) => onEdit("zdr", text),
3139
- onReset: () => onReset("zdr"),
3140
- t
3141
- }),
3142
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3143
- id: "cc-command-guard",
3144
- label: t("commandGuard"),
3145
- hint: t("commandGuardHint"),
3146
- state: state.commandGuard,
3147
- disabled,
3148
- defaultChecked: false,
3149
- onEdit: (text) => onEdit("commandGuard", text),
3150
- onReset: () => onReset("commandGuard"),
3151
- t
3152
- }),
3153
- showGuardLevel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SegmentedField, {
3154
- className: "cc-rowNested",
3155
- label: t("commandGuardLevel"),
3156
- hint: t("commandGuardLevelHint"),
3157
- state: state.commandGuardLevel,
3158
- disabled,
3159
- options: [
3160
- {
3161
- value: "high",
3162
- label: t("commandGuardLevelHigh")
3163
- },
3164
- {
3165
- value: "medium",
3166
- label: t("commandGuardLevelMedium")
3167
- },
3168
- {
3169
- value: "low",
3170
- label: t("commandGuardLevelLow")
3171
- }
3172
- ],
3173
- defaultValue: COMMAND_GUARD_DEFAULT_LEVEL_CHOICE,
3174
- onEdit: (text) => onEdit("commandGuardLevel", text),
3175
- onReset: () => onReset("commandGuardLevel"),
3176
- t
3177
- }) : null
3178
- ]
3023
+ divided: true,
3024
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3025
+ id: "cc-zdr",
3026
+ label: t("zdr"),
3027
+ hint: t("zdrHint"),
3028
+ state: state.zdr,
3029
+ disabled,
3030
+ defaultChecked: false,
3031
+ onEdit: (text) => onEdit("zdr", text),
3032
+ onReset: () => onReset("zdr"),
3033
+ t
3034
+ })
3179
3035
  });
3180
3036
  }
3181
3037
  /** Integrations & display: surfaces outside chat that reuse this provider. */
3182
3038
  function IntegrationsCard({ state, disabled, t, onEdit, onReset }) {
3183
3039
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsGroup, {
3184
3040
  title: t("integrationsTitle"),
3041
+ divided: true,
3185
3042
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3186
3043
  id: "cc-web-search",
3187
3044
  label: t("webSearch"),
@@ -3215,7 +3072,7 @@ window.__ModuleLoader__.load({
3215
3072
  const overridden = ADVANCED_FIELDS.filter((field) => state[field].overridden).length;
3216
3073
  const invalid = ADVANCED_FIELDS.some((field) => state[field].invalid);
3217
3074
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3218
- className: "cc-group",
3075
+ className: "cc-group cc-groupDivided",
3219
3076
  "aria-label": t("advancedSettings"),
3220
3077
  children: [
3221
3078
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -3295,6 +3152,17 @@ window.__ModuleLoader__.load({
3295
3152
  onEdit: (text) => onEdit("transportMaxRetries", text),
3296
3153
  onReset: () => onReset("transportMaxRetries"),
3297
3154
  t
3155
+ }),
3156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
3157
+ id: "cc-offload-seen-images-for-cache",
3158
+ label: t("offloadSeenImagesForCache"),
3159
+ hint: t("offloadSeenImagesForCacheHint"),
3160
+ state: state.offloadSeenImagesForCache,
3161
+ disabled,
3162
+ defaultChecked: false,
3163
+ onEdit: (text) => onEdit("offloadSeenImagesForCache", text),
3164
+ onReset: () => onReset("offloadSeenImagesForCache"),
3165
+ t
3298
3166
  })
3299
3167
  ]
3300
3168
  }) : null,
@@ -3358,12 +3226,33 @@ window.__ModuleLoader__.load({
3358
3226
  }
3359
3227
  return rows;
3360
3228
  }
3361
- /** The row's status dot: error for a rejected key, warning while cooling down. */
3229
+ /**
3230
+ * The row's status dot. Read top-down, so a problem always outranks "serving":
3231
+ * red for a key the Host rejected, amber while a rate limit is cooling down,
3232
+ * green for the account that is actually serving requests. A standby (or not
3233
+ * yet configured) account draws NOTHING — `cc-tabDotBlank` is an unfilled,
3234
+ * borderless dot that only holds the column so the account names stay aligned
3235
+ * with the rows that do have a verdict. Green therefore means "in use", not
3236
+ * "healthy": a wall of green dots would say nothing.
3237
+ */
3362
3238
  function statusDotClass(row) {
3363
3239
  const entry = row.usage;
3364
- if (!row.configured || entry?.mark === "invalid-credential") return "cc-tabDot cc-tabDotError";
3240
+ if (entry?.mark === "invalid-credential") return "cc-tabDot cc-tabDotError";
3365
3241
  if (entry !== void 0 && (entry.mark !== "" || entry.cooldownUntil > 0)) return "cc-tabDot cc-tabDotWarn";
3366
- return "cc-tabDot cc-tabDotOk";
3242
+ if (row.configured && entry?.active === true) return "cc-tabDot cc-tabDotOk";
3243
+ return "cc-tabDot cc-tabDotBlank";
3244
+ }
3245
+ /**
3246
+ * The same verdict as a sentence, for the dot's tooltip. Undefined on a blank
3247
+ * dot: there is no verdict to read out, and a hover hotspot on an invisible
3248
+ * 8px mark would only invite the question it cannot usefully answer.
3249
+ */
3250
+ function statusDotTitle(row, t) {
3251
+ const entry = row.usage;
3252
+ if (entry?.mark === "invalid-credential") return t("usageInvalidKey");
3253
+ if (entry !== void 0 && (entry.mark !== "" || entry.cooldownUntil > 0)) return t("usageCooldown");
3254
+ if (!row.configured) return t("apiKeyUnset");
3255
+ return entry?.active === true ? t("usageActive") : void 0;
3367
3256
  }
3368
3257
  /** One compact quota meter for a row's summary line. */
3369
3258
  function MiniMeter({ label, limit }) {
@@ -3587,6 +3476,7 @@ window.__ModuleLoader__.load({
3587
3476
  children: [
3588
3477
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3589
3478
  className: statusDotClass(row),
3479
+ title: statusDotTitle(row, t),
3590
3480
  "aria-hidden": "true"
3591
3481
  }),
3592
3482
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -3927,9 +3817,8 @@ window.__ModuleLoader__.load({
3927
3817
  }
3928
3818
  /**
3929
3819
  * The unified account card: every account the pool rotates through, one row
3930
- * each, with its status and quota inline. Replaces the former split between
3931
- * a credentials card, a usage card and a routing-rules card, so one account's
3932
- * facts no longer have to be cross-read across three surfaces.
3820
+ * each, with its status and quota inline — so one account's facts no longer
3821
+ * have to be cross-read across separate surfaces.
3933
3822
  */
3934
3823
  function AccountsCard({ t, state, usage, login, disabled, actions, onRefresh }) {
3935
3824
  const [adding, setAdding] = (0, react.useState)(false);
@@ -4278,7 +4167,6 @@ window.__ModuleLoader__.load({
4278
4167
  }
4279
4168
  //#endregion
4280
4169
  //#region src/client/login-row.tsx
4281
- /** The sign-in alternative to pasting a key (settings page + Models card). */
4282
4170
  function LoginRow({ state, targetRef, disabled, disabledHint, t, onBegin, onCancel }) {
4283
4171
  const { visible: visibleState, busyElsewhere } = loginStateForTarget(state, targetRef);
4284
4172
  const busy = visibleState.phase === "starting" || visibleState.phase === "waiting";
@@ -4329,11 +4217,11 @@ window.__ModuleLoader__.load({
4329
4217
  * Find the official editor card among the slot outlet's siblings, or null
4330
4218
  * while it is closed. The Models page renders the editor as an immediate
4331
4219
  * sibling of the outlet wrapper — after it in a provider row (the target of
4332
- * the row's 编辑 toggle), before it in the first-run setup card and the
4333
- * add-provider card, where it is always open. The editor is the only such
4334
- * sibling whose CSS module class carries the `editor` stem
4335
- * (`<hash>_editor`); the row header and the add card's provider select
4336
- * never do, so the lookup needs no hash knowledge.
4220
+ * the row's 编辑 toggle), before it in the first-run setup and add-provider
4221
+ * cards, where it is always open. The editor is the only such sibling whose
4222
+ * CSS module class carries the `editor` stem (`<hash>_editor`); the row header
4223
+ * and the add card's provider select never do, so the lookup needs no hash
4224
+ * knowledge.
4337
4225
  */
4338
4226
  function adjacentEditorCard(wrapper) {
4339
4227
  if (wrapper === null) return null;
@@ -4358,14 +4246,14 @@ window.__ModuleLoader__.load({
4358
4246
  apiKeyWritable: snapshot.apiKeyWritable
4359
4247
  };
4360
4248
  }
4361
- /** Status badge for the credential state (green when configured). */
4249
+ /** Credential-state badge: the solid pill when set, the muted outline when not. */
4362
4250
  function StatusBadge({ ok, okLabel, pendingLabel }) {
4363
4251
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4364
4252
  className: ok ? "cc-badge" : "cc-badgeMuted",
4365
4253
  children: ok ? okLabel : pendingLabel
4366
4254
  });
4367
4255
  }
4368
- /** Compact key field for the not-configured card. */
4256
+ /** The key paste field, with the show/hide toggle over it. */
4369
4257
  function CardKeyField({ state, disabled, t, onEdit }) {
4370
4258
  const [visible, setVisible] = (0, react.useState)(false);
4371
4259
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4407,16 +4295,12 @@ window.__ModuleLoader__.load({
4407
4295
  }
4408
4296
  /**
4409
4297
  * The slot component body. Dispatched on every Command Code provider card of
4410
- * the Models page (saved row, first-run setup posture, and add-provider
4411
- * draft).
4298
+ * the Models page (saved row, first-run setup posture, add-provider draft).
4412
4299
  *
4413
- * Closed (the official 编辑 toggle off) the panel renders nothing: the row
4414
- * head the Models page owns already names the provider and shows the
4415
- * credential dot, so a page full of providers stays compact. Opening the
4416
- * official editor mounts the editor shell as the outlet's sibling; the panel
4417
- * watches for it, hides the shell (it carries only the settings.yaml hint and
4418
- * a disabled apply for this namespace), and shows the real controls — badges,
4419
- * API-key field, sign-in, discard/save.
4300
+ * Closed (the official 编辑 toggle off) it renders nothing: the row head the
4301
+ * Models page owns already names the provider and shows the credential dot, so
4302
+ * a page full of providers stays compact. Open, it hides that editor shell and
4303
+ * shows the real controls — badges, API-key field, sign-in, discard/save.
4420
4304
  */
4421
4305
  function CommandCodeProviderCard(props) {
4422
4306
  const { t } = props;
@@ -4663,15 +4547,14 @@ window.__ModuleLoader__.load({
4663
4547
  return Math.abs(value) < .01 ? formatMoneyExact(value) : formatMoney(value);
4664
4548
  }
4665
4549
  /**
4666
- * What a figure shows when the endpoint that carries it did not report one.
4667
- * A dash, never a zero: the panel exists to state what the account has spent,
4668
- * so "we were not told" has to look different from "nothing".
4550
+ * What a figure shows when the endpoint that carries it did not report one. A
4551
+ * dash, never a zero: the panel exists to state what the account has spent, so
4552
+ * "we were not told" has to look different from "nothing".
4669
4553
  */
4670
4554
  const UNREPORTED = "—";
4671
4555
  /** Local reset time; empty when the endpoint reported none. */
4672
4556
  function resetText(ms) {
4673
- if (ms <= 0) return "";
4674
- return new Date(ms).toLocaleString();
4557
+ return formatResetAt(ms);
4675
4558
  }
4676
4559
  /** Local short date; empty when unset. */
4677
4560
  function dateText(ms) {
@@ -4714,18 +4597,17 @@ window.__ModuleLoader__.load({
4714
4597
  * Build the monthly credit view from the two endpoints that carry it.
4715
4598
  *
4716
4599
  * The limit is the PLAN's credit total, not the billing endpoint's
4717
- * `monthlyCredits` — that field is a remaining balance (see
4718
- * {@link PanelMonthlyView}). Unknown plans (no `monthlyCredits` on the plan
4719
- * record) therefore still show their balances, just without a ratio.
4600
+ * `monthlyCredits` (that field is a remaining balance — see
4601
+ * {@link PanelMonthlyView}), so a plan that publishes no total still shows its
4602
+ * balances, just without a ratio.
4720
4603
  *
4721
4604
  * The trap this guards: a MISSING balance is not a consumed one. The billing
4722
4605
  * endpoint is one of four the report fetches in parallel, so it fails on its
4723
- * own while the plan still arrives — and reading the absent balance as 0 would
4724
- * turn that partial failure into `limit - 0 = limit`, i.e. a confident "100%
4725
- * used, quota exhausted" for an account that may have spent nothing. The
4726
- * official CLI draws no meter unless the credits payload is present at all
4727
- * (`hasCreditsInfo`), so "unreported" is the upstream-faithful reading here
4728
- * too. Every derived figure stays a placeholder until the balance reported.
4606
+ * own while the plan still arrives, and reading the absent balance as 0 turns
4607
+ * that partial failure into `limit - 0 = limit` — a confident "100% used" for
4608
+ * an account that may have spent nothing. The official CLI draws no meter unless
4609
+ * the credits payload is present at all (`hasCreditsInfo`); every derived figure
4610
+ * here stays a placeholder until the balance reported.
4729
4611
  */
4730
4612
  function monthlyView(report) {
4731
4613
  const credits = report.credits;
@@ -4835,10 +4717,10 @@ window.__ModuleLoader__.load({
4835
4717
  };
4836
4718
  }
4837
4719
  /**
4838
- * Every panel string, resolved once per projection through the translator.
4839
- * One object with all keys (rather than per-field lookups in the components)
4840
- * keeps the copy table and the render sites in lockstep: a key cannot be read
4841
- * from `text` unless {@link PANEL_KEYS} declares it.
4720
+ * Every panel string, resolved once per projection through the translator. One
4721
+ * object with all keys (rather than per-field lookups in the components) keeps
4722
+ * the copy table and the render sites in lockstep: a key cannot be read from
4723
+ * `text` unless {@link PANEL_KEYS} declares it.
4842
4724
  */
4843
4725
  function panelStrings(t) {
4844
4726
  const out = {};
@@ -4908,9 +4790,9 @@ window.__ModuleLoader__.load({
4908
4790
  };
4909
4791
  }
4910
4792
  /**
4911
- * How often the panel re-reads the report while a surface is mounted. The
4912
- * quota windows move slowly and one report costs four upstream calls, so this
4913
- * is a background freshness tick, not a live meter.
4793
+ * How often the panel re-reads the report while a surface is mounted. The quota
4794
+ * windows move slowly and one report costs four upstream calls, so this is a
4795
+ * background freshness tick, not a live meter.
4914
4796
  */
4915
4797
  const PANEL_AUTO_REFRESH_MS = 12e4;
4916
4798
  /** The default timer (the client bundle runs in a browser; node tests inject one). */
@@ -4927,21 +4809,18 @@ window.__ModuleLoader__.load({
4927
4809
  /** Pending timer handle of the running loop. */
4928
4810
  let handle;
4929
4811
  /**
4930
- * Start the shared auto refresh. One fetch when the surface appears (the
4931
- * sidebar row is the point of the panel — it must be current, not wait for a
4932
- * click), then a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface
4933
- * stays mounted.
4812
+ * Start the shared auto refresh. One fetch when the surface appears (the sidebar
4813
+ * row is the point of the panel — it must be current, not wait for a click), then
4814
+ * a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface stays mounted.
4934
4815
  *
4935
- * Reference-counted: the sidebar entry and the dashboard can be mounted at
4936
- * once, so only the first start fetches and only the last stop halts the loop.
4937
- * Every tick goes through `usage.refresh()`, which already collapses a
4938
- * concurrent fetch onto the in-flight one — a tick never double-fetches
4939
- * against the settings page's own refresh.
4816
+ * Reference-counted: the sidebar entry and the dashboard can be mounted at once,
4817
+ * so only the first start fetches and only the last stop halts the loop. Every
4818
+ * tick goes through `usage.refresh()`, which collapses a concurrent fetch onto
4819
+ * the in-flight one. Returns the disposer that drops this surface's reference.
4940
4820
  *
4941
4821
  * @param usage - the shared usage controller.
4942
4822
  * @param isConfigured - whether a credential exists right now (re-read per tick).
4943
4823
  * @param timer - timer seam for tests.
4944
- * @returns the disposer that drops this surface's reference.
4945
4824
  */
4946
4825
  function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER) {
4947
4826
  references += 1;
@@ -4972,26 +4851,20 @@ window.__ModuleLoader__.load({
4972
4851
  //#endregion
4973
4852
  //#region src/client/panel-view.tsx
4974
4853
  /**
4975
- * React components for the Command Code plans & quota panel (browser half):
4976
- * the sidebar footer card and the dashboard it opens in the center column.
4854
+ * React components for the Command Code plans & quota panel: the sidebar footer
4855
+ * card and the dashboard it opens in the center column.
4977
4856
  *
4978
4857
  * Both render one {@link PanelView} projected by `./panel.ts` — no fact is
4979
4858
  * derived here. Strings arrive already localized in `view.text`: the slots
4980
4859
  * declare the `panel.commandcode` locale namespace, so the renderer hands this
4981
- * component a `t` seat that is passed into `buildPanelView` — the panel follows
4982
- * the harness's active language (a language switch mints a new `t`, so the
4983
- * surfaces re-render without an extra channel).
4984
- *
4985
- * The footer card is the panel's home: the sidebar shell renders it in the foot
4986
- * area directly above the Settings seat, so each quota window's own spend and
4987
- * limit — the 5-hour and the weekly one — are on screen without opening
4988
- * anything. Clicking it selects the `main` panel this file also renders — and
4989
- * unlike a `sidebar.panellist` row, whose button chrome and label the SHELL
4990
- * owns, this entry owns its whole surface and therefore calls `open()` itself.
4860
+ * component a `t` seat that is passed into `buildPanelView`, and a language
4861
+ * switch mints a new `t` identity, which re-renders both surfaces.
4991
4862
  *
4992
- * Styles ride the stylesheet `./panel-styles.ts` returns, injected once by
4993
- * the client entry; classes are `ccp-` prefixed to stay clear of the settings
4994
- * page's `cc-` set.
4863
+ * The footer card is the panel's home — the shell renders it in the foot area
4864
+ * directly above the Settings seat, and unlike a `sidebar.panellist` row (whose
4865
+ * chrome and label the SHELL owns) this entry owns its whole surface and
4866
+ * therefore calls `open()` itself. Styles ride the stylesheet
4867
+ * `./panel-styles.ts` returns; classes are `ccp-` prefixed.
4995
4868
  *
4996
4869
  * @module dsh-commandcode-provider/client/panel-view
4997
4870
  */
@@ -5114,14 +4987,6 @@ window.__ModuleLoader__.load({
5114
4987
  ]
5115
4988
  });
5116
4989
  }
5117
- /** One usage tile. */
5118
- function StatTile({ stat, label }) {
5119
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
5120
- label,
5121
- value: stat.value,
5122
- sub: stat.sub
5123
- });
5124
- }
5125
4990
  /** The rotation/credential badge line for one account. */
5126
4991
  function markText(account, text) {
5127
4992
  if (account.mark === void 0) return "";
@@ -5268,9 +5133,10 @@ window.__ModuleLoader__.load({
5268
5133
  children: text("usage")
5269
5134
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5270
5135
  className: "ccp-tiles",
5271
- children: account.stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatTile, {
5272
- stat,
5273
- label: text(stat.label)
5136
+ children: account.stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
5137
+ label: text(stat.label),
5138
+ value: stat.value,
5139
+ sub: stat.sub
5274
5140
  }, stat.label))
5275
5141
  })]
5276
5142
  }) : null
@@ -5409,27 +5275,24 @@ window.__ModuleLoader__.load({
5409
5275
  }
5410
5276
  /**
5411
5277
  * The sidebar footer card, registered into `sidebar.footer.action` — the list
5412
- * the shell renders in the sidebar's foot area directly ABOVE the Settings
5413
- * seat, so the panel reads as a bottom-pinned sibling of Settings rather than
5414
- * a global panel icon at the top of the column.
5278
+ * the shell renders in the foot area directly ABOVE the Settings seat, so the
5279
+ * panel reads as a bottom-pinned sibling of Settings rather than a global panel
5280
+ * icon at the top of the column.
5415
5281
  *
5416
- * The shell wraps nothing here, so this component owns the surface: the
5417
- * button, its chrome and its accessible name. In the expanded column it draws
5418
- * the title row, then one block per quota window (5-hour, then weekly) — the
5419
- * window's own spend and limit (`$1.32 / $6.00`), its percentage and its bar —
5420
- * and nothing else: the card's figures are the two windows the account runs
5421
- * into, so the period total stays in the tooltip rather than taking a third
5422
- * line. In the 56px rail it collapses to a 36px icon button carrying the ring,
5423
- * matching the shell's own rail geometry. `wide` comes from the shell as an
5424
- * owner prop — unlike the old `sidebar.panellist` row, this slot really does
5425
- * supply it.
5282
+ * The shell wraps nothing here, so this component owns the surface: the button,
5283
+ * its chrome and its accessible name. Expanded, it draws the title row and one
5284
+ * block per quota window (5-hour, then weekly) — the window's own spend and
5285
+ * limit, its percentage and its bar — and nothing else, because those two
5286
+ * windows are the figures an account runs into, so the period total stays in the
5287
+ * tooltip. In the 56px rail it collapses to a 36px icon button carrying the ring,
5288
+ * matching the shell's own rail geometry; `wide` comes from the shell.
5426
5289
  *
5427
5290
  * Rendering is gated on the settings page's `showSidebarQuota` toggle, which
5428
5291
  * defaults OFF: a fresh install shows no quota surface in the sidebar and runs
5429
5292
  * no background poll. The gate reads the STORED document (the settings
5430
- * controller's `sidebarQuota` fact), so an unsaved draft cannot show the card,
5431
- * while a landed save flips it live. The entry is still REGISTERED — this is a
5432
- * render decision, not a slot registration, so nothing churns the slot ledger.
5293
+ * controller's `sidebarQuota` fact), so an unsaved draft cannot show the card
5294
+ * while a landed save flips it live. The entry stays REGISTERED — this is a
5295
+ * render decision, so nothing churns the slot ledger.
5433
5296
  */
5434
5297
  function CommandCodeFooterEntry(props) {
5435
5298
  const visible = props.useCommandCodeSettings((snapshot) => snapshot.sidebarQuota);
@@ -5530,13 +5393,13 @@ window.__ModuleLoader__.load({
5530
5393
  return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
5531
5394
  }
5532
5395
  /**
5533
- * All prompt billing buckets determine a request's band, never cumulative session input.
5396
+ * All prompt billing buckets determine a request's band, never cumulative
5397
+ * session input.
5534
5398
  *
5535
5399
  * A tiered model returns the matching band and does NOT then apply `peak`: the
5536
- * page publishes those two dimensions independently, no row carries both today,
5537
- * and the generator would report a tiered row on every sync — so the combination
5538
- * becoming real is visible before it reaches a user. This ordering is a latent
5539
- * choice, not a verified upstream rule.
5400
+ * page publishes those two dimensions independently and no row carries both
5401
+ * today, so the combination becoming real is visible before it reaches a user.
5402
+ * This ordering is a latent choice, not a verified upstream rule.
5540
5403
  */
5541
5404
  function requestRates(price, at, contextTokens, table) {
5542
5405
  const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
@@ -5572,31 +5435,27 @@ window.__ModuleLoader__.load({
5572
5435
  /**
5573
5436
  * Session-cost view model for the composer readout.
5574
5437
  *
5575
- * Deliberately JSX-free and React-free, mirroring `./panel.ts`: it turns the
5576
- * session's token accounting plus the Host's price table into one presentation
5577
- * value AND the exact text the two surfaces of the harness's token-usage UI
5578
- * receive — the amount appended to the shipped pill, and the rows appended to
5579
- * the shipped usage dialog. Node tests therefore drive the whole calculation,
5580
- * and every user-visible string, without a DOM.
5438
+ * JSX-free and React-free, mirroring `./panel.ts`: it turns the session's token
5439
+ * accounting plus the Host's price table into one presentation value AND the
5440
+ * exact text the two surfaces of the harness's token-usage UI receive — the
5441
+ * amount appended to the shipped pill, and the rows appended to the shipped
5442
+ * usage dialog. Node tests therefore drive the whole calculation, and every
5443
+ * user-visible string, without a DOM.
5581
5444
  *
5582
- * The figures it prices are the session's DURABLE cumulative buckets, which is
5583
- * what the composer's own "N tokens" pill reads too (`tokenUsage`), so the two
5584
- * surfaces can never disagree about how much was used. Dollars are computed
5585
- * here because Command Code publishes per-token rates and bills against
5586
- * dollar-denominated windows; the account's own reported `totalCost` is a
5587
- * billing PERIOD figure, not this session's.
5445
+ * It prices the session's DURABLE cumulative buckets, which is what the
5446
+ * composer's own "N tokens" pill reads too (`tokenUsage`), so the two surfaces
5447
+ * can never disagree about how much was used. Dollars are computed here because
5448
+ * Command Code publishes per-token rates and bills against dollar-denominated
5449
+ * windows; the account's own reported `totalCost` is a billing PERIOD figure,
5450
+ * not this session's.
5588
5451
  *
5589
- * Three rules are load-bearing:
5590
- *
5591
- * 1. **Only Command Code usage is priced.** A session served by another
5592
- * provider must render nothing, never a Command Code estimate.
5593
- * 2. **A missing rate is never invented.** The pricing page publishes
5594
- * input/output/cache-read rates for every model but a cache-WRITE rate for
5595
- * only some, so unpriced cache-write tokens are surfaced as such rather than
5596
- * charged at a guessed multiple of the input rate.
5597
- * 3. **Unpriceable means invisible.** No usage, no model, no table, an unknown
5598
- * model, or all-zero buckets renders nothing at all — a confident `$0.00`
5599
- * would be a lie, and this module never returns one.
5452
+ * Three rules are load-bearing: only Command Code usage is priced (a session
5453
+ * served by another provider must render nothing, never a Command Code
5454
+ * estimate); a missing rate is never invented (the pricing page publishes a
5455
+ * cache-WRITE rate for only some models, so unpriced cache-write tokens are
5456
+ * surfaced as such rather than charged at a guessed multiple of the input rate);
5457
+ * and unpriceable means invisible — a confident `$0.00` would be a lie, and
5458
+ * this module never returns one.
5600
5459
  *
5601
5460
  * @module dsh-commandcode-provider/client/session-cost
5602
5461
  */
@@ -5621,15 +5480,12 @@ window.__ModuleLoader__.load({
5621
5480
  * between its own items.
5622
5481
  */
5623
5482
  separator: "·",
5624
- /** The heading the tooltip leads with. */
5625
5483
  panelTitle: "Session cost",
5626
5484
  /** Marks an estimate from published rates rather than an invoice. */
5627
5485
  approximate: "≈",
5628
- /** Tooltip line for the unpriced cache-write tokens. */
5629
5486
  unpricedCacheWrite: "cache write tokens have no published rate",
5630
5487
  /** Tooltip line naming the rate half in force. */
5631
5488
  peakRates: "peak rates",
5632
- /** Tooltip line naming the rate half in force. */
5633
5489
  offPeakRates: "off-peak rates",
5634
5490
  /** Tooltip line explaining the approximate marker. */
5635
5491
  approximateNote: "estimate from published rates, not the provider invoice",
@@ -5690,8 +5546,8 @@ window.__ModuleLoader__.load({
5690
5546
  *
5691
5547
  * The panel's `money()` convention (2 decimals, 4 below a cent) is right for
5692
5548
  * billing windows, but a live session total starts far below a cent, where
5693
- * `toFixed(4)` would print a flat `$0.0000` — which reads as broken rather than
5694
- * as small. So a total under $0.0001 is stated as a bound instead.
5549
+ * `toFixed(4)` would print a flat `$0.0000` — broken rather than small. So a
5550
+ * total under $0.0001 is stated as a bound instead.
5695
5551
  */
5696
5552
  function sessionCostAmount(total) {
5697
5553
  if (!Number.isFinite(total) || total <= 0) return formatMoney(0);
@@ -5899,15 +5755,14 @@ window.__ModuleLoader__.load({
5899
5755
  * and never the outlet's parent, because the parent is the composer footer,
5900
5756
  * which holds the `ContextMeter` as well, and that meter's trigger is itself a
5901
5757
  * `button[aria-haspopup="dialog"]` rendered AFTER the dock. A parent-scoped
5902
- * "last trigger wins" would therefore append the cost to the context ring
5903
- * instead of the token pill.
5758
+ * "last trigger wins" would append the cost to the context ring instead.
5904
5759
  */
5905
5760
  const STATS_ROOT = "[data-composer-stats]";
5906
5761
  /**
5907
5762
  * The shipped token-usage dialog's `<dl>`. Unique WITHIN one `StatsPills`
5908
- * render: the per-message turn-usage panel has its own dialog with different
5909
- * markup, so a single composer's usage dialog is unambiguous. It is not unique
5910
- * across the document once two composers are live — see {@link resolveDialog}.
5763
+ * render (the per-message turn-usage panel has its own dialog with different
5764
+ * markup), but not across the document once two composers are live — see
5765
+ * {@link resolveDialog}.
5911
5766
  */
5912
5767
  const USAGE_DIALOG = "[data-session-stats-usage]";
5913
5768
  /**
@@ -5917,8 +5772,8 @@ window.__ModuleLoader__.load({
5917
5772
  */
5918
5773
  const DIALOG_TRIGGER = "button[aria-haspopup=\"dialog\"]";
5919
5774
  /**
5920
- * Stable id for the hidden node the appended cost is described by. The pill's
5921
- * own `aria-label` is computed by the harness on every render and cannot be
5775
+ * Stable id for the hidden node the appended cost is described by. The pill's own
5776
+ * `aria-label` is recomputed by the harness on every render and cannot be
5922
5777
  * extended, so a description is how the cost reaches assistive technology.
5923
5778
  */
5924
5779
  const A11Y_ID = "dsh-commandcode-session-cost";
@@ -6040,11 +5895,9 @@ window.__ModuleLoader__.load({
6040
5895
  * registration order. {@link STATS_ROOT} narrows that to the stats row where
6041
5896
  * the markup marks it; where it does not, the outlet is already the narrowest
6042
5897
  * correct container, because the composer footer's `ContextMeter` sits BESIDE
6043
- * the outlet rather than inside it.
6044
- *
6045
- * A missing scope is "no pill", never a document-wide search: the outlet is
6046
- * what makes this lookup per-composer, and a second composer can be live at
6047
- * once (the sidebar can mount an embedded Conversation).
5898
+ * the outlet rather than inside it. A missing scope is "no pill", never a
5899
+ * document-wide search: the outlet is what makes this lookup per-composer, and
5900
+ * two composers can be live at once.
6048
5901
  */
6049
5902
  resolvePillButton() {
6050
5903
  const scope = this.scope();
@@ -6176,20 +6029,18 @@ window.__ModuleLoader__.load({
6176
6029
  *
6177
6030
  * The dialog is portaled onto `body`, so unlike the pill it cannot be scoped
6178
6031
  * from this entry, and the attribute is unique per DIALOG rather than per
6179
- * document. While one composer was guaranteed, a document-level lookup was
6180
- * exact. 0.1.6-alpha.2 ends that guarantee: it mounts an embedded Conversation
6181
- * in the sidebar, so a second composer — with its own session, its own dock
6182
- * and its own dialog — can be live and open at the same time.
6032
+ * document. While one composer was guaranteed a document-level lookup was
6033
+ * exact; 0.1.6-alpha.2 ended that guarantee by mounting an embedded
6034
+ * Conversation in the sidebar, so a second composer — its own session, dock and
6035
+ * dialog — can be live and open at the same time.
6183
6036
  *
6184
6037
  * There is no DOM link from a dialog back to its trigger (the portaled panel
6185
- * carries no id and no `aria-controls`), so with two dialogs open document
6186
- * order says nothing about ownership. The shape confirmation in
6187
- * {@link dialogShapeMatches} would usually reject the stranger, but it compares
6188
- * against THIS entry's counts, so a coincidentally matching dialog would be
6189
- * priced with another session's figures. Ambiguity is therefore answered by
6190
- * declining to decorate at all — the same rule the shape check already
6191
- * follows, applied one level up: a dialog we cannot prove is ours is left
6192
- * alone.
6038
+ * carries no id and no `aria-controls`), so document order says nothing about
6039
+ * ownership. The shape confirmation in {@link dialogShapeMatches} would usually
6040
+ * reject the stranger, but it compares against THIS entry's counts, so a
6041
+ * coincidentally matching dialog would be priced with another session's
6042
+ * figures. Ambiguity is therefore answered by declining to decorate at all:
6043
+ * a dialog we cannot prove is ours is left alone.
6193
6044
  */
6194
6045
  resolveDialog() {
6195
6046
  if (this.dialogHost?.isConnected === true) return this.dialogHost;
@@ -6231,12 +6082,11 @@ window.__ModuleLoader__.load({
6231
6082
  /**
6232
6083
  * Whether the dialog really holds the rows this session's buckets predict.
6233
6084
  *
6234
- * The shipped labels belong to the `chat` locale — they are `Uncached input` in
6235
- * English and something else entirely in Chinese — so rows are matched by
6236
- * POSITION, and this is what makes that safe: the row count must agree, the
6237
- * cache-hit row must be the percentage it is, and every other value must carry
6238
- * exactly the token count of the bucket predicted for it. A mismatch means the
6239
- * dialog is not what this view describes, and nothing is decorated.
6085
+ * The shipped labels belong to the `chat` locale and differ per language, so rows
6086
+ * are matched by POSITION, and this is what makes that safe: the row count must
6087
+ * agree, the cache-hit row must be the percentage it is, and every other value
6088
+ * must carry exactly the token count of the bucket predicted for it. A mismatch
6089
+ * means the dialog is not what this view describes, and nothing is decorated.
6240
6090
  */
6241
6091
  function dialogShapeMatches(pairs, plan, prices) {
6242
6092
  if (pairs.length !== plan.length) return false;
@@ -6269,13 +6119,13 @@ window.__ModuleLoader__.load({
6269
6119
  /**
6270
6120
  * The dock outlet this entry renders inside, i.e. what it scopes itself from.
6271
6121
  *
6272
- * The OUTLET itself, not its parent. The outlet is one `display:contents` div
6273
- * holding exactly this slot's entries (the shipped `stats` cell and ours), so it
6274
- * is the narrowest container that is still per-composer. Its parent is NOT:
6275
- * from 0.1.6-alpha.2 the composer stacks the dock inside a footer that also
6276
- * holds the `ContextMeter`, whose trigger is another
6277
- * `button[aria-haspopup="dialog"]` rendered after the dock — scoping to the
6278
- * parent would let the cost land on the context ring.
6122
+ * The OUTLET, not its parent. The outlet is one `display:contents` div holding
6123
+ * exactly this slot's entries (the shipped `stats` cell and ours), so it is the
6124
+ * narrowest container that is still per-composer. Its parent is NOT: from
6125
+ * 0.1.6-alpha.2 the composer stacks the dock inside a footer that also holds the
6126
+ * `ContextMeter`, whose trigger is another `button[aria-haspopup="dialog"]`
6127
+ * rendered after the dock — scoping to the parent would let the cost land on the
6128
+ * context ring.
6279
6129
  */
6280
6130
  const DOCK_ANCHOR = "[data-slot=\"conversation.composer.dock\"]";
6281
6131
  /** Module-level constant so the marker's `style` prop never diffs. */
@@ -6339,30 +6189,24 @@ window.__ModuleLoader__.load({
6339
6189
  * Stylesheet for the Command Code settings page — and the Models-page provider
6340
6190
  * card, which shares its `cc-` classes.
6341
6191
  *
6342
- * Returned as a string rather than injected here so the modules stay free of
6343
- * DOM side effects at import time: `./index.ts` installs it once, keyed by
6344
- * `PAGE_CSS_ID`. It is a module of its own rather than a literal inside the
6345
- * client entry so `tests/styles.test.ts` can audit every rule without importing
6346
- * the React tree (which pulls `*.module.css` through the primitives package and
6347
- * needs a loader registered first).
6348
- *
6349
- * Every colour comes from a harness theme alias with a neutral fallback, so the
6350
- * page follows the active theme (light/dark and any brand pack) without
6351
- * hardcoded values. Classes are `cc-` prefixed to stay clear of the panel's
6352
- * `ccp-` set.
6192
+ * Returned as a string, not injected here, so importing this module has no DOM
6193
+ * side effect: `./index.ts` installs it once under {@link PAGE_CSS_ID}. It is a
6194
+ * module of its own rather than a literal in the entry so `tests/styles.test.ts`
6195
+ * can audit every rule without importing the React tree (which pulls
6196
+ * `*.module.css` through the primitives package and needs a loader registered
6197
+ * first). The rules are GLOBAL CSS, so containment is a contract (issue #48):
6198
+ * every selector is qualified by one of our own `cc-` classes. Colours are
6199
+ * harness theme aliases with neutral fallbacks, and the `cc-` prefix keeps
6200
+ * these clear of the panel's `ccp-` set.
6353
6201
  *
6354
6202
  * @module dsh-commandcode-provider/client/page-styles
6355
6203
  */
6356
6204
  /**
6357
- * Stylesheet id (the `data-plugin-css` value that makes injection idempotent).
6205
+ * Stylesheet id — the `data-plugin-css` value that makes injection idempotent.
6358
6206
  *
6359
- * The package prefix must match the one the panel stylesheet uses
6360
- * (`PANEL_CSS_ID` in `./panel-styles.ts`) and this package's real name: an id is
6361
- * the injection's identity, so a stale fork prefix would let a second copy of
6362
- * the plugin inject the same rules twice and would misreport the owner in the
6363
- * DOM. The two ids must also stay distinct from each other — they key two
6364
- * separate style tags, and a shared id would make the second injection a no-op
6365
- * that silently drops one stylesheet.
6207
+ * The prefix must match this package's real name and the panel stylesheet's
6208
+ * (`PANEL_CSS_ID`); the two ids must also stay DISTINCT, since they key two
6209
+ * separate style tags and a shared id would make the second injection a no-op.
6366
6210
  */
6367
6211
  const PAGE_CSS_ID = "@mars-sea/dsh-commandcode-provider/CommandCodeSettingsPage.module.css";
6368
6212
  /**
@@ -6370,13 +6214,14 @@ window.__ModuleLoader__.load({
6370
6214
  *
6371
6215
  * The metrics are the harness's own settings pages', read out of the 0.1.7
6372
6216
  * bundles so the page sits beside General and Models without looking foreign:
6373
- * a row is 16px of padding over a 0.5px border-l2 hairline, its title 14/22
6374
- * label-primary and its description 12/18 label-tertiary, the control on the
6375
- * right. Selectors and buttons are capsules (36px / 28px compact), inputs are
6217
+ * a row is 12px of padding, its title 14/22 label-primary and its description
6218
+ * 12/18 label-tertiary, the control on the right. The hairline rhythm is
6219
+ * section-level: a 0.5px border-l2 rule separates the GROUPS (see
6220
+ * `cc-groupDivided`), never the rows inside one, so a group's rows read as one
6221
+ * list. Selectors and buttons are capsules (36px / 28px compact), inputs are
6376
6222
  * 32px with an 8px radius and a 0.5px border-l4 edge, a card is a 0.5px
6377
6223
  * border-l4 outline with a 16px radius, and a nested panel is a
6378
- * bg-module-platform fill with a 12px radius. Errors use state-error-primary,
6379
- * the platform's error colour (label-error is its text-only sibling).
6224
+ * bg-module-platform fill with a 12px radius. Errors use state-error-primary.
6380
6225
  */
6381
6226
  const PAGE_CSS = `
6382
6227
  .cc-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;display:flex}
@@ -6384,8 +6229,8 @@ window.__ModuleLoader__.load({
6384
6229
  .cc-intro{margin:4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px}
6385
6230
  .cc-readOnly{margin:8px 0 0;color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-tertiary));font-size:12px;line-height:18px}
6386
6231
  .cc-spacer{flex:1}
6387
- /* Groups: a heading over hairline-separated rows, no card surface. */
6388
6232
  .cc-group{flex-direction:column;display:flex;margin-top:28px}
6233
+ .cc-groupDivided{margin-top:24px;padding-top:20px;border-top:.5px solid var(--dsw-alias-border-l2)}
6389
6234
  .cc-groupHead{align-items:center;gap:8px;display:flex;min-height:28px;padding-bottom:4px}
6390
6235
  .cc-groupTitle{margin:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}
6391
6236
  .cc-groupDesc{margin:0 0 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
@@ -6393,10 +6238,8 @@ window.__ModuleLoader__.load({
6393
6238
  .cc-disclosure{width:100%;padding:0 0 4px;border:0;background:0 0;font:inherit;text-align:left;cursor:pointer;border-radius:6px}
6394
6239
  .cc-disclosure:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
6395
6240
  .cc-rows{flex-direction:column;display:flex}
6396
- .cc-row{align-items:center;gap:8px;display:flex;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
6397
- .cc-rows>.cc-row:last-child{border-bottom:0}
6398
- .cc-rowNested{padding-left:16px}
6399
- .cc-rowFlush{padding:0;border-bottom:0}
6241
+ .cc-row{align-items:center;gap:8px;display:flex;padding:12px 0}
6242
+ .cc-rowFlush{padding:0}
6400
6243
  .cc-rowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:32px;display:flex}
6401
6244
  .cc-rowTitleLine{align-items:center;gap:8px;display:flex;min-width:0}
6402
6245
  .cc-rowTitle{min-width:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}
@@ -6407,19 +6250,15 @@ window.__ModuleLoader__.load({
6407
6250
  .cc-rowControl{flex:none;align-items:center;justify-content:flex-end;gap:8px;display:inline-flex;max-width:60%}
6408
6251
  .cc-rowInput{width:200px}
6409
6252
  .cc-rowInputWide{width:280px}
6410
- /* The platform's link button: a compact capsule with no fill until hovered. */
6411
6253
  .cc-linkButton{box-sizing:border-box;flex:none;align-items:center;display:inline-flex;height:28px;padding:0 10px;border:0;border-radius:14px;background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}
6412
6254
  .cc-linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
6413
6255
  .cc-linkButton:disabled{cursor:default;opacity:.4}
6414
6256
  .cc-linkButton:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
6415
- /* Tags: the platform Tag's capsule (11/17, weight 500). */
6416
6257
  .cc-badges{align-items:center;gap:8px;display:inline-flex}
6417
- .cc-badge,.cc-badgeMuted{flex:none;align-items:center;display:inline-flex;white-space:nowrap;border-radius:999px;corner-shape:round;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
6258
+ .cc-badge,.cc-badgeMuted{flex:none;align-items:center;display:inline-flex;white-space:nowrap;border-radius:999px;corner-shape:round;padding:1px 8px;font-size:12px;font-weight:500;line-height:17px}
6418
6259
  .cc-badge{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
6419
6260
  .cc-badgeMuted{border:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-tertiary)}
6420
6261
  .cc-badgeWarn{background:var(--dsw-alias-state-warn-tertiary,var(--dsw-alias-bg-module-platform));color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-secondary))}
6421
- /* The stacked field (the Models-page provider card and the account forms):
6422
- * the platform SettingsForm field's metrics. */
6423
6262
  .cc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
6424
6263
  .cc-field+.cc-field{border-top:.5px solid var(--dsw-alias-border-l2)}
6425
6264
  .cc-fieldHead{align-items:center;gap:8px;display:flex}
@@ -6432,10 +6271,7 @@ window.__ModuleLoader__.load({
6432
6271
  .cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
6433
6272
  .cc-input:disabled{opacity:.6;cursor:default}
6434
6273
  .cc-inputInvalid{border-color:var(--dsw-alias-state-error-primary)}
6435
- .cc-invalid{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}
6436
6274
  .cc-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}
6437
- /* The model multi-select trigger: the platform's selector capsule (the
6438
- * language / permission pickers on General). */
6439
6275
  .cc-modelSelectAnchor{flex-direction:column;align-items:stretch;gap:6px;display:inline-flex;min-width:160px}
6440
6276
  .cc-selector{box-sizing:border-box;align-items:center;gap:12px;display:inline-flex;height:36px;padding:0 14px;border:0;border-radius:18px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;text-align:left;cursor:pointer}
6441
6277
  .cc-selector:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
@@ -6449,51 +6285,34 @@ window.__ModuleLoader__.load({
6449
6285
  .cc-modelSearch::-webkit-search-cancel-button{cursor:pointer}
6450
6286
  .cc-checkRow{align-items:center;gap:8px;display:inline-flex;min-width:0}
6451
6287
  .cc-checkRow:hover{cursor:pointer}
6452
- /* A hand-drawn box rather than the platform's accent-color checkbox: the
6453
- * native control's colours come from color-scheme, which the theme sets once
6454
- * at BOOT (it is not re-applied when the theme is switched in-session), so a
6455
- * native box can paint light-chrome white on a dark page. Drawn from tokens it
6456
- * cannot. The mark rides a brand-primary fill, so it takes the theme's brand
6457
- * foreground — the platform's pairing for anything painted on that fill, and
6458
- * the only one that survives the dark mode's near-white brand. The box
6459
- * outline is a border-l3 hairline: border-l2 (#ffffff1f in dark) leaves an
6460
- * unchecked box all but invisible against the menu surface. */
6288
+ /* A hand-drawn box, not the platform's accent-color checkbox: the native
6289
+ * control's colours come from color-scheme, which the theme sets once at BOOT
6290
+ * and never re-applies in-session, so a native box can paint light-chrome white
6291
+ * on a dark page. The mark rides a brand-primary fill, so it takes the brand
6292
+ * foreground (the only pairing that survives dark mode's near-white brand), and
6293
+ * the outline is a border-l3 hairline because border-l2 leaves an unchecked box
6294
+ * invisible against the menu surface. */
6461
6295
  .cc-check{box-sizing:border-box;appearance:none;flex-shrink:0;width:16px;height:16px;margin:0;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;background:0 0;position:relative}
6462
6296
  .cc-check:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
6463
6297
  .cc-check:checked::after{content:'';position:absolute;top:3px;left:5px;width:3px;height:7px;border:solid var(--dsw-alias-label-primary-foreground,#fff);border-width:0 1.5px 1.5px 0;transform:rotate(45deg)}
6464
6298
  .cc-checkName{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6465
- /* The platform SegmentedControl: a translucent track and one raised pill that
6466
- * slides under the picked segment. Segments are equal grid tracks, so the
6467
- * indicator's width and offset follow from the count and the index alone. */
6468
- .cc-segmented{position:relative;display:inline-grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:2px;padding:3px;border-radius:9px;background:var(--dsw-alias-interactive-bg-hover)}
6469
- .cc-segmentIndicator{position:absolute;top:3px;left:3px;width:calc((100% - 6px - 2px * (var(--cc-segment-count) - 1)) / var(--cc-segment-count));height:calc(100% - 6px);border-radius:7px;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft,0 1px 2px rgba(0,0,0,.12));transform:translateX(calc(var(--cc-segment-index) * (100% + 2px)));transition:transform .16s ease;pointer-events:none}
6470
- .cc-segment{box-sizing:border-box;position:relative;z-index:1;height:28px;padding:0 16px;border:0;border-radius:7px;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;font-weight:500;line-height:20px;white-space:nowrap;cursor:pointer;transition:color .12s ease}
6471
- .cc-segment:hover:not(:disabled),.cc-segment[aria-checked=true]{color:var(--dsw-alias-label-primary)}
6472
- .cc-segment:disabled{cursor:default;opacity:.4}
6473
- .cc-segment:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
6474
6299
  /* Modelled on the platform's own Switch primitive (ui-primitives
6475
- * Switch.module.css): a 36x20 capsule that pads its track by 2px and slides a
6476
- * 16px thumb across the 32px content box.
6477
- *
6478
- * Three things are load-bearing. (1) corner-shape:round on BOTH the track and
6479
- * the thumb: the theme sets a global superellipse corner shape on every
6480
- * element, and a superellipse capsule squares its ends off around a round
6481
- * thumb. (2) No literal colours. --dsw-alias-brand-primary INVERTS between the
6482
- * two modes (near-black in light, near-white in dark), so a hardcoded white
6483
- * thumb vanishes on the checked track in dark mode; the brand foreground is the
6484
- * token the platform paints on that fill. (3) --dsw-alias-border-l3 as the off
6485
- * track: border-l2 (#ffffff1f in dark) leaves it indistinguishable from the
6486
- * page. */
6300
+ * Switch.module.css): a 36x20 capsule padding its track by 2px and sliding a
6301
+ * 16px thumb across the 32px content box. Three things are load-bearing.
6302
+ * (1) corner-shape:round on BOTH track and thumb — the theme sets a global
6303
+ * superellipse corner shape, which squares a capsule's ends off around a round
6304
+ * thumb. (2) No literal colours: brand-primary INVERTS between the modes, so a
6305
+ * hardcoded white thumb vanishes on the checked track in dark mode; the brand
6306
+ * foreground is what the platform paints on that fill. (3) border-l3 as the
6307
+ * off track — border-l2 leaves it indistinguishable from the page. */
6487
6308
  .cc-toggle{box-sizing:border-box;appearance:none;flex-shrink:0;width:36px;height:20px;margin:0;padding:2px;border:0;border-radius:10px;corner-shape:round;background:var(--dsw-alias-border-l3);cursor:pointer;position:relative}
6488
6309
  .cc-toggle:checked{background:var(--dsw-alias-brand-primary)}
6489
6310
  .cc-toggle::after{content:'';display:block;width:16px;height:16px;border-radius:50%;corner-shape:round;background:var(--dsw-alias-label-primary-foreground);transition:transform .12s ease}
6490
- /* 32px content box minus the 16px thumb. */
6491
6311
  .cc-toggle:checked::after{transform:translateX(16px)}
6492
6312
  .cc-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
6493
6313
  .cc-toggle:disabled{cursor:default;opacity:.5}
6494
6314
  .cc-chevron{flex-shrink:0;border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);width:7px;height:7px;margin-right:6px;margin-bottom:3px;transform:rotate(45deg);transition:transform .15s ease}
6495
6315
  .cc-chevronUp{transform:rotate(-135deg);margin-bottom:-3px}
6496
- /* Accounts: one outlined card per account, the Models page's provider card. */
6497
6316
  .cc-accountMode{align-items:center;gap:4px;display:flex;margin:-4px 0 12px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
6498
6317
  .cc-accountList{flex-direction:column;gap:8px;display:flex}
6499
6318
  .cc-accountList+.cc-addButton,.cc-accountList+.cc-addPanel{margin-top:8px}
@@ -6506,10 +6325,11 @@ window.__ModuleLoader__.load({
6506
6325
  .cc-iconButton{flex-shrink:0;align-items:center;justify-content:center;display:inline-flex;width:28px;height:28px;padding:0;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-tertiary);cursor:pointer}
6507
6326
  .cc-iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
6508
6327
  .cc-iconButton:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
6509
- /* Three dots from one element: the dot itself plus two box-shadow copies. */
6328
+ /* Three dots from one element: the dot plus two box-shadow copies. */
6510
6329
  .cc-kebab{width:3px;height:3px;border-radius:50%;corner-shape:round;background:currentColor;box-shadow:0 -5px 0 currentColor,0 5px 0 currentColor}
6511
6330
  .cc-tabDot{flex-shrink:0;width:8px;height:8px;border-radius:50%;corner-shape:round}
6512
6331
  .cc-tabDotOk{background:var(--dsw-alias-state-success-primary)}
6332
+ .cc-tabDotBlank{background:0 0;border:0}
6513
6333
  .cc-tabDotWarn{background:var(--dsw-alias-state-warn-primary,#d97706)}
6514
6334
  .cc-tabDotError{background:var(--dsw-alias-state-error-primary)}
6515
6335
  .cc-accountMeters{flex-wrap:wrap;gap:6px 20px;display:flex;padding-left:16px}
@@ -6528,20 +6348,16 @@ window.__ModuleLoader__.load({
6528
6348
  .cc-confirmText{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}
6529
6349
  .cc-dangerButton{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
6530
6350
  .cc-dangerButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}
6531
- /* Adding an account: the Models page's dashed add button, opening a filled
6532
- * panel in its place. */
6533
6351
  .cc-addButton{box-sizing:border-box;align-items:center;justify-content:center;gap:6px;display:flex;width:100%;height:44px;border:1px dashed var(--dsw-alias-border-l3);border-radius:16px;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;cursor:pointer}
6534
6352
  .cc-addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
6535
6353
  .cc-addButton:disabled{cursor:default;opacity:.4}
6536
6354
  .cc-addButton:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}
6537
- /* A plus from two crossed bars. */
6538
6355
  .cc-addGlyph{position:relative;width:12px;height:12px}
6539
6356
  .cc-addGlyph::before,.cc-addGlyph::after{content:'';position:absolute;background:currentColor;border-radius:1px}
6540
6357
  .cc-addGlyph::before{left:0;right:0;top:5.25px;height:1.5px}
6541
6358
  .cc-addGlyph::after{top:0;bottom:0;left:5.25px;width:1.5px}
6542
6359
  .cc-addPanel{flex-direction:column;gap:12px;display:flex;padding:14px 16px;border-radius:12px;background:var(--dsw-alias-bg-module-platform)}
6543
6360
  .cc-panelTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}
6544
- /* Account report: stat tiles are filled panels, like the platform's editors. */
6545
6361
  .cc-accountReport{flex-direction:column;gap:14px;display:flex}
6546
6362
  .cc-usageHead{align-items:center;gap:8px;display:flex}
6547
6363
  .cc-usageTitle{color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:14px;font-weight:500;line-height:22px}
@@ -6549,10 +6365,9 @@ window.__ModuleLoader__.load({
6549
6365
  .cc-usagePlan{flex:none;white-space:nowrap;border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);border-radius:999px;corner-shape:round;padding:0 7px;font-size:11px;font-weight:500;line-height:17px}
6550
6366
  .cc-usagePlanStatus{flex:none;margin:0;white-space:nowrap;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
6551
6367
  .cc-usageHint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}
6552
- /* Two fixed grids instead of one auto-fit track: auto-fit wrapped the four
6553
- * activity tiles 3 + 1 at the details' width. Each group fills its own row
6554
- * (four activity tiles, three balances) and the activity row folds to 2 × 2
6555
- * in a narrow details panel. */
6368
+ /* Two fixed grids, not one auto-fit track: auto-fit wrapped the four activity
6369
+ * tiles 3 + 1 at the details' width. The activity row folds to 2 × 2 in a
6370
+ * narrow details panel. */
6556
6371
  .cc-usageStats{grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;display:grid}
6557
6372
  .cc-usageStatsActivity{grid-template-columns:repeat(4,minmax(0,1fr))}
6558
6373
  .cc-usageStatsBalance{grid-template-columns:repeat(3,minmax(0,1fr))}
@@ -6580,31 +6395,25 @@ window.__ModuleLoader__.load({
6580
6395
  .cc-usageBlockedHint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
6581
6396
  .cc-usageBlockedDetail{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px;word-break:break-word}
6582
6397
  .cc-version{margin:32px 0 0;text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
6583
- /* The update hint rides the footer version line, quiet until hovered. */
6584
6398
  .cc-versionLink{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-secondary));text-decoration:none}
6585
6399
  .cc-versionLink:hover{color:var(--dsw-alias-label-primary);text-decoration:underline;text-underline-position:under}
6586
- /* The Models-page provider panel root: unstyled by design — the row card the
6587
- * Models page owns supplies the surface, the panel only stacks its controls. */
6588
6400
  .cc-providerCard{flex-direction:column;display:flex}
6589
6401
  .cc-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}
6590
6402
  .cc-failed{display:inline-block;max-width:260px;min-width:0;color:var(--dsw-alias-state-error-primary);margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px}
6591
- /* The authorization link is the only link-coloured element on a login row. */
6592
6403
  .cc-loginLink{color:var(--dsw-alias-link,var(--dsw-alias-brand-primary));text-decoration:none;font-size:12px;line-height:18px}
6593
6404
  .cc-loginLink:hover{text-decoration:underline;text-underline-position:under}
6594
6405
  .cc-loginDone{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary))}
6595
6406
  .cc-loginError{color:var(--dsw-alias-state-error-primary)}
6596
6407
  /* The floating save bar: a centered capsule pinned to the bottom of the
6597
- * settings scrollport. The dock is a zero-height sticky strip at the end of
6598
- * the page, so the bar overlays the scrolling content instead of taking a row
6599
- * of its own; while it is shown the section reserves room at its end so the
6600
- * last line can still scroll clear of it. Hidden, it sinks and fades, and
6601
- * visibility (switched after the transition) takes its buttons out of the tab
6602
- * order.
6603
- *
6604
- * Its surface is an elevated layer (bg-layer-1 over the settings panel's
6605
- * layer-2) with the platform's prominent elevation and no stroke. The corners
6606
- * are concentric: a 44px capsule around 36px capsule buttons inset by 4px, so
6607
- * the gap around each button is even. */
6408
+ * settings scrollport. The dock is a zero-height sticky strip at the end of the
6409
+ * page, so the bar overlays the scrolling content instead of taking a row of
6410
+ * its own; while shown, the section reserves room at its end so the last line
6411
+ * can scroll clear. Hidden, it sinks and fades, and visibility (switched after
6412
+ * the transition) takes its buttons out of the tab order. Its surface is an
6413
+ * elevated layer (bg-layer-1 over the settings panel's layer-2) with the
6414
+ * platform's prominent elevation and no stroke, and its corners are concentric
6415
+ * (a 44px capsule around 36px buttons inset by 4px) so the gap around each
6416
+ * button is even. */
6608
6417
  .cc-sectionWithBar{padding-bottom:80px}
6609
6418
  .cc-saveBarDock{position:sticky;bottom:0;z-index:2;height:0;pointer-events:none}
6610
6419
  .cc-saveBar{--cc-saveBar-tone:var(--dsw-alias-state-warn-primary,#d97706);position:absolute;left:50%;bottom:20px;box-sizing:border-box;width:max-content;max-width:calc(100% - 24px);align-items:center;gap:10px;display:flex;height:44px;padding:4px 4px 4px 16px;border:0;border-radius:22px;corner-shape:round;background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-prominent,0 12px 32px -8px rgba(0,0,0,.24),0 2px 8px rgba(0,0,0,.08));opacity:0;visibility:hidden;transform:translate(-50%,12px);transition:opacity .16s ease,transform .16s ease,visibility 0s linear .16s}
@@ -6623,7 +6432,7 @@ window.__ModuleLoader__.load({
6623
6432
  .cc-saveBarGhost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
6624
6433
  .cc-saveBarPrimary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-foreground,#fff)}
6625
6434
  .cc-saveBarPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary)))}
6626
- @media (prefers-reduced-motion:reduce){.cc-chevron,.cc-toggle::after,.cc-usageBarFill,.cc-saveBar,.cc-saveBarShown,.cc-segmentIndicator,.cc-segment{transition:none}}
6435
+ @media (prefers-reduced-motion:reduce){.cc-chevron,.cc-toggle::after,.cc-usageBarFill,.cc-saveBar,.cc-saveBarShown{transition:none}}
6627
6436
  `;
6628
6437
  //#endregion
6629
6438
  //#region src/client/panel-styles.ts
@@ -6631,70 +6440,48 @@ window.__ModuleLoader__.load({
6631
6440
  * Stylesheet for the Command Code plans & quota panel (the sidebar footer card
6632
6441
  * and the dashboard it opens).
6633
6442
  *
6634
- * Returned as a string rather than injected here so the modules stay free of
6635
- * DOM side effects at import time — the client entry installs it once, keyed
6636
- * by the same `data-plugin-css` attribute the settings-page stylesheet
6637
- * (`./page-styles.ts`) uses, and removes it again when the plugin's fiber
6638
- * unwinds.
6639
- *
6640
- * These rules are GLOBAL CSS: every selector in here can match markup this
6641
- * plugin did not render. So each one is either qualified by a `ccp-` class of
6642
- * our own, or anchored to the sidebar region it has to lay out — and the one
6643
- * foreign anchor carries its justification in place (see the footer card
6644
- * block; issue #48). `tests/styles.test.ts` audits that containment for both
6645
- * stylesheets and simulates it against the real foreign class stems.
6443
+ * Returned as a string, not injected here, so importing this module has no DOM
6444
+ * side effect: `./index.ts` installs it once under {@link PANEL_CSS_ID} and
6445
+ * removes it when the fiber unwinds.
6646
6446
  *
6647
- * Every colour comes from a harness theme alias with a neutral fallback, so
6648
- * the panel follows the active theme (light/dark and any brand pack) without
6649
- * hardcoded values. Classes are `ccp-` prefixed to stay clear of the settings
6650
- * page's `cc-` set.
6447
+ * These rules are GLOBAL CSS, so containment is a contract (issue #48): every
6448
+ * selector is qualified by one of our own `ccp-` classes except the sidebar
6449
+ * anchor below, which carries its justification in place. `tests/styles.test.ts`
6450
+ * audits both stylesheets against the real foreign class stems. Colours are
6451
+ * harness theme aliases with neutral fallbacks; the `ccp-` prefix keeps these
6452
+ * clear of the settings page's `cc-` set.
6651
6453
  *
6652
6454
  * @module dsh-commandcode-provider/client/panel-styles
6653
6455
  */
6654
6456
  /**
6655
- * Stylesheet id (the `data-plugin-css` value that makes injection idempotent).
6457
+ * Stylesheet id — the `data-plugin-css` value that makes injection idempotent.
6656
6458
  *
6657
- * The package prefix must match the one the settings-page stylesheet uses
6658
- * (`PAGE_CSS_ID` in `./page-styles.ts`) and this package's real name: an id is
6659
- * the injection's identity, so a stale fork prefix would let a second copy of
6660
- * the plugin inject the same rules twice and would misreport the owner in the
6661
- * DOM. The two ids must also stay distinct from each other — they key two
6662
- * separate style tags, and a shared id would make the second injection a no-op
6663
- * that silently drops one stylesheet.
6459
+ * The prefix must match this package's real name and the settings-page sheet's
6460
+ * (`PAGE_CSS_ID`); the two ids must also stay DISTINCT, since they key two
6461
+ * separate style tags and a shared id would make the second injection a no-op.
6664
6462
  */
6665
6463
  const PANEL_CSS_ID = "@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css";
6666
6464
  /** The panel stylesheet. */
6667
6465
  const PANEL_CSS = `
6668
6466
  /* ------------------------------------------------- sidebar footer card */
6669
- /* The shell's foot area renders this list ABOVE the Settings seat, so the card
6670
- is the sidebar's bottom-most content. The shell supplies no chrome: the entry
6671
- is the button. It is deliberately quiet — a surface that sits beside Settings
6672
- should read as part of the column, not as a call to action — with one hover
6673
- step and a hairline border.
6674
-
6675
- The shell's container is a flex ROW whose occupants (this card and ui-cordis's
6676
- footer chip) each declare a full-width line and shrink-proof flex, so as a row
6677
- it would overflow the column: this card's flex:0 0 auto cannot shrink, and the
6678
- chip's width:100% basis would absorb the whole overflow and collapse to
6679
- nothing. Both were written for a full-width line, which is exactly what a
6680
- column gives them. Matched by the CSS-module class STEM — never a hashed name
6681
- — so a dsh that renames it degrades to the shell's own row rather than
6682
- breaking.
6467
+ /* The shell renders this list ABOVE the Settings seat, so the card is the
6468
+ sidebar's bottom-most content, and supplies no chrome: the entry IS the
6469
+ button. Deliberately quiet — a surface beside Settings should read as part of
6470
+ the column — with one hover step and a hairline border.
6683
6471
 
6684
- THE ANCHOR IS LOAD-BEARING, and it is the whole reason this selector is not
6685
- just [class*="_footerActions"] (issue #48). "footerActions" is NOT a stem this
6686
- shell owns alone: @deepseek-ai/dsh-client-ui-user-questions renders the
6687
- ask-user-question dialog's button row as Mbwy4a_footerActions, so the
6688
- unanchored rule forced THAT row into a column too and stacked the dialog's
6472
+ The shell's container is a flex ROW whose occupants each declare a full-width
6473
+ line and shrink-proof flex, so as a row it would overflow the column: this
6474
+ card cannot shrink and the neighbouring chip would absorb the overflow and
6475
+ collapse to nothing. The first rule is that fix, and THE ANCHOR IS
6476
+ LOAD-BEARING (issue #48): "footerActions" is not a stem this shell owns alone
6477
+ — dsh-client-ui-user-questions renders the ask-user-question dialog's button
6478
+ row as Mbwy4a_footerActions, and the unanchored rule stacked THAT row's
6689
6479
  side-by-side buttons on every page. "footArea" is declared by
6690
- dsh-client-ui-sidebar alone — verified against every client bundle of the
6691
- 0.1.6-alpha.1 engine, and against our own cc-/ccp- class names, which collide
6692
- with no engine class — so anchoring under it keeps the sidebar layout fix and
6693
- cannot reach another component's markup. The descendant combinator (rather
6694
- than a child one) deliberately survives a wrapper element appearing between
6695
- the two: a renamed or restructured shell should degrade to its own row, not
6696
- lose the fix. tests/styles.test.ts pins both halves of this — the dialog row
6697
- is no longer matched, the sidebar row still is. */
6480
+ dsh-client-ui-sidebar alone (audited across every client bundle of the
6481
+ 0.1.6-alpha.1 engine, and against our own cc-/ccp- names, which collide with
6482
+ no engine class). The DESCENDANT combinator survives a wrapper element
6483
+ appearing between the two, so a restructured shell degrades to its own row
6484
+ rather than losing the fix. tests/styles.test.ts pins both halves. */
6698
6485
  [class*="_footArea"] [class*="_footerActions"]{flex-direction:column}
6699
6486
  .ccp-foot{box-sizing:border-box;flex:0 0 auto;width:100%;min-width:0;font:inherit;color:var(--dsw-alias-label-secondary);text-align:left;cursor:pointer;background:0 0;border:1px solid transparent;border-radius:10px;flex-direction:column;gap:6px;margin:0 0 4px;padding:8px;display:flex}
6700
6487
  .ccp-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
@@ -6704,34 +6491,31 @@ window.__ModuleLoader__.load({
6704
6491
  /* One block per quota window: a head line carrying the window's own spend and
6705
6492
  limit, then the FULL-WIDTH bar under it. Stacking the two lets the card show
6706
6493
  the dollar figures — the reason this surface exists — without squeezing the
6707
- bar into what is left beside them. Mirrors the dashboard's own window block. */
6494
+ bar into what is left beside them. Mirrors the dashboard's window block. */
6708
6495
  .ccp-footRow{flex-direction:column;gap:4px;min-width:0;display:flex}
6709
6496
  .ccp-footHead{align-items:baseline;gap:8px;min-width:0;display:flex}
6710
6497
  .ccp-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
6711
6498
  .ccp-footAmount{flex:none;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums;white-space:nowrap}
6712
6499
  /* The card's markup must stay PHRASING content — it renders inside the shell's
6713
- own button — so these bars are spans, not divs. That makes display:block
6714
- load-bearing on BOTH: an inline box ignores width and height outright, so
6715
- without it the 5px track still painted (a flex item is blockified by its
6716
- container) while the fill collapsed to 0x0 and the bar showed no usage. */
6500
+ own button — so these bars are spans, which makes display:block load-bearing:
6501
+ an inline box ignores width outright, so the fill would collapse to 0x0 and
6502
+ the bar would show no usage. */
6717
6503
  .ccp-footBar{display:block;background:var(--dsw-alias-bg-layer-2);border-radius:999px;height:5px;overflow:hidden}
6718
6504
  .ccp-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
6719
6505
  .ccp-footFillWarn{background:var(--dsw-alias-state-error-primary)}
6720
6506
  .ccp-footPct{flex:none;width:34px;color:var(--dsw-alias-label-secondary);text-align:right;font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}
6721
6507
 
6722
- /* The 56px rail: one icon button on the shell's own rail geometry (36px cell),
6723
- so the collapsed column keeps a single 18px glyph like its siblings. */
6508
+ /* The 56px rail: one icon button on the shell's own 36px rail cell, so the
6509
+ collapsed column keeps a single 18px glyph like its siblings. */
6724
6510
  .ccp-railButton{box-sizing:border-box;width:36px;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:1px solid transparent;border-radius:8px;flex:none;justify-content:center;align-items:center;margin:0 0 4px;padding:0;display:inline-flex}
6725
6511
  .ccp-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
6726
6512
  .ccp-railButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
6727
6513
 
6728
- /* The ring glyph. Sized entirely by its own width/height attribute, so the
6514
+ /* The ring glyph, sized entirely by its own width/height attribute, so the
6729
6515
  footer row, the rail button and the dashboard can each ask for their own. */
6730
6516
  .ccp-glyph{flex:none;justify-content:center;align-items:center;display:inline-flex;color:var(--dsw-alias-brand-primary)}
6731
6517
 
6732
6518
  /* ------------------------------------------------------------ dashboard */
6733
- /* The center column in the layout frame: fill it, scroll the content column,
6734
- and cap the reading width like the harness's own panels. */
6735
6519
  .ccp-main{background:var(--dsw-alias-bg-layer-1);width:100%;height:100%;overflow:auto;display:block}
6736
6520
  .ccp-mainInner{max-width:760px;margin:0 auto;padding:24px 20px 40px;flex-direction:column;gap:14px;display:flex;color:var(--dsw-alias-label-primary)}
6737
6521
  .ccp-header{align-items:center;gap:10px;display:flex;flex-wrap:wrap}
@@ -6740,14 +6524,12 @@ window.__ModuleLoader__.load({
6740
6524
  .ccp-subtitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
6741
6525
  .ccp-spacer{flex:1}
6742
6526
  .ccp-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;font-variant-numeric:tabular-nums}
6743
- /* The dashboard's exit: an icon-sized glyph button. The primitive's own
6744
- geometry is kept (size="sm"); only the glyph is enlarged and optically
6745
- centred, so it reads as a dismiss control rather than a text button. */
6527
+ /* The dashboard's exit: the primitive's own size="sm" geometry is kept; only the
6528
+ glyph is enlarged and optically centred, so it reads as a dismiss control. */
6746
6529
  .ccp-close{min-width:28px;justify-content:center;padding-left:0;padding-right:0}
6747
6530
  .ccp-close span{font-size:16px;line-height:1}
6748
6531
  .ccp-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
6749
6532
 
6750
- /* Notices: the no-key guidance, a blocked report, and a stale-data error. */
6751
6533
  .ccp-notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:12px 14px;flex-direction:column;gap:4px;display:flex}
6752
6534
  .ccp-noticeError{border-color:var(--dsw-alias-state-error-primary)}
6753
6535
  .ccp-noticeTitle{margin:0;font-size:13px;font-weight:600;line-height:1.5}
@@ -6767,15 +6549,14 @@ window.__ModuleLoader__.load({
6767
6549
  .ccp-fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
6768
6550
  .ccp-planName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}
6769
6551
 
6770
- /* Stat tiles: the monthly credits and the usage totals share one grid. */
6552
+ /* The monthly credits and the usage totals share one grid. */
6771
6553
  .ccp-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
6772
6554
  .ccp-tile{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:8px 10px;flex-direction:column;gap:2px;display:flex;min-width:0}
6773
6555
  .ccp-tileLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}
6774
6556
  .ccp-tileValue{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;font-variant-numeric:tabular-nums}
6775
6557
  .ccp-tileSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6776
6558
 
6777
- /* Quota bars: the monthly bar and the two windows stack in one column, each a
6778
- label row plus the track. */
6559
+ /* Quota bars: the monthly bar and the two windows. */
6779
6560
  .ccp-windows{flex-direction:column;gap:14px;display:flex}
6780
6561
  .ccp-window{flex-direction:column;gap:6px;display:flex}
6781
6562
  .ccp-windowHead{align-items:baseline;gap:8px;display:flex}
@@ -6788,7 +6569,6 @@ window.__ModuleLoader__.load({
6788
6569
  .ccp-barFillWarn{background:var(--dsw-alias-state-error-primary)}
6789
6570
  .ccp-windowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
6790
6571
 
6791
- /* Badges. */
6792
6572
  .ccp-badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-brand-primary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;line-height:17px}
6793
6573
  .ccp-badgeError{background:transparent;color:var(--dsw-alias-state-error-primary)}
6794
6574
  .ccp-badgeWarn{background:var(--dsw-alias-state-warn-tertiary,var(--dsw-alias-bg-module-platform));color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-secondary))}
@@ -6815,12 +6595,14 @@ window.__ModuleLoader__.load({
6815
6595
  apiKeyLocked: "密钥由只读来源提供",
6816
6596
  apiBase: "API 地址",
6817
6597
  apiBaseHint: "默认 https://api.commandcode.ai,一般无需修改。",
6818
- requestTimeoutMs: "请求超时(毫秒)",
6819
- requestTimeoutMsHint: "等待响应首个字节的超时;默认 60000。",
6820
- streamIdleTimeoutMs: "流空闲超时(毫秒)",
6821
- streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300000(长思考模型可静默数分钟,默认值刻意放宽)。",
6598
+ requestTimeoutMs: "请求超时(秒)",
6599
+ requestTimeoutMsHint: "等待响应首个字节的超时;默认 300 秒(与官方 CLI 一致)。Provider API 路由要等上游模型吐出第一个 token 才返回响应头,上游慢时仍可能不够;报超时时可按提示调高。",
6600
+ streamIdleTimeoutMs: "流空闲超时(秒)",
6601
+ streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300 秒(长思考模型可静默数分钟,默认值刻意放宽)。",
6822
6602
  transportMaxRetries: "网络失败重试次数",
6823
6603
  transportMaxRetriesHint: "连接失败时自动重试几次,默认 5(各次等待 0.5+1+2+4+8 秒,合计约 15 秒)。超过后直接报错,不再按指数退避长时间等待;填 0 表示不重试。限流、5xx 等由服务端要求重试的失败不受此项影响。",
6604
+ offloadSeenImagesForCache: "已看过的图片不再重复发送",
6605
+ offloadSeenImagesForCacheHint: "仅 CLI 传输。模型看过图片并回复后,永久用文字占位替代旧图片,以减少新增图片时的缓存回退。模型如需重看旧图,必须重新读取文件或让用户重新附加。默认关闭。",
6824
6606
  advancedSettings: "高级设置",
6825
6607
  advancedSettingsHint: "API 地址和网络参数,一般无需修改。",
6826
6608
  advancedOverriddenOne: "已自定义 1 项",
@@ -6832,13 +6614,6 @@ window.__ModuleLoader__.load({
6832
6614
  webSearchHint: "使用同一密钥承载 dsh 的 web_search;关闭后恢复原搜索后端。",
6833
6615
  showSidebarQuota: "在侧边栏显示额度卡片",
6834
6616
  showSidebarQuotaHint: "在侧边栏底部显示套餐与额度卡片,点击可打开完整仪表盘。默认关闭,关闭时不会为它后台刷新用量。",
6835
- commandGuard: "AI 命令安全预判",
6836
- commandGuardHint: "命令确认弹窗前,先由 typesafe/jev 判断它是否安全;沙箱提权还会额外判断范围与必要性。三项都足够确定才自动放行,其余照常弹窗。默认关闭。",
6837
- commandGuardLevel: "自动放行阈值",
6838
- commandGuardLevelHint: "阈值越高,需要越确定才放行,弹窗越多。高 = 0.95,中 = 0.9(默认),低 = 0.8。判定超过 3 秒未返回会照常弹窗。",
6839
- commandGuardLevelHigh: "高",
6840
- commandGuardLevelMedium: "中",
6841
- commandGuardLevelLow: "低",
6842
6617
  zdr: "零数据保留(ZDR)",
6843
6618
  zdrHint: "仅使用不留存、不训练的上游;可用模型可能更少且价格通常更高。",
6844
6619
  accountsTitle: "账户",
@@ -6848,6 +6623,8 @@ window.__ModuleLoader__.load({
6848
6623
  modelSearchPlaceholder: "搜索模型…",
6849
6624
  modelSearchEmpty: "没有匹配的模型。",
6850
6625
  modelStale: "已下架",
6626
+ modelAllowance: "${amount}/月",
6627
+ modelAllowanceTitle: "该模型每月可用的额度(美元)。套餐内所有模型共用同一个月度额度池,这不是额外赠送的额度。",
6851
6628
  visibleModelsTitle: "模型白名单",
6852
6629
  visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
6853
6630
  visibleModelsPick: "选择要保留的模型…",
@@ -6974,12 +6751,14 @@ window.__ModuleLoader__.load({
6974
6751
  apiKeyLocked: "Key provided by a read-only source",
6975
6752
  apiBase: "API base URL",
6976
6753
  apiBaseHint: "Defaults to https://api.commandcode.ai; usually leave as-is.",
6977
- requestTimeoutMs: "Request timeout (ms)",
6978
- requestTimeoutMsHint: "Time to wait for the first response byte; default 60000.",
6979
- streamIdleTimeoutMs: "Stream idle timeout (ms)",
6980
- streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300000 (deliberately generous — long-thinking models can stay silent for minutes).",
6754
+ requestTimeoutMs: "Request timeout (seconds)",
6755
+ requestTimeoutMsHint: "Time to wait for the first response byte; default 300 s (the official CLI’s). The Provider API route withholds headers until the upstream model emits its first token, so a slow upstream can still outlast the default; raise it when a timeout message suggests it.",
6756
+ streamIdleTimeoutMs: "Stream idle timeout (seconds)",
6757
+ streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300 s (deliberately generous — long-thinking models can stay silent for minutes).",
6981
6758
  transportMaxRetries: "Transport retries",
6982
6759
  transportMaxRetriesHint: "How many times a failed connection is retried automatically; default 5 (waits of 0.5+1+2+4+8s, ~15s in total). After that the failure is reported instead of waiting on the exponential backoff. 0 disables the retries. Rate limits and 5xx answers keep their own, longer retry window.",
6760
+ offloadSeenImagesForCache: "Stop replaying images already seen",
6761
+ offloadSeenImagesForCacheHint: "CLI transport only. After this model responds to an image, durably replace older pixels with text to avoid cache rollback when another image arrives. To inspect an old image again, the model must reread its file or the user must attach it again. Off by default.",
6983
6762
  advancedSettings: "Advanced",
6984
6763
  advancedSettingsHint: "API base URL and network limits; usually leave as-is.",
6985
6764
  advancedOverriddenOne: "1 customized",
@@ -6991,13 +6770,6 @@ window.__ModuleLoader__.load({
6991
6770
  webSearchHint: "Use the same key for dsh web search; turning it off restores the previous backend.",
6992
6771
  showSidebarQuota: "Show the quota card in the sidebar",
6993
6772
  showSidebarQuotaHint: "Show a plan and quota card at the bottom of the sidebar that opens the full dashboard. Off by default; while off, no background usage refresh runs for it.",
6994
- commandGuard: "AI command guard",
6995
- commandGuardHint: "Before a shell command asks for approval, typesafe/jev judges whether it is safe. A sandbox escalation also needs separate scope and necessity verdicts; all three must be confident enough to skip the prompt. Off by default.",
6996
- commandGuardLevel: "Auto-approve threshold",
6997
- commandGuardLevelHint: "A higher threshold needs more confidence to skip the prompt, so you see more prompts. High = 0.95, Medium = 0.9 (default), Low = 0.8. A judgement that takes longer than 3 seconds shows the normal prompt.",
6998
- commandGuardLevelHigh: "High",
6999
- commandGuardLevelMedium: "Medium",
7000
- commandGuardLevelLow: "Low",
7001
6773
  zdr: "Zero data retention (ZDR)",
7002
6774
  zdrHint: "Use only upstreams that retain no data and do not train on requests; availability and pricing may differ.",
7003
6775
  accountsTitle: "Accounts",
@@ -7007,6 +6779,8 @@ window.__ModuleLoader__.load({
7007
6779
  modelSearchPlaceholder: "Search models…",
7008
6780
  modelSearchEmpty: "No matching models.",
7009
6781
  modelStale: "Retired",
6782
+ modelAllowance: "${amount}/mo",
6783
+ modelAllowanceTitle: "Monthly usage this model can draw, in USD. Every model shares one monthly pool — this is not extra credit on top of the plan.",
7010
6784
  visibleModelsTitle: "Model allowlist",
7011
6785
  visibleModelsHint: "Check the models you want to keep, and model pickers will list only those. If nothing is checked, every model is shown. After saving, the change applies the next time you open a model picker.",
7012
6786
  visibleModelsPick: "Select models to keep…",
@@ -7124,56 +6898,42 @@ window.__ModuleLoader__.load({
7124
6898
  };
7125
6899
  //#endregion
7126
6900
  //#region src/client/index.ts
7127
- /** Inject the page stylesheet once (idempotent per tag). */
7128
- function injectPageCss() {
7129
- if (typeof document === "undefined") return;
7130
- if (document.querySelector(`style[data-plugin-css="@mars-sea/dsh-commandcode-provider/CommandCodeSettingsPage.module.css"]`) !== null) return;
7131
- const tag = document.createElement("style");
7132
- tag.dataset.plugin = "@mars-sea/dsh-commandcode-provider";
7133
- tag.dataset.pluginCss = PAGE_CSS_ID;
7134
- tag.textContent = PAGE_CSS;
7135
- document.head.appendChild(tag);
7136
- }
7137
- /**
7138
- * Install the plans & quota panel's stylesheet and return its disposer, for
7139
- * `ctx.effect` to own. Keyed by its own `data-plugin-css` id, so the injection
7140
- * is idempotent even if a second surface asks for it later.
7141
- */
7142
- function injectPanelCss() {
6901
+ /** Install one stylesheet by id and return a disposer for its owning effect. */
6902
+ function injectCss(id, css) {
7143
6903
  if (typeof document === "undefined") return () => {};
7144
- if (document.querySelector(`style[data-plugin-css="@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css"]`) !== null) return () => {};
6904
+ if (document.querySelector(`style[data-plugin-css="${id}"]`) !== null) return () => {};
7145
6905
  const tag = document.createElement("style");
7146
6906
  tag.dataset.plugin = "@mars-sea/dsh-commandcode-provider";
7147
- tag.dataset.pluginCss = PANEL_CSS_ID;
7148
- tag.textContent = PANEL_CSS;
6907
+ tag.dataset.pluginCss = id;
6908
+ tag.textContent = css;
7149
6909
  document.head.appendChild(tag);
7150
- return () => {
7151
- tag.remove();
7152
- };
6910
+ return () => tag.remove();
7153
6911
  }
7154
6912
  /**
7155
6913
  * Plans & quota panel id. It is the layout's `MainPanelId`: one string shared
7156
6914
  * by the `sidebar.footer.action` card and the `main` slot cell, so the card
7157
6915
  * selects this panel and nothing else. `dsh-client-ui-layout` is not a
7158
- * dependency of this bundle (its type is only a brand over `string`), so the
7159
- * brand is applied at the call site instead of importing the package.
6916
+ * dependency of this bundle, so its brand is applied at the call site instead
6917
+ * of importing the package.
7160
6918
  */
7161
6919
  const PANEL_ID = "commandcode-panel";
7162
6920
  /**
7163
6921
  * The composer figure's entry id in `conversation.composer.dock`. Its own id,
7164
- * not the shipped `stats` cell's: reusing `stats` would REPLACE the tokens /
6922
+ * never the shipped `stats` cell's: reusing that id would REPLACE the tokens /
7165
6923
  * cache-hit / throughput readout rather than inject into it, and that readout
7166
- * is the harness's to format (see `./session-cost-display.ts`).
6924
+ * is the harness's to format.
7167
6925
  */
7168
6926
  const SESSION_COST_ID = "commandcode-session-cost";
7169
6927
  /**
7170
6928
  * Client plugin body. Gates on `slots`, `locale` and `remote` — never on
7171
6929
  * `settingsScope`, whose wrapper service the 0.1.7 settings rewrite removed
7172
- * (the settings scope here speaks the `remote.settings` wire directly). The
7173
- * page mounts once `remote.credentials` appears.
6930
+ * (the scope below speaks the `remote.settings` wire directly), because
6931
+ * gating on a service 0.1.7 does not provide would park EVERY surface below
6932
+ * into silence. `settingsScope` must never reappear in the exported inject
6933
+ * list at the bottom of this file.
7174
6934
  */
7175
6935
  function apply(ctx) {
7176
- injectPageCss();
6936
+ injectCss(PAGE_CSS_ID, PAGE_CSS);
7177
6937
  ctx.effect(() => ctx.locale.register("settings.commandcode", {
7178
6938
  zh,
7179
6939
  en
@@ -7422,7 +7182,7 @@ window.__ModuleLoader__.load({
7422
7182
  ctx.get("layout")?.selectPanel(null);
7423
7183
  }
7424
7184
  });
7425
- ctx.effect(() => injectPanelCss(), "dsh-commandcode-provider: panel styles");
7185
+ ctx.effect(() => injectCss(PANEL_CSS_ID, PANEL_CSS), "dsh-commandcode-provider: panel styles");
7426
7186
  try {
7427
7187
  ctx.slots.inject("main", () => ctx.slots.register({
7428
7188
  name: "main",