@mars-sea/dsh-commandcode-provider 0.11.14 → 0.11.17

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
@@ -576,14 +527,6 @@ window.__ModuleLoader__.load({
576
527
  };
577
528
  }
578
529
  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";
587
530
  /** The fields this page edits inside the `llm-commandcode` namespace. */
588
531
  const SECTION_FIELDS = [
589
532
  textField("apiBase"),
@@ -602,8 +545,6 @@ window.__ModuleLoader__.load({
602
545
  booleanField$1("filterModelsByPlan"),
603
546
  booleanField$1("webSearch"),
604
547
  booleanField$1("showSidebarQuota"),
605
- booleanField$1("commandGuard"),
606
- choiceField("commandGuardLevel", COMMAND_GUARD_LEVEL_CHOICES),
607
548
  booleanField$1("zdr")
608
549
  ];
609
550
  /** Whether two model-id lists are equal as sets (order-insensitive). */
@@ -643,17 +584,12 @@ window.__ModuleLoader__.load({
643
584
  }
644
585
  /**
645
586
  * Controller bridging the `llm-commandcode` scope and the credentials domain
646
- * onto the page.
587
+ * onto the page. See the module header for the two write paths.
647
588
  *
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.
589
+ * Account ops commit immediately, so a new account can be created, signed into
590
+ * and pinned in one gesture — the Host refuses a login for a reference the
591
+ * stored `accounts` list does not name, so a staged row could not be signed
592
+ * into at all.
657
593
  */
658
594
  var CommandCodeSettingsController = class {
659
595
  scope;
@@ -663,13 +599,11 @@ window.__ModuleLoader__.load({
663
599
  listeners = /* @__PURE__ */ new Set();
664
600
  disposers = [];
665
601
  disposed = false;
666
- /** The credential reference the default account resolves. */
667
602
  credentialRef = DEFAULT_API_KEY_REF;
668
603
  /** Host-reported configured/writable state per credential reference. */
669
604
  credentialStates = /* @__PURE__ */ new Map();
670
605
  /** Staged visible-model allowlist (undefined = no draft). */
671
606
  visibleModelsDraft = void 0;
672
- /** The catalog the model editors offer (Host-side). */
673
607
  catalogModels = [];
674
608
  catalogFailed = false;
675
609
  saving = false;
@@ -679,10 +613,6 @@ window.__ModuleLoader__.load({
679
613
  accountQueue = Promise.resolve();
680
614
  accountPending = 0;
681
615
  accountFailed = void 0;
682
- /**
683
- * @param scope - bound scope for the `llm-commandcode` namespace.
684
- * @param api - credentials wire face.
685
- */
686
616
  constructor(scope, api) {
687
617
  this.scope = scope;
688
618
  this.api = api;
@@ -748,8 +678,6 @@ window.__ModuleLoader__.load({
748
678
  filterModelsByPlan: this.field("filterModelsByPlan"),
749
679
  webSearch: this.field("webSearch"),
750
680
  showSidebarQuota: this.field("showSidebarQuota"),
751
- commandGuard: this.field("commandGuard"),
752
- commandGuardLevel: this.field("commandGuardLevel"),
753
681
  zdr: this.field("zdr"),
754
682
  sidebarQuota: this.sectionValue("showSidebarQuota") === true,
755
683
  activeAccount: typeof active === "string" ? active : "",
@@ -973,9 +901,8 @@ window.__ModuleLoader__.load({
973
901
  return target === "default" ? this.credentialRef : target;
974
902
  }
975
903
  /**
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.
904
+ * The first free `<credentialRef>_<n>` reference, so a renamed `apiKeyEnv`
905
+ * yields `MY_KEY_2`-style refs consistent with the default slot.
979
906
  */
980
907
  nextAccountRef() {
981
908
  const used = /* @__PURE__ */ new Set([this.credentialRef, ...this.storedExtras().map((extra) => extra.ref)]);
@@ -1133,10 +1060,10 @@ window.__ModuleLoader__.load({
1133
1060
  if (changed) this.publish();
1134
1061
  }
1135
1062
  /**
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.
1063
+ * Fetch the model catalog through the Host Remote. Runs once at construction;
1064
+ * call again (e.g. from the client entry once the Remote mount lands) to
1065
+ * (re)try — a later success clears a prior failure flag, so the editors
1066
+ * recover without a page reload.
1140
1067
  */
1141
1068
  refreshCatalog() {
1142
1069
  const models = this.api.models;
@@ -1355,11 +1282,9 @@ window.__ModuleLoader__.load({
1355
1282
  return String(value);
1356
1283
  }
1357
1284
  /**
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).
1285
+ * Success-rate percentage (already in percent units, e.g. 99.96): at most two
1286
+ * decimals, trailing zeros trimmed — `100` stays `100`, not `100.00`, and
1287
+ * `99.965552876334` becomes `99.97`. The `%` suffix is the caller's job.
1363
1288
  */
1364
1289
  function formatSuccessRate(value) {
1365
1290
  return String(Number(value.toFixed(2)));
@@ -1402,15 +1327,12 @@ window.__ModuleLoader__.load({
1402
1327
  };
1403
1328
  /**
1404
1329
  * 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()`.
1330
+ * budget (three retries at 1/2/4 s — bounded, not a poll). Public API mirrors
1331
+ * {@link CommandCodeUsageController}: `state()`, `subscribe` and `ensure()`;
1332
+ * `reload()` additionally drops the cache and restarts the budget when the
1333
+ * Host namespace rebinds. A MANUAL `ensure()` still fetches after the budget
1334
+ * is spent, and a PERMANENT failure — the Host serving no such endpoint — is
1335
+ * terminal for the binding until `reload()`.
1414
1336
  */
1415
1337
  var CommandCodePricesController = class {
1416
1338
  remote;
@@ -1444,12 +1366,9 @@ window.__ModuleLoader__.load({
1444
1366
  }
1445
1367
  /**
1446
1368
  * 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.
1369
+ * unless a bounded retry already owns the next attempt. Only the client
1370
+ * entry calls this (on rebind and on a manual refresh), so a mounted readout
1371
+ * renders the current state instead of triggering a fetch of its own.
1453
1372
  */
1454
1373
  ensure() {
1455
1374
  if (this.disposed || this.permanent || this.inFlight || this.current.status === "ready") return;
@@ -1575,12 +1494,11 @@ window.__ModuleLoader__.load({
1575
1494
  this.remote = remote;
1576
1495
  this.pollMs = pollMs;
1577
1496
  }
1578
- /** Subscribe to state projections. @returns the disposer. */
1497
+ /** Subscribe to state projections. */
1579
1498
  subscribe(listener) {
1580
1499
  this.listeners.add(listener);
1581
1500
  return () => this.listeners.delete(listener);
1582
1501
  }
1583
- /** Build the current panel state face. */
1584
1502
  state() {
1585
1503
  return {
1586
1504
  phase: this.phase,
@@ -1773,9 +1691,9 @@ window.__ModuleLoader__.load({
1773
1691
  return new Promise((resolve) => setTimeout(resolve, ms));
1774
1692
  }
1775
1693
  /**
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.
1694
+ * The per-reason copy for a failed attempt. Shared by the settings page's login
1695
+ * panel and the Models-page card's login row — the same reason can surface from
1696
+ * either.
1779
1697
  */
1780
1698
  function loginFailureCopy(reason, t) {
1781
1699
  if (reason === "denied") return t("loginDenied");
@@ -1788,8 +1706,8 @@ window.__ModuleLoader__.load({
1788
1706
  }
1789
1707
  /**
1790
1708
  * 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.
1709
+ * page's login panel and the Models-page card's login row so both surfaces
1710
+ * can never drift apart. Pure: no timers, no state.
1793
1711
  */
1794
1712
  function loginHint(state, t) {
1795
1713
  if (state.phase === "starting" || state.phase === "waiting") return {
@@ -1830,16 +1748,16 @@ window.__ModuleLoader__.load({
1830
1748
  /** The wire namespace all Command Code endpoints share. */
1831
1749
  const REMOTE_NAMESPACE = "commandcode";
1832
1750
  /**
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.
1751
+ * Build the validator helpers one endpoint uses. `prefix` names the endpoint
1752
+ * in the rejection message (e.g. `commandcode/report result:`), so each wire
1753
+ * file keeps its own diagnostic phrasing while sharing the helper bodies.
1837
1754
  *
1838
1755
  * The helpers return the reject call directly in the failure branch: since
1839
1756
  * `reject` is typed `never`, the ternary's union collapses to the success type
1840
1757
  * without relying on TypeScript's control-flow analysis of a never-returning
1841
1758
  * call (which only recognizes function declarations, not the destructured
1842
- * arrow `reject` callers receive from this factory).
1759
+ * arrow `reject` callers receive from this factory). Callers that need the
1760
+ * narrowing in an `if` must therefore assign inside the positive branch.
1843
1761
  */
1844
1762
  function makeBoundaryValidator(prefix) {
1845
1763
  const reject = (field) => {
@@ -1867,10 +1785,8 @@ window.__ModuleLoader__.load({
1867
1785
  }
1868
1786
  /**
1869
1787
  * 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.
1788
+ * `commandcode` namespace, the `commandcodeUsage` service, and a strict result
1789
+ * — only the endpoint, method, result type symbol, and schema differ.
1874
1790
  */
1875
1791
  function makeRemoteDescriptor(endpoint, method, typeSymbol, schema) {
1876
1792
  return {
@@ -1894,7 +1810,6 @@ window.__ModuleLoader__.load({
1894
1810
  * so rejection messages name the offending boundary.
1895
1811
  */
1896
1812
  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
1813
  /**
1899
1814
  * Parse one quota window, or undefined when the frame carries no such window.
1900
1815
  *
@@ -2174,9 +2089,9 @@ window.__ModuleLoader__.load({
2174
2089
  /**
2175
2090
  * Model-select helpers for the settings page's model editors (browser half).
2176
2091
  *
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
2092
+ * The routing-rule editor and the visible-models filter both pick catalog models
2093
+ * through the same checkbox multi-select dropdown (`ModelMultiSelect` in
2094
+ * section.tsx). The dropdown's data shaping — search filtering, stale-id
2180
2095
  * detection, tier grouping — lives here, React-free, so node tests can drive
2181
2096
  * it directly.
2182
2097
  *
@@ -2189,10 +2104,8 @@ window.__ModuleLoader__.load({
2189
2104
  */
2190
2105
  /**
2191
2106
  * 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.
2107
+ * `KNOWN_PLANS` tiers + `PLAN_LABELS` in src/capabilities.ts; an unknown tier
2108
+ * key falls back to the raw key rather than vanishing the row.
2196
2109
  */
2197
2110
  const TIER_HEADINGS = {
2198
2111
  go: "Go",
@@ -2222,14 +2135,11 @@ window.__ModuleLoader__.load({
2222
2135
  return model.id.toLowerCase().includes(needle) || model.name.toLowerCase().includes(needle);
2223
2136
  }
2224
2137
  /**
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.
2138
+ * Build the dropdown options: the catalog (already in picker order) plus any
2139
+ * selected ids the catalog no longer carries, flagged stale so a saved
2140
+ * selection never silently loses an entry. Stale rows survive a non-blank
2141
+ * {@link matchesModelQuery} search only while they match too, so a search for
2142
+ * a live model does not surface unrelated stale ids.
2233
2143
  */
2234
2144
  function buildModelSelectOptions(catalog, selected, query = "") {
2235
2145
  const catalogIds = new Set(catalog.map((model) => model.id));
@@ -2256,12 +2166,10 @@ window.__ModuleLoader__.load({
2256
2166
  return options;
2257
2167
  }
2258
2168
  /**
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.
2169
+ * Group dropdown options under plan-tier headings (`tierOf` maps a model id to
2170
+ * its heading, or undefined for unmapped models — see {@link tierHeadingFor}).
2171
+ * Live rows keep their relative order and repeats merge, so a catalog
2172
+ * interleaving two tiers still renders one section per tier.
2265
2173
  */
2266
2174
  function groupModelSelectOptions(options, tierOf) {
2267
2175
  const groups = [];
@@ -2291,13 +2199,11 @@ window.__ModuleLoader__.load({
2291
2199
  }
2292
2200
  /**
2293
2201
  * 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.
2202
+ * "retired". FALSE while the first fetch is in flight, after a failure, and
2203
+ * for a loaded-but-empty catalog — an empty list makes every selected id look
2204
+ * stale, which would turn the one-click stale cleanup into a button that
2205
+ * silently empties the allowlist. Users who really want it cleared have the
2206
+ * explicit "show all" entry.
2301
2207
  */
2302
2208
  function catalogIsReady(readiness) {
2303
2209
  return readiness.catalogIds.length > 0 && !readiness.catalogFailed;
@@ -2313,7 +2219,7 @@ window.__ModuleLoader__.load({
2313
2219
  }
2314
2220
  //#endregion
2315
2221
  //#region package.json
2316
- var version = "0.11.14";
2222
+ var version = "0.11.17";
2317
2223
  var repository = {
2318
2224
  "type": "git",
2319
2225
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -2321,17 +2227,15 @@ window.__ModuleLoader__.load({
2321
2227
  //#endregion
2322
2228
  //#region src/client/version.ts
2323
2229
  /**
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.
2230
+ * The plugin's own version, read from package.json at build time. The client
2231
+ * bundle inlines the JSON import (rolldown resolves it during the tsdown
2232
+ * build; node tests read it through tsx), so the rendered value always matches
2233
+ * the published package version with no second constant to keep in sync —
2234
+ * rendered as a muted footer line so a user can report the exact build.
2331
2235
  *
2332
2236
  * @module dsh-commandcode-provider/client/version
2333
2237
  */
2334
- /** The published package version (e.g. `'0.6.0'`). */
2238
+ /** The published package version. */
2335
2239
  const PLUGIN_VERSION = version;
2336
2240
  /**
2337
2241
  * This package's GitHub releases page, derived from the repository field so
@@ -2351,14 +2255,12 @@ window.__ModuleLoader__.load({
2351
2255
  */
2352
2256
  const NPM_LATEST_URL = "https://registry.npmjs.org/@mars-sea%2Fdsh-commandcode-provider/latest";
2353
2257
  /**
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.
2258
+ * Compare two version strings (`major.minor.patch[-pre]`): negative when `a`
2259
+ * sorts before `b`, positive when after, zero when equal.
2356
2260
  *
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.
2261
+ * Tolerant by design — a leading `v` is stripped, unparsable numeric parts
2262
+ * count as `0`, and semver prerelease rules apply. Enough for release tags,
2263
+ * not a full semver validator.
2362
2264
  */
2363
2265
  function compareVersions(a, b) {
2364
2266
  const left = splitVersion(a);
@@ -2494,21 +2396,17 @@ window.__ModuleLoader__.load({
2494
2396
  * React component for the "Command Code" settings page (browser half).
2495
2397
  *
2496
2398
  * 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
2399
+ * level as General / Models / Plugins. All copy comes from the
2499
2400
  * `settings.commandcode` locale namespace; all state comes from the
2500
2401
  * `CommandCodeSettingsController` injected by the slot registration.
2501
2402
  *
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.
2403
+ * Layout, top to bottom: Accounts (one card per account, status, quota,
2404
+ * actions and dedicated models — every operation commits immediately), Models
2405
+ * (plan filter + visible-model allowlist), Privacy & security (ZDR),
2406
+ * Integrations & display (web search, sidebar quota card), and a collapsed
2407
+ * Advanced card (API base + network limits). Everything below Accounts is a
2408
+ * flat group of rows (title/description left, control right) inside the staged
2409
+ * form the floating save bar writes.
2512
2410
  *
2513
2411
  * Styles are injected once by the client entry (see src/client/index.ts) and
2514
2412
  * class-prefixed `cc-` to stay local.
@@ -2645,53 +2543,6 @@ window.__ModuleLoader__.load({
2645
2543
  })] })
2646
2544
  });
2647
2545
  }
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
2546
  /** One stat tile in the account card's summary grid. */
2696
2547
  function UsageStat({ label, value, sub }) {
2697
2548
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2760,10 +2611,7 @@ window.__ModuleLoader__.load({
2760
2611
  ]
2761
2612
  });
2762
2613
  }
2763
- /**
2764
- * One pool account's facts (identity, totals, credits, window limits)
2765
- * rendered inside an account row's details.
2766
- */
2614
+ /** One pool account's facts (identity, totals, credits, window limits). */
2767
2615
  function AccountReport({ entry, fetchedAt, t, summaryOnly = false, headless = false }) {
2768
2616
  const report = entry.report;
2769
2617
  const account = report.account;
@@ -2919,16 +2767,12 @@ window.__ModuleLoader__.load({
2919
2767
  return t("usageNetworkHint");
2920
2768
  }
2921
2769
  /**
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.
2770
+ * A checkbox multi-select dropdown for picking catalog models (the routing-rule
2771
+ * rows and the visible-models filter share it). The trigger shows the selection
2772
+ * count; the Menu lists every catalog model, grouping under plan-tier headings
2773
+ * and flagging selected ids the catalog no longer carries as stale, so a saved
2774
+ * selection never silently loses an entry (the VisibleModelsCard offers a
2775
+ * one-click cleanup).
2932
2776
  *
2933
2777
  * With `deferCommit` the toggles collect in a local draft that is handed to
2934
2778
  * `onSelect` once, when the menu closes: the per-account picker writes
@@ -3118,64 +2962,23 @@ window.__ModuleLoader__.load({
3118
2962
  });
3119
2963
  }
3120
2964
  /**
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.
2965
+ * Privacy & security: the opt-in that changes where data goes. ZDR restricts
2966
+ * which upstream serves a request, so it starts off.
3125
2967
  */
3126
2968
  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, {
2969
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsGroup, {
3129
2970
  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
- ]
2971
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleField, {
2972
+ id: "cc-zdr",
2973
+ label: t("zdr"),
2974
+ hint: t("zdrHint"),
2975
+ state: state.zdr,
2976
+ disabled,
2977
+ defaultChecked: false,
2978
+ onEdit: (text) => onEdit("zdr", text),
2979
+ onReset: () => onReset("zdr"),
2980
+ t
2981
+ })
3179
2982
  });
3180
2983
  }
3181
2984
  /** Integrations & display: surfaces outside chat that reuse this provider. */
@@ -3424,7 +3227,7 @@ window.__ModuleLoader__.load({
3424
3227
  className: "cc-input",
3425
3228
  type: secret && !visible ? "password" : "text",
3426
3229
  "aria-label": label,
3427
- autoComplete: "off",
3230
+ autoComplete: "new-password",
3428
3231
  spellCheck: false,
3429
3232
  autoFocus: true,
3430
3233
  placeholder,
@@ -3927,9 +3730,8 @@ window.__ModuleLoader__.load({
3927
3730
  }
3928
3731
  /**
3929
3732
  * 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.
3733
+ * each, with its status and quota inline — so one account's facts no longer
3734
+ * have to be cross-read across separate surfaces.
3933
3735
  */
3934
3736
  function AccountsCard({ t, state, usage, login, disabled, actions, onRefresh }) {
3935
3737
  const [adding, setAdding] = (0, react.useState)(false);
@@ -4278,7 +4080,6 @@ window.__ModuleLoader__.load({
4278
4080
  }
4279
4081
  //#endregion
4280
4082
  //#region src/client/login-row.tsx
4281
- /** The sign-in alternative to pasting a key (settings page + Models card). */
4282
4083
  function LoginRow({ state, targetRef, disabled, disabledHint, t, onBegin, onCancel }) {
4283
4084
  const { visible: visibleState, busyElsewhere } = loginStateForTarget(state, targetRef);
4284
4085
  const busy = visibleState.phase === "starting" || visibleState.phase === "waiting";
@@ -4329,11 +4130,11 @@ window.__ModuleLoader__.load({
4329
4130
  * Find the official editor card among the slot outlet's siblings, or null
4330
4131
  * while it is closed. The Models page renders the editor as an immediate
4331
4132
  * 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.
4133
+ * the row's 编辑 toggle), before it in the first-run setup and add-provider
4134
+ * cards, where it is always open. The editor is the only such sibling whose
4135
+ * CSS module class carries the `editor` stem (`<hash>_editor`); the row header
4136
+ * and the add card's provider select never do, so the lookup needs no hash
4137
+ * knowledge.
4337
4138
  */
4338
4139
  function adjacentEditorCard(wrapper) {
4339
4140
  if (wrapper === null) return null;
@@ -4358,14 +4159,14 @@ window.__ModuleLoader__.load({
4358
4159
  apiKeyWritable: snapshot.apiKeyWritable
4359
4160
  };
4360
4161
  }
4361
- /** Status badge for the credential state (green when configured). */
4162
+ /** Credential-state badge: the solid pill when set, the muted outline when not. */
4362
4163
  function StatusBadge({ ok, okLabel, pendingLabel }) {
4363
4164
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4364
4165
  className: ok ? "cc-badge" : "cc-badgeMuted",
4365
4166
  children: ok ? okLabel : pendingLabel
4366
4167
  });
4367
4168
  }
4368
- /** Compact key field for the not-configured card. */
4169
+ /** The key paste field, with the show/hide toggle over it. */
4369
4170
  function CardKeyField({ state, disabled, t, onEdit }) {
4370
4171
  const [visible, setVisible] = (0, react.useState)(false);
4371
4172
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4392,7 +4193,7 @@ window.__ModuleLoader__.load({
4392
4193
  id: "cc-card-api-key",
4393
4194
  className: "cc-input",
4394
4195
  type: visible ? "text" : "password",
4395
- autoComplete: "off",
4196
+ autoComplete: "new-password",
4396
4197
  spellCheck: false,
4397
4198
  value: state.text,
4398
4199
  disabled,
@@ -4407,16 +4208,12 @@ window.__ModuleLoader__.load({
4407
4208
  }
4408
4209
  /**
4409
4210
  * 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).
4211
+ * the Models page (saved row, first-run setup posture, add-provider draft).
4412
4212
  *
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.
4213
+ * Closed (the official 编辑 toggle off) it renders nothing: the row head the
4214
+ * Models page owns already names the provider and shows the credential dot, so
4215
+ * a page full of providers stays compact. Open, it hides that editor shell and
4216
+ * shows the real controls — badges, API-key field, sign-in, discard/save.
4420
4217
  */
4421
4218
  function CommandCodeProviderCard(props) {
4422
4219
  const { t } = props;
@@ -4663,9 +4460,9 @@ window.__ModuleLoader__.load({
4663
4460
  return Math.abs(value) < .01 ? formatMoneyExact(value) : formatMoney(value);
4664
4461
  }
4665
4462
  /**
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".
4463
+ * What a figure shows when the endpoint that carries it did not report one. A
4464
+ * dash, never a zero: the panel exists to state what the account has spent, so
4465
+ * "we were not told" has to look different from "nothing".
4669
4466
  */
4670
4467
  const UNREPORTED = "—";
4671
4468
  /** Local reset time; empty when the endpoint reported none. */
@@ -4714,18 +4511,17 @@ window.__ModuleLoader__.load({
4714
4511
  * Build the monthly credit view from the two endpoints that carry it.
4715
4512
  *
4716
4513
  * 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.
4514
+ * `monthlyCredits` (that field is a remaining balance — see
4515
+ * {@link PanelMonthlyView}), so a plan that publishes no total still shows its
4516
+ * balances, just without a ratio.
4720
4517
  *
4721
4518
  * The trap this guards: a MISSING balance is not a consumed one. The billing
4722
4519
  * 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.
4520
+ * own while the plan still arrives, and reading the absent balance as 0 turns
4521
+ * that partial failure into `limit - 0 = limit` — a confident "100% used" for
4522
+ * an account that may have spent nothing. The official CLI draws no meter unless
4523
+ * the credits payload is present at all (`hasCreditsInfo`); every derived figure
4524
+ * here stays a placeholder until the balance reported.
4729
4525
  */
4730
4526
  function monthlyView(report) {
4731
4527
  const credits = report.credits;
@@ -4835,10 +4631,10 @@ window.__ModuleLoader__.load({
4835
4631
  };
4836
4632
  }
4837
4633
  /**
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.
4634
+ * Every panel string, resolved once per projection through the translator. One
4635
+ * object with all keys (rather than per-field lookups in the components) keeps
4636
+ * the copy table and the render sites in lockstep: a key cannot be read from
4637
+ * `text` unless {@link PANEL_KEYS} declares it.
4842
4638
  */
4843
4639
  function panelStrings(t) {
4844
4640
  const out = {};
@@ -4908,9 +4704,9 @@ window.__ModuleLoader__.load({
4908
4704
  };
4909
4705
  }
4910
4706
  /**
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.
4707
+ * How often the panel re-reads the report while a surface is mounted. The quota
4708
+ * windows move slowly and one report costs four upstream calls, so this is a
4709
+ * background freshness tick, not a live meter.
4914
4710
  */
4915
4711
  const PANEL_AUTO_REFRESH_MS = 12e4;
4916
4712
  /** The default timer (the client bundle runs in a browser; node tests inject one). */
@@ -4927,21 +4723,18 @@ window.__ModuleLoader__.load({
4927
4723
  /** Pending timer handle of the running loop. */
4928
4724
  let handle;
4929
4725
  /**
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.
4726
+ * Start the shared auto refresh. One fetch when the surface appears (the sidebar
4727
+ * row is the point of the panel — it must be current, not wait for a click), then
4728
+ * a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface stays mounted.
4934
4729
  *
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.
4730
+ * Reference-counted: the sidebar entry and the dashboard can be mounted at once,
4731
+ * so only the first start fetches and only the last stop halts the loop. Every
4732
+ * tick goes through `usage.refresh()`, which collapses a concurrent fetch onto
4733
+ * the in-flight one. Returns the disposer that drops this surface's reference.
4940
4734
  *
4941
4735
  * @param usage - the shared usage controller.
4942
4736
  * @param isConfigured - whether a credential exists right now (re-read per tick).
4943
4737
  * @param timer - timer seam for tests.
4944
- * @returns the disposer that drops this surface's reference.
4945
4738
  */
4946
4739
  function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER) {
4947
4740
  references += 1;
@@ -4972,26 +4765,20 @@ window.__ModuleLoader__.load({
4972
4765
  //#endregion
4973
4766
  //#region src/client/panel-view.tsx
4974
4767
  /**
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.
4768
+ * React components for the Command Code plans & quota panel: the sidebar footer
4769
+ * card and the dashboard it opens in the center column.
4977
4770
  *
4978
4771
  * Both render one {@link PanelView} projected by `./panel.ts` — no fact is
4979
4772
  * derived here. Strings arrive already localized in `view.text`: the slots
4980
4773
  * 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).
4774
+ * component a `t` seat that is passed into `buildPanelView`, and a language
4775
+ * switch mints a new `t` identity, which re-renders both surfaces.
4984
4776
  *
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.
4991
- *
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.
4777
+ * The footer card is the panel's home — the shell renders it in the foot area
4778
+ * directly above the Settings seat, and unlike a `sidebar.panellist` row (whose
4779
+ * chrome and label the SHELL owns) this entry owns its whole surface and
4780
+ * therefore calls `open()` itself. Styles ride the stylesheet
4781
+ * `./panel-styles.ts` returns; classes are `ccp-` prefixed.
4995
4782
  *
4996
4783
  * @module dsh-commandcode-provider/client/panel-view
4997
4784
  */
@@ -5409,27 +5196,24 @@ window.__ModuleLoader__.load({
5409
5196
  }
5410
5197
  /**
5411
5198
  * 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.
5199
+ * the shell renders in the foot area directly ABOVE the Settings seat, so the
5200
+ * panel reads as a bottom-pinned sibling of Settings rather than a global panel
5201
+ * icon at the top of the column.
5415
5202
  *
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.
5203
+ * The shell wraps nothing here, so this component owns the surface: the button,
5204
+ * its chrome and its accessible name. Expanded, it draws the title row and one
5205
+ * block per quota window (5-hour, then weekly) — the window's own spend and
5206
+ * limit, its percentage and its bar — and nothing else, because those two
5207
+ * windows are the figures an account runs into, so the period total stays in the
5208
+ * tooltip. In the 56px rail it collapses to a 36px icon button carrying the ring,
5209
+ * matching the shell's own rail geometry; `wide` comes from the shell.
5426
5210
  *
5427
5211
  * Rendering is gated on the settings page's `showSidebarQuota` toggle, which
5428
5212
  * defaults OFF: a fresh install shows no quota surface in the sidebar and runs
5429
5213
  * 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.
5214
+ * controller's `sidebarQuota` fact), so an unsaved draft cannot show the card
5215
+ * while a landed save flips it live. The entry stays REGISTERED — this is a
5216
+ * render decision, so nothing churns the slot ledger.
5433
5217
  */
5434
5218
  function CommandCodeFooterEntry(props) {
5435
5219
  const visible = props.useCommandCodeSettings((snapshot) => snapshot.sidebarQuota);
@@ -5530,13 +5314,13 @@ window.__ModuleLoader__.load({
5530
5314
  return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
5531
5315
  }
5532
5316
  /**
5533
- * All prompt billing buckets determine a request's band, never cumulative session input.
5317
+ * All prompt billing buckets determine a request's band, never cumulative
5318
+ * session input.
5534
5319
  *
5535
5320
  * 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.
5321
+ * page publishes those two dimensions independently and no row carries both
5322
+ * today, so the combination becoming real is visible before it reaches a user.
5323
+ * This ordering is a latent choice, not a verified upstream rule.
5540
5324
  */
5541
5325
  function requestRates(price, at, contextTokens, table) {
5542
5326
  const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
@@ -5572,31 +5356,27 @@ window.__ModuleLoader__.load({
5572
5356
  /**
5573
5357
  * Session-cost view model for the composer readout.
5574
5358
  *
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.
5581
- *
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.
5359
+ * JSX-free and React-free, mirroring `./panel.ts`: it turns the session's token
5360
+ * accounting plus the Host's price table into one presentation value AND the
5361
+ * exact text the two surfaces of the harness's token-usage UI receive — the
5362
+ * amount appended to the shipped pill, and the rows appended to the shipped
5363
+ * usage dialog. Node tests therefore drive the whole calculation, and every
5364
+ * user-visible string, without a DOM.
5588
5365
  *
5589
- * Three rules are load-bearing:
5366
+ * It prices the session's DURABLE cumulative buckets, which is what the
5367
+ * composer's own "N tokens" pill reads too (`tokenUsage`), so the two surfaces
5368
+ * can never disagree about how much was used. Dollars are computed here because
5369
+ * Command Code publishes per-token rates and bills against dollar-denominated
5370
+ * windows; the account's own reported `totalCost` is a billing PERIOD figure,
5371
+ * not this session's.
5590
5372
  *
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.
5373
+ * Three rules are load-bearing: only Command Code usage is priced (a session
5374
+ * served by another provider must render nothing, never a Command Code
5375
+ * estimate); a missing rate is never invented (the pricing page publishes a
5376
+ * cache-WRITE rate for only some models, so unpriced cache-write tokens are
5377
+ * surfaced as such rather than charged at a guessed multiple of the input rate);
5378
+ * and unpriceable means invisible — a confident `$0.00` would be a lie, and
5379
+ * this module never returns one.
5600
5380
  *
5601
5381
  * @module dsh-commandcode-provider/client/session-cost
5602
5382
  */
@@ -5621,15 +5401,12 @@ window.__ModuleLoader__.load({
5621
5401
  * between its own items.
5622
5402
  */
5623
5403
  separator: "·",
5624
- /** The heading the tooltip leads with. */
5625
5404
  panelTitle: "Session cost",
5626
5405
  /** Marks an estimate from published rates rather than an invoice. */
5627
5406
  approximate: "≈",
5628
- /** Tooltip line for the unpriced cache-write tokens. */
5629
5407
  unpricedCacheWrite: "cache write tokens have no published rate",
5630
5408
  /** Tooltip line naming the rate half in force. */
5631
5409
  peakRates: "peak rates",
5632
- /** Tooltip line naming the rate half in force. */
5633
5410
  offPeakRates: "off-peak rates",
5634
5411
  /** Tooltip line explaining the approximate marker. */
5635
5412
  approximateNote: "estimate from published rates, not the provider invoice",
@@ -5690,8 +5467,8 @@ window.__ModuleLoader__.load({
5690
5467
  *
5691
5468
  * The panel's `money()` convention (2 decimals, 4 below a cent) is right for
5692
5469
  * 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.
5470
+ * `toFixed(4)` would print a flat `$0.0000` — broken rather than small. So a
5471
+ * total under $0.0001 is stated as a bound instead.
5695
5472
  */
5696
5473
  function sessionCostAmount(total) {
5697
5474
  if (!Number.isFinite(total) || total <= 0) return formatMoney(0);
@@ -5899,15 +5676,14 @@ window.__ModuleLoader__.load({
5899
5676
  * and never the outlet's parent, because the parent is the composer footer,
5900
5677
  * which holds the `ContextMeter` as well, and that meter's trigger is itself a
5901
5678
  * `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.
5679
+ * "last trigger wins" would append the cost to the context ring instead.
5904
5680
  */
5905
5681
  const STATS_ROOT = "[data-composer-stats]";
5906
5682
  /**
5907
5683
  * 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}.
5684
+ * render (the per-message turn-usage panel has its own dialog with different
5685
+ * markup), but not across the document once two composers are live — see
5686
+ * {@link resolveDialog}.
5911
5687
  */
5912
5688
  const USAGE_DIALOG = "[data-session-stats-usage]";
5913
5689
  /**
@@ -5917,8 +5693,8 @@ window.__ModuleLoader__.load({
5917
5693
  */
5918
5694
  const DIALOG_TRIGGER = "button[aria-haspopup=\"dialog\"]";
5919
5695
  /**
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
5696
+ * Stable id for the hidden node the appended cost is described by. The pill's own
5697
+ * `aria-label` is recomputed by the harness on every render and cannot be
5922
5698
  * extended, so a description is how the cost reaches assistive technology.
5923
5699
  */
5924
5700
  const A11Y_ID = "dsh-commandcode-session-cost";
@@ -6040,11 +5816,9 @@ window.__ModuleLoader__.load({
6040
5816
  * registration order. {@link STATS_ROOT} narrows that to the stats row where
6041
5817
  * the markup marks it; where it does not, the outlet is already the narrowest
6042
5818
  * 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).
5819
+ * the outlet rather than inside it. A missing scope is "no pill", never a
5820
+ * document-wide search: the outlet is what makes this lookup per-composer, and
5821
+ * two composers can be live at once.
6048
5822
  */
6049
5823
  resolvePillButton() {
6050
5824
  const scope = this.scope();
@@ -6176,20 +5950,18 @@ window.__ModuleLoader__.load({
6176
5950
  *
6177
5951
  * The dialog is portaled onto `body`, so unlike the pill it cannot be scoped
6178
5952
  * 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.
5953
+ * document. While one composer was guaranteed a document-level lookup was
5954
+ * exact; 0.1.6-alpha.2 ended that guarantee by mounting an embedded
5955
+ * Conversation in the sidebar, so a second composer — its own session, dock and
5956
+ * dialog — can be live and open at the same time.
6183
5957
  *
6184
5958
  * 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.
5959
+ * carries no id and no `aria-controls`), so document order says nothing about
5960
+ * ownership. The shape confirmation in {@link dialogShapeMatches} would usually
5961
+ * reject the stranger, but it compares against THIS entry's counts, so a
5962
+ * coincidentally matching dialog would be priced with another session's
5963
+ * figures. Ambiguity is therefore answered by declining to decorate at all:
5964
+ * a dialog we cannot prove is ours is left alone.
6193
5965
  */
6194
5966
  resolveDialog() {
6195
5967
  if (this.dialogHost?.isConnected === true) return this.dialogHost;
@@ -6231,12 +6003,11 @@ window.__ModuleLoader__.load({
6231
6003
  /**
6232
6004
  * Whether the dialog really holds the rows this session's buckets predict.
6233
6005
  *
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.
6006
+ * The shipped labels belong to the `chat` locale and differ per language, so rows
6007
+ * are matched by POSITION, and this is what makes that safe: the row count must
6008
+ * agree, the cache-hit row must be the percentage it is, and every other value
6009
+ * must carry exactly the token count of the bucket predicted for it. A mismatch
6010
+ * means the dialog is not what this view describes, and nothing is decorated.
6240
6011
  */
6241
6012
  function dialogShapeMatches(pairs, plan, prices) {
6242
6013
  if (pairs.length !== plan.length) return false;
@@ -6269,13 +6040,13 @@ window.__ModuleLoader__.load({
6269
6040
  /**
6270
6041
  * The dock outlet this entry renders inside, i.e. what it scopes itself from.
6271
6042
  *
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.
6043
+ * The OUTLET, not its parent. The outlet is one `display:contents` div holding
6044
+ * exactly this slot's entries (the shipped `stats` cell and ours), so it is the
6045
+ * narrowest container that is still per-composer. Its parent is NOT: from
6046
+ * 0.1.6-alpha.2 the composer stacks the dock inside a footer that also holds the
6047
+ * `ContextMeter`, whose trigger is another `button[aria-haspopup="dialog"]`
6048
+ * rendered after the dock — scoping to the parent would let the cost land on the
6049
+ * context ring.
6279
6050
  */
6280
6051
  const DOCK_ANCHOR = "[data-slot=\"conversation.composer.dock\"]";
6281
6052
  /** Module-level constant so the marker's `style` prop never diffs. */
@@ -6339,44 +6110,37 @@ window.__ModuleLoader__.load({
6339
6110
  * Stylesheet for the Command Code settings page — and the Models-page provider
6340
6111
  * card, which shares its `cc-` classes.
6341
6112
  *
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.
6113
+ * Returned as a string, not injected here, so importing this module has no DOM
6114
+ * side effect: `./index.ts` installs it once under {@link PAGE_CSS_ID}. It is a
6115
+ * module of its own rather than a literal in the entry so `tests/styles.test.ts`
6116
+ * can audit every rule without importing the React tree (which pulls
6117
+ * `*.module.css` through the primitives package and needs a loader registered
6118
+ * first). The rules are GLOBAL CSS, so containment is a contract (issue #48):
6119
+ * every selector is qualified by one of our own `cc-` classes. Colours are
6120
+ * harness theme aliases with neutral fallbacks, and the `cc-` prefix keeps
6121
+ * these clear of the panel's `ccp-` set.
6353
6122
  *
6354
6123
  * @module dsh-commandcode-provider/client/page-styles
6355
6124
  */
6356
6125
  /**
6357
- * Stylesheet id (the `data-plugin-css` value that makes injection idempotent).
6126
+ * Stylesheet id — the `data-plugin-css` value that makes injection idempotent.
6358
6127
  *
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.
6128
+ * The prefix must match this package's real name and the panel stylesheet's
6129
+ * (`PANEL_CSS_ID`); the two ids must also stay DISTINCT, since they key two
6130
+ * separate style tags and a shared id would make the second injection a no-op.
6366
6131
  */
6367
6132
  const PAGE_CSS_ID = "@mars-sea/dsh-commandcode-provider/CommandCodeSettingsPage.module.css";
6368
6133
  /**
6369
6134
  * The settings-page stylesheet.
6370
6135
  *
6371
6136
  * The metrics are the harness's own settings pages', read out of the 0.1.7
6372
- * 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
6137
+ * bundles so the page sits beside General and Models without looking foreign: a
6138
+ * row is 16px of padding over a 0.5px border-l2 hairline, its title 14/22
6374
6139
  * label-primary and its description 12/18 label-tertiary, the control on the
6375
6140
  * right. Selectors and buttons are capsules (36px / 28px compact), inputs are
6376
6141
  * 32px with an 8px radius and a 0.5px border-l4 edge, a card is a 0.5px
6377
6142
  * 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).
6143
+ * bg-module-platform fill with a 12px radius. Errors use state-error-primary.
6380
6144
  */
6381
6145
  const PAGE_CSS = `
6382
6146
  .cc-section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;display:flex}
@@ -6384,7 +6148,6 @@ window.__ModuleLoader__.load({
6384
6148
  .cc-intro{margin:4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px}
6385
6149
  .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
6150
  .cc-spacer{flex:1}
6387
- /* Groups: a heading over hairline-separated rows, no card surface. */
6388
6151
  .cc-group{flex-direction:column;display:flex;margin-top:28px}
6389
6152
  .cc-groupHead{align-items:center;gap:8px;display:flex;min-height:28px;padding-bottom:4px}
6390
6153
  .cc-groupTitle{margin:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}
@@ -6395,7 +6158,6 @@ window.__ModuleLoader__.load({
6395
6158
  .cc-rows{flex-direction:column;display:flex}
6396
6159
  .cc-row{align-items:center;gap:8px;display:flex;padding:16px 0;border-bottom:.5px solid var(--dsw-alias-border-l2)}
6397
6160
  .cc-rows>.cc-row:last-child{border-bottom:0}
6398
- .cc-rowNested{padding-left:16px}
6399
6161
  .cc-rowFlush{padding:0;border-bottom:0}
6400
6162
  .cc-rowText{flex-direction:column;flex:1;gap:4px;min-width:0;padding-right:32px;display:flex}
6401
6163
  .cc-rowTitleLine{align-items:center;gap:8px;display:flex;min-width:0}
@@ -6407,19 +6169,15 @@ window.__ModuleLoader__.load({
6407
6169
  .cc-rowControl{flex:none;align-items:center;justify-content:flex-end;gap:8px;display:inline-flex;max-width:60%}
6408
6170
  .cc-rowInput{width:200px}
6409
6171
  .cc-rowInputWide{width:280px}
6410
- /* The platform's link button: a compact capsule with no fill until hovered. */
6411
6172
  .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
6173
  .cc-linkButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
6413
6174
  .cc-linkButton:disabled{cursor:default;opacity:.4}
6414
6175
  .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
6176
  .cc-badges{align-items:center;gap:8px;display:inline-flex}
6417
6177
  .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}
6418
6178
  .cc-badge{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
6419
6179
  .cc-badgeMuted{border:.5px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-tertiary)}
6420
6180
  .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
6181
  .cc-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
6424
6182
  .cc-field+.cc-field{border-top:.5px solid var(--dsw-alias-border-l2)}
6425
6183
  .cc-fieldHead{align-items:center;gap:8px;display:flex}
@@ -6432,10 +6190,7 @@ window.__ModuleLoader__.load({
6432
6190
  .cc-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
6433
6191
  .cc-input:disabled{opacity:.6;cursor:default}
6434
6192
  .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
6193
  .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
6194
  .cc-modelSelectAnchor{flex-direction:column;align-items:stretch;gap:6px;display:inline-flex;min-width:160px}
6440
6195
  .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
6196
  .cc-selector:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
@@ -6449,51 +6204,34 @@ window.__ModuleLoader__.load({
6449
6204
  .cc-modelSearch::-webkit-search-cancel-button{cursor:pointer}
6450
6205
  .cc-checkRow{align-items:center;gap:8px;display:inline-flex;min-width:0}
6451
6206
  .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. */
6207
+ /* A hand-drawn box, not the platform's accent-color checkbox: the native
6208
+ * control's colours come from color-scheme, which the theme sets once at BOOT
6209
+ * and never re-applies in-session, so a native box can paint light-chrome white
6210
+ * on a dark page. The mark rides a brand-primary fill, so it takes the brand
6211
+ * foreground (the only pairing that survives dark mode's near-white brand), and
6212
+ * the outline is a border-l3 hairline because border-l2 leaves an unchecked box
6213
+ * invisible against the menu surface. */
6461
6214
  .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
6215
  .cc-check:checked{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}
6463
6216
  .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
6217
  .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
6218
  /* 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. */
6219
+ * Switch.module.css): a 36x20 capsule padding its track by 2px and sliding a
6220
+ * 16px thumb across the 32px content box. Three things are load-bearing.
6221
+ * (1) corner-shape:round on BOTH track and thumb — the theme sets a global
6222
+ * superellipse corner shape, which squares a capsule's ends off around a round
6223
+ * thumb. (2) No literal colours: brand-primary INVERTS between the modes, so a
6224
+ * hardcoded white thumb vanishes on the checked track in dark mode; the brand
6225
+ * foreground is what the platform paints on that fill. (3) border-l3 as the
6226
+ * off track — border-l2 leaves it indistinguishable from the page. */
6487
6227
  .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
6228
  .cc-toggle:checked{background:var(--dsw-alias-brand-primary)}
6489
6229
  .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
6230
  .cc-toggle:checked::after{transform:translateX(16px)}
6492
6231
  .cc-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
6493
6232
  .cc-toggle:disabled{cursor:default;opacity:.5}
6494
6233
  .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
6234
  .cc-chevronUp{transform:rotate(-135deg);margin-bottom:-3px}
6496
- /* Accounts: one outlined card per account, the Models page's provider card. */
6497
6235
  .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
6236
  .cc-accountList{flex-direction:column;gap:8px;display:flex}
6499
6237
  .cc-accountList+.cc-addButton,.cc-accountList+.cc-addPanel{margin-top:8px}
@@ -6506,7 +6244,7 @@ window.__ModuleLoader__.load({
6506
6244
  .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
6245
  .cc-iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
6508
6246
  .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. */
6247
+ /* Three dots from one element: the dot plus two box-shadow copies. */
6510
6248
  .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
6249
  .cc-tabDot{flex-shrink:0;width:8px;height:8px;border-radius:50%;corner-shape:round}
6512
6250
  .cc-tabDotOk{background:var(--dsw-alias-state-success-primary)}
@@ -6528,20 +6266,16 @@ window.__ModuleLoader__.load({
6528
6266
  .cc-confirmText{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}
6529
6267
  .cc-dangerButton{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
6530
6268
  .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
6269
  .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
6270
  .cc-addButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
6535
6271
  .cc-addButton:disabled{cursor:default;opacity:.4}
6536
6272
  .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
6273
  .cc-addGlyph{position:relative;width:12px;height:12px}
6539
6274
  .cc-addGlyph::before,.cc-addGlyph::after{content:'';position:absolute;background:currentColor;border-radius:1px}
6540
6275
  .cc-addGlyph::before{left:0;right:0;top:5.25px;height:1.5px}
6541
6276
  .cc-addGlyph::after{top:0;bottom:0;left:5.25px;width:1.5px}
6542
6277
  .cc-addPanel{flex-direction:column;gap:12px;display:flex;padding:14px 16px;border-radius:12px;background:var(--dsw-alias-bg-module-platform)}
6543
6278
  .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
6279
  .cc-accountReport{flex-direction:column;gap:14px;display:flex}
6546
6280
  .cc-usageHead{align-items:center;gap:8px;display:flex}
6547
6281
  .cc-usageTitle{color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:14px;font-weight:500;line-height:22px}
@@ -6549,10 +6283,9 @@ window.__ModuleLoader__.load({
6549
6283
  .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
6284
  .cc-usagePlanStatus{flex:none;margin:0;white-space:nowrap;color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}
6551
6285
  .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. */
6286
+ /* Two fixed grids, not one auto-fit track: auto-fit wrapped the four activity
6287
+ * tiles 3 + 1 at the details' width. The activity row folds to 2 × 2 in a
6288
+ * narrow details panel. */
6556
6289
  .cc-usageStats{grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;display:grid}
6557
6290
  .cc-usageStatsActivity{grid-template-columns:repeat(4,minmax(0,1fr))}
6558
6291
  .cc-usageStatsBalance{grid-template-columns:repeat(3,minmax(0,1fr))}
@@ -6580,31 +6313,25 @@ window.__ModuleLoader__.load({
6580
6313
  .cc-usageBlockedHint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
6581
6314
  .cc-usageBlockedDetail{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px;word-break:break-word}
6582
6315
  .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
6316
  .cc-versionLink{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-label-secondary));text-decoration:none}
6585
6317
  .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
6318
  .cc-providerCard{flex-direction:column;display:flex}
6589
6319
  .cc-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}
6590
6320
  .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
6321
  .cc-loginLink{color:var(--dsw-alias-link,var(--dsw-alias-brand-primary));text-decoration:none;font-size:12px;line-height:18px}
6593
6322
  .cc-loginLink:hover{text-decoration:underline;text-underline-position:under}
6594
6323
  .cc-loginDone{color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary))}
6595
6324
  .cc-loginError{color:var(--dsw-alias-state-error-primary)}
6596
6325
  /* 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. */
6326
+ * settings scrollport. The dock is a zero-height sticky strip at the end of the
6327
+ * page, so the bar overlays the scrolling content instead of taking a row of
6328
+ * its own; while shown, the section reserves room at its end so the last line
6329
+ * can scroll clear. Hidden, it sinks and fades, and visibility (switched after
6330
+ * the transition) takes its buttons out of the tab order. Its surface is an
6331
+ * elevated layer (bg-layer-1 over the settings panel's layer-2) with the
6332
+ * platform's prominent elevation and no stroke, and its corners are concentric
6333
+ * (a 44px capsule around 36px buttons inset by 4px) so the gap around each
6334
+ * button is even. */
6608
6335
  .cc-sectionWithBar{padding-bottom:80px}
6609
6336
  .cc-saveBarDock{position:sticky;bottom:0;z-index:2;height:0;pointer-events:none}
6610
6337
  .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 +6350,7 @@ window.__ModuleLoader__.load({
6623
6350
  .cc-saveBarGhost:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
6624
6351
  .cc-saveBarPrimary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-foreground,#fff)}
6625
6352
  .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}}
6353
+ @media (prefers-reduced-motion:reduce){.cc-chevron,.cc-toggle::after,.cc-usageBarFill,.cc-saveBar,.cc-saveBarShown{transition:none}}
6627
6354
  `;
6628
6355
  //#endregion
6629
6356
  //#region src/client/panel-styles.ts
@@ -6631,70 +6358,48 @@ window.__ModuleLoader__.load({
6631
6358
  * Stylesheet for the Command Code plans & quota panel (the sidebar footer card
6632
6359
  * and the dashboard it opens).
6633
6360
  *
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.
6361
+ * Returned as a string, not injected here, so importing this module has no DOM
6362
+ * side effect: `./index.ts` installs it once under {@link PANEL_CSS_ID} and
6363
+ * removes it when the fiber unwinds.
6639
6364
  *
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.
6646
- *
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.
6365
+ * These rules are GLOBAL CSS, so containment is a contract (issue #48): every
6366
+ * selector is qualified by one of our own `ccp-` classes except the sidebar
6367
+ * anchor below, which carries its justification in place. `tests/styles.test.ts`
6368
+ * audits both stylesheets against the real foreign class stems. Colours are
6369
+ * harness theme aliases with neutral fallbacks; the `ccp-` prefix keeps these
6370
+ * clear of the settings page's `cc-` set.
6651
6371
  *
6652
6372
  * @module dsh-commandcode-provider/client/panel-styles
6653
6373
  */
6654
6374
  /**
6655
- * Stylesheet id (the `data-plugin-css` value that makes injection idempotent).
6375
+ * Stylesheet id — the `data-plugin-css` value that makes injection idempotent.
6656
6376
  *
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.
6377
+ * The prefix must match this package's real name and the settings-page sheet's
6378
+ * (`PAGE_CSS_ID`); the two ids must also stay DISTINCT, since they key two
6379
+ * separate style tags and a shared id would make the second injection a no-op.
6664
6380
  */
6665
6381
  const PANEL_CSS_ID = "@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css";
6666
6382
  /** The panel stylesheet. */
6667
6383
  const PANEL_CSS = `
6668
6384
  /* ------------------------------------------------- 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.
6385
+ /* The shell renders this list ABOVE the Settings seat, so the card is the
6386
+ sidebar's bottom-most content, and supplies no chrome: the entry IS the
6387
+ button. Deliberately quiet — a surface beside Settings should read as part of
6388
+ the column — with one hover step and a hairline border.
6683
6389
 
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
6390
+ The shell's container is a flex ROW whose occupants each declare a full-width
6391
+ line and shrink-proof flex, so as a row it would overflow the column: this
6392
+ card cannot shrink and the neighbouring chip would absorb the overflow and
6393
+ collapse to nothing. The first rule is that fix, and THE ANCHOR IS
6394
+ LOAD-BEARING (issue #48): "footerActions" is not a stem this shell owns alone
6395
+ — dsh-client-ui-user-questions renders the ask-user-question dialog's button
6396
+ row as Mbwy4a_footerActions, and the unanchored rule stacked THAT row's
6689
6397
  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. */
6398
+ dsh-client-ui-sidebar alone (audited across every client bundle of the
6399
+ 0.1.6-alpha.1 engine, and against our own cc-/ccp- names, which collide with
6400
+ no engine class). The DESCENDANT combinator survives a wrapper element
6401
+ appearing between the two, so a restructured shell degrades to its own row
6402
+ rather than losing the fix. tests/styles.test.ts pins both halves. */
6698
6403
  [class*="_footArea"] [class*="_footerActions"]{flex-direction:column}
6699
6404
  .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
6405
  .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 +6409,31 @@ window.__ModuleLoader__.load({
6704
6409
  /* One block per quota window: a head line carrying the window's own spend and
6705
6410
  limit, then the FULL-WIDTH bar under it. Stacking the two lets the card show
6706
6411
  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. */
6412
+ bar into what is left beside them. Mirrors the dashboard's window block. */
6708
6413
  .ccp-footRow{flex-direction:column;gap:4px;min-width:0;display:flex}
6709
6414
  .ccp-footHead{align-items:baseline;gap:8px;min-width:0;display:flex}
6710
6415
  .ccp-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
6711
6416
  .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
6417
  /* 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. */
6418
+ own button — so these bars are spans, which makes display:block load-bearing:
6419
+ an inline box ignores width outright, so the fill would collapse to 0x0 and
6420
+ the bar would show no usage. */
6717
6421
  .ccp-footBar{display:block;background:var(--dsw-alias-bg-layer-2);border-radius:999px;height:5px;overflow:hidden}
6718
6422
  .ccp-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
6719
6423
  .ccp-footFillWarn{background:var(--dsw-alias-state-error-primary)}
6720
6424
  .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
6425
 
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. */
6426
+ /* The 56px rail: one icon button on the shell's own 36px rail cell, so the
6427
+ collapsed column keeps a single 18px glyph like its siblings. */
6724
6428
  .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
6429
  .ccp-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
6726
6430
  .ccp-railButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
6727
6431
 
6728
- /* The ring glyph. Sized entirely by its own width/height attribute, so the
6432
+ /* The ring glyph, sized entirely by its own width/height attribute, so the
6729
6433
  footer row, the rail button and the dashboard can each ask for their own. */
6730
6434
  .ccp-glyph{flex:none;justify-content:center;align-items:center;display:inline-flex;color:var(--dsw-alias-brand-primary)}
6731
6435
 
6732
6436
  /* ------------------------------------------------------------ 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
6437
  .ccp-main{background:var(--dsw-alias-bg-layer-1);width:100%;height:100%;overflow:auto;display:block}
6736
6438
  .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
6439
  .ccp-header{align-items:center;gap:10px;display:flex;flex-wrap:wrap}
@@ -6740,14 +6442,12 @@ window.__ModuleLoader__.load({
6740
6442
  .ccp-subtitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
6741
6443
  .ccp-spacer{flex:1}
6742
6444
  .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. */
6445
+ /* The dashboard's exit: the primitive's own size="sm" geometry is kept; only the
6446
+ glyph is enlarged and optically centred, so it reads as a dismiss control. */
6746
6447
  .ccp-close{min-width:28px;justify-content:center;padding-left:0;padding-right:0}
6747
6448
  .ccp-close span{font-size:16px;line-height:1}
6748
6449
  .ccp-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
6749
6450
 
6750
- /* Notices: the no-key guidance, a blocked report, and a stale-data error. */
6751
6451
  .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
6452
  .ccp-noticeError{border-color:var(--dsw-alias-state-error-primary)}
6753
6453
  .ccp-noticeTitle{margin:0;font-size:13px;font-weight:600;line-height:1.5}
@@ -6767,15 +6467,14 @@ window.__ModuleLoader__.load({
6767
6467
  .ccp-fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
6768
6468
  .ccp-planName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}
6769
6469
 
6770
- /* Stat tiles: the monthly credits and the usage totals share one grid. */
6470
+ /* The monthly credits and the usage totals share one grid. */
6771
6471
  .ccp-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
6772
6472
  .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
6473
  .ccp-tileLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}
6774
6474
  .ccp-tileValue{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;font-variant-numeric:tabular-nums}
6775
6475
  .ccp-tileSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
6776
6476
 
6777
- /* Quota bars: the monthly bar and the two windows stack in one column, each a
6778
- label row plus the track. */
6477
+ /* Quota bars: the monthly bar and the two windows. */
6779
6478
  .ccp-windows{flex-direction:column;gap:14px;display:flex}
6780
6479
  .ccp-window{flex-direction:column;gap:6px;display:flex}
6781
6480
  .ccp-windowHead{align-items:baseline;gap:8px;display:flex}
@@ -6788,7 +6487,6 @@ window.__ModuleLoader__.load({
6788
6487
  .ccp-barFillWarn{background:var(--dsw-alias-state-error-primary)}
6789
6488
  .ccp-windowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
6790
6489
 
6791
- /* Badges. */
6792
6490
  .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
6491
  .ccp-badgeError{background:transparent;color:var(--dsw-alias-state-error-primary)}
6794
6492
  .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))}
@@ -6832,13 +6530,6 @@ window.__ModuleLoader__.load({
6832
6530
  webSearchHint: "使用同一密钥承载 dsh 的 web_search;关闭后恢复原搜索后端。",
6833
6531
  showSidebarQuota: "在侧边栏显示额度卡片",
6834
6532
  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
6533
  zdr: "零数据保留(ZDR)",
6843
6534
  zdrHint: "仅使用不留存、不训练的上游;可用模型可能更少且价格通常更高。",
6844
6535
  accountsTitle: "账户",
@@ -6991,13 +6682,6 @@ window.__ModuleLoader__.load({
6991
6682
  webSearchHint: "Use the same key for dsh web search; turning it off restores the previous backend.",
6992
6683
  showSidebarQuota: "Show the quota card in the sidebar",
6993
6684
  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
6685
  zdr: "Zero data retention (ZDR)",
7002
6686
  zdrHint: "Use only upstreams that retain no data and do not train on requests; availability and pricing may differ.",
7003
6687
  accountsTitle: "Accounts",
@@ -7136,8 +6820,8 @@ window.__ModuleLoader__.load({
7136
6820
  }
7137
6821
  /**
7138
6822
  * 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.
6823
+ * `ctx.effect` to own. Keyed by its own `data-plugin-css` id, so a second
6824
+ * surface asking for it later is a no-op.
7141
6825
  */
7142
6826
  function injectPanelCss() {
7143
6827
  if (typeof document === "undefined") return () => {};
@@ -7155,22 +6839,24 @@ window.__ModuleLoader__.load({
7155
6839
  * Plans & quota panel id. It is the layout's `MainPanelId`: one string shared
7156
6840
  * by the `sidebar.footer.action` card and the `main` slot cell, so the card
7157
6841
  * 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.
6842
+ * dependency of this bundle, so its brand is applied at the call site instead
6843
+ * of importing the package.
7160
6844
  */
7161
6845
  const PANEL_ID = "commandcode-panel";
7162
6846
  /**
7163
6847
  * 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 /
6848
+ * never the shipped `stats` cell's: reusing that id would REPLACE the tokens /
7165
6849
  * cache-hit / throughput readout rather than inject into it, and that readout
7166
- * is the harness's to format (see `./session-cost-display.ts`).
6850
+ * is the harness's to format.
7167
6851
  */
7168
6852
  const SESSION_COST_ID = "commandcode-session-cost";
7169
6853
  /**
7170
6854
  * Client plugin body. Gates on `slots`, `locale` and `remote` — never on
7171
6855
  * `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.
6856
+ * (the scope below speaks the `remote.settings` wire directly), because
6857
+ * gating on a service 0.1.7 does not provide would park EVERY surface below
6858
+ * into silence. `settingsScope` must never reappear in the exported inject
6859
+ * list at the bottom of this file.
7174
6860
  */
7175
6861
  function apply(ctx) {
7176
6862
  injectPageCss();
@@ -7212,9 +6898,19 @@ window.__ModuleLoader__.load({
7212
6898
  ctx.effect(() => () => controller.dispose(), "dsh-commandcode-provider: settings controller");
7213
6899
  const store = createSnapshotStore(controller.state());
7214
6900
  controller.subscribe(() => store.set(controller.state()));
7215
- ctx.effect(() => ctx.remote.$on("credentials/reference-updated", () => {
7216
- controller.refreshCredentials();
7217
- }), "dsh-commandcode-provider: credential invalidations");
6901
+ ctx.effect(() => {
6902
+ const remote = ctx.remote;
6903
+ const unregisterReference = remote.$on("credentials/reference-updated", () => {
6904
+ controller.refreshCredentials();
6905
+ });
6906
+ const unregisterRecord = remote.$on("credentials/record-updated", () => {
6907
+ controller.refreshCredentials();
6908
+ });
6909
+ return () => {
6910
+ unregisterReference();
6911
+ unregisterRecord();
6912
+ };
6913
+ }, "dsh-commandcode-provider: credential invalidations");
7218
6914
  let usageNamespace;
7219
6915
  let usageMountError;
7220
6916
  let pricesController;