@mars-sea/dsh-commandcode-provider 0.10.6 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1225,7 +1225,7 @@ window.__ModuleLoader__.load({
1225
1225
  }
1226
1226
  //#endregion
1227
1227
  //#region src/client/usage.ts
1228
- const IDLE = {
1228
+ const IDLE$1 = {
1229
1229
  status: "idle",
1230
1230
  report: void 0,
1231
1231
  error: void 0,
@@ -1239,7 +1239,7 @@ window.__ModuleLoader__.load({
1239
1239
  var CommandCodeUsageController = class {
1240
1240
  remote;
1241
1241
  listeners = /* @__PURE__ */ new Set();
1242
- current = IDLE;
1242
+ current = IDLE$1;
1243
1243
  generation = 0;
1244
1244
  inFlight = false;
1245
1245
  disposed = false;
@@ -1341,6 +1341,166 @@ window.__ModuleLoader__.load({
1341
1341
  if (ms <= 0) return "";
1342
1342
  return new Date(ms).toLocaleString();
1343
1343
  }
1344
+ /** Host facts drive the usage card, including keys invisible to browser credentials. */
1345
+ function usageCardState(usage) {
1346
+ const accounts = usage.report?.accounts ?? [];
1347
+ return {
1348
+ loading: usage.status === "loading",
1349
+ shouldRefresh: usage.status === "idle",
1350
+ noKey: accounts.length > 0 && !accounts.some((account) => account.configured)
1351
+ };
1352
+ }
1353
+ //#endregion
1354
+ //#region src/client/prices.ts
1355
+ const IDLE = {
1356
+ status: "idle",
1357
+ table: void 0,
1358
+ error: void 0
1359
+ };
1360
+ const RETRY_TIMER = {
1361
+ set(callback, ms) {
1362
+ const handle = setTimeout(callback, ms);
1363
+ handle.unref?.();
1364
+ return handle;
1365
+ },
1366
+ clear(handle) {
1367
+ clearTimeout(handle);
1368
+ }
1369
+ };
1370
+ /**
1371
+ * Cache over the `commandcode/prices` Remote, with a bounded transient-retry
1372
+ * budget. Public API mirrors {@link CommandCodeUsageController}: `state()`,
1373
+ * `subscribe`, and `ensure()`; `reload()` additionally drops the cache and
1374
+ * restarts the budget when the Host namespace rebinds.
1375
+ *
1376
+ * Not a one-shot: the last fetch is cached until `reload()`, a transient
1377
+ * failure is retried three times at 1/2/4 s (the delays are bounded, not a
1378
+ * poll), and a MANUAL `ensure()` still fetches after that budget is spent. A
1379
+ * PERMANENT failure — the Host serving no such endpoint — is terminal for the
1380
+ * binding and is only cleared by `reload()`.
1381
+ */
1382
+ var CommandCodePricesController = class {
1383
+ remote;
1384
+ listeners = /* @__PURE__ */ new Set();
1385
+ current = IDLE;
1386
+ generation = 0;
1387
+ inFlight = false;
1388
+ disposed = false;
1389
+ retryHandle;
1390
+ attempts = 0;
1391
+ permanent = false;
1392
+ timer;
1393
+ constructor(remote, timer = RETRY_TIMER) {
1394
+ this.timer = timer;
1395
+ this.remote = remote;
1396
+ }
1397
+ /** Release every subscription. Idempotent; in-flight results are dropped. */
1398
+ dispose() {
1399
+ this.disposed = true;
1400
+ this.clearRetry();
1401
+ this.listeners.clear();
1402
+ }
1403
+ /** Subscribe to state projections. @returns the disposer. */
1404
+ subscribe(listener) {
1405
+ this.listeners.add(listener);
1406
+ return () => this.listeners.delete(listener);
1407
+ }
1408
+ /** The current state face. */
1409
+ state() {
1410
+ return this.current;
1411
+ }
1412
+ /**
1413
+ * Fetch the table unless it is already loaded, permanent, or in flight — and
1414
+ * unless a bounded retry already owns the next attempt.
1415
+ *
1416
+ * The only callers are the client entry: `reload()` when the Remote namespace
1417
+ * lands or rebinds, and a manual refresh. Nothing in the composer calls it, so
1418
+ * a mounted readout does not trigger a fetch of its own — it renders whatever
1419
+ * the table's state currently is.
1420
+ */
1421
+ ensure() {
1422
+ if (this.disposed || this.permanent || this.inFlight || this.current.status === "ready") return;
1423
+ this.clearRetry();
1424
+ const call = this.remote.prices;
1425
+ if (typeof call !== "function") {
1426
+ this.permanent = true;
1427
+ this.publish({
1428
+ status: "error",
1429
+ table: void 0,
1430
+ error: "the Host serves no commandcode/prices endpoint"
1431
+ });
1432
+ return;
1433
+ }
1434
+ const generation = this.generation;
1435
+ this.inFlight = true;
1436
+ this.attempts += 1;
1437
+ this.publish({
1438
+ status: "loading",
1439
+ table: this.current.table,
1440
+ error: void 0
1441
+ });
1442
+ let request;
1443
+ try {
1444
+ request = call.call(this.remote);
1445
+ } catch (error) {
1446
+ request = Promise.reject(error);
1447
+ }
1448
+ request.then((result) => {
1449
+ if (this.disposed || generation !== this.generation) return;
1450
+ this.inFlight = false;
1451
+ if (result.ok) {
1452
+ this.publish({
1453
+ status: "ready",
1454
+ table: result.value,
1455
+ error: void 0
1456
+ });
1457
+ return;
1458
+ }
1459
+ this.permanent = result.error.permanent === true;
1460
+ this.publish({
1461
+ status: "error",
1462
+ table: void 0,
1463
+ error: result.error.message
1464
+ });
1465
+ this.scheduleRetry();
1466
+ }, (error) => {
1467
+ if (this.disposed || generation !== this.generation) return;
1468
+ this.inFlight = false;
1469
+ this.publish({
1470
+ status: "error",
1471
+ table: void 0,
1472
+ error: error instanceof Error ? error.message : String(error)
1473
+ });
1474
+ this.scheduleRetry();
1475
+ });
1476
+ }
1477
+ /** Rebinding a Host invalidates cached prices and restarts the bounded retry budget. */
1478
+ reload() {
1479
+ if (this.disposed) return;
1480
+ this.generation += 1;
1481
+ this.inFlight = false;
1482
+ this.clearRetry();
1483
+ this.permanent = false;
1484
+ this.attempts = 0;
1485
+ this.current = IDLE;
1486
+ this.ensure();
1487
+ }
1488
+ clearRetry() {
1489
+ if (this.retryHandle !== void 0) this.timer.clear(this.retryHandle);
1490
+ this.retryHandle = void 0;
1491
+ }
1492
+ scheduleRetry() {
1493
+ if (this.disposed || this.permanent || this.attempts >= 4) return;
1494
+ this.retryHandle = this.timer.set(() => {
1495
+ this.retryHandle = void 0;
1496
+ this.ensure();
1497
+ }, 1e3 * 2 ** (this.attempts - 1));
1498
+ }
1499
+ publish(next) {
1500
+ this.current = next;
1501
+ for (const listener of this.listeners) listener();
1502
+ }
1503
+ };
1344
1504
  //#endregion
1345
1505
  //#region src/client/login.ts
1346
1506
  /** How often a live attempt is polled. */
@@ -1680,7 +1840,16 @@ window.__ModuleLoader__.load({
1680
1840
  */
1681
1841
  const { reject: reject$1, record: record$1, stringField: stringField$1, numberField, booleanField } = makeBoundaryValidator("commandcode/report result:");
1682
1842
  /** Validate one window-limit block (`fiveHour` / `weekly`). */
1843
+ /**
1844
+ * Parse one quota window, or undefined when the frame carries no such window.
1845
+ *
1846
+ * The distinction is load-bearing at this boundary: an ABSENT window means the
1847
+ * billing endpoint reported none (an unlimited plan), while a present block with
1848
+ * `cap: 0` means uncapped spend that was really reported. Collapsing the two
1849
+ * would draw a zeroed quota row for an account that has no such limit.
1850
+ */
1683
1851
  function windowLimit(value, field) {
1852
+ if (value === void 0) return void 0;
1684
1853
  const source = record$1(value, field);
1685
1854
  return {
1686
1855
  used: numberField(source, "used", `${field}.used`),
@@ -1728,13 +1897,18 @@ window.__ModuleLoader__.load({
1728
1897
  }
1729
1898
  if (source.credits !== void 0) {
1730
1899
  const credits = record$1(source.credits, "credits");
1731
- report.credits = {
1900
+ const parsed = {
1732
1901
  monthlyCredits: numberField(credits, "monthlyCredits", "credits.monthlyCredits"),
1733
1902
  purchasedCredits: numberField(credits, "purchasedCredits", "credits.purchasedCredits"),
1734
- freeCredits: numberField(credits, "freeCredits", "credits.freeCredits"),
1735
- fiveHour: windowLimit(credits.fiveHour, "credits.fiveHour"),
1736
- weekly: windowLimit(credits.weekly, "credits.weekly")
1903
+ freeCredits: numberField(credits, "freeCredits", "credits.freeCredits")
1737
1904
  };
1905
+ if (credits.monthlyReported !== void 0) parsed.monthlyReported = booleanField(credits, "monthlyReported", "credits.monthlyReported");
1906
+ for (const flag of ["purchasedReported", "freeReported"]) if (credits[flag] !== void 0) parsed[flag] = booleanField(credits, flag, `credits.${flag}`);
1907
+ const fiveHour = windowLimit(credits.fiveHour, "credits.fiveHour");
1908
+ const weekly = windowLimit(credits.weekly, "credits.weekly");
1909
+ if (fiveHour !== void 0) parsed.fiveHour = fiveHour;
1910
+ if (weekly !== void 0) parsed.weekly = weekly;
1911
+ report.credits = parsed;
1738
1912
  }
1739
1913
  if (source.plan !== void 0) {
1740
1914
  const plan = record$1(source.plan, "plan");
@@ -1803,6 +1977,72 @@ window.__ModuleLoader__.load({
1803
1977
  package: USAGE_REMOTE_PACKAGE,
1804
1978
  descriptors: [makeRemoteDescriptor(MODELS_ENDPOINT, "models", `${USAGE_REMOTE_PACKAGE}#CommandCodeCatalog`, { parse: parseCatalog })]
1805
1979
  };
1980
+ /** Canonical `<namespace>/<method>` endpoint of the price-table Remote. */
1981
+ const PRICES_ENDPOINT = "commandcode/prices";
1982
+ /**
1983
+ * The shared read/validate helpers for the price-table endpoint — its own
1984
+ * instance so price boundary errors name `commandcode/prices`.
1985
+ */
1986
+ const { reject: priceReject, record: priceRecord, stringField: priceString, numberField: priceNumber, booleanField: priceBoolean } = makeBoundaryValidator("commandcode/prices result:");
1987
+ /** Parse one rate block (`rates`, or a model's `peak` override). */
1988
+ function parseRates(source, field) {
1989
+ const rates = {
1990
+ inputCost: priceNumber(source, "inputCost", `${field}.inputCost`),
1991
+ outputCost: priceNumber(source, "outputCost", `${field}.outputCost`),
1992
+ cacheReadCost: priceNumber(source, "cacheReadCost", `${field}.cacheReadCost`)
1993
+ };
1994
+ if (source.cacheWriteCost !== void 0) rates.cacheWriteCost = priceNumber(source, "cacheWriteCost", `${field}.cacheWriteCost`);
1995
+ return rates;
1996
+ }
1997
+ /** Parse one untrusted boundary value into a {@link CommandCodeModelPrice}. */
1998
+ function parseModelPrice(value) {
1999
+ const source = priceRecord(value, "model");
2000
+ const price = {
2001
+ id: priceString(source, "id", "model.id"),
2002
+ slug: priceString(source, "slug", "model.slug"),
2003
+ ...parseRates(source, "model")
2004
+ };
2005
+ if (source.peak !== void 0) price.peak = parseRates(priceRecord(source.peak, "model.peak"), "model.peak");
2006
+ if (source.contextTiers !== void 0) {
2007
+ if (!Array.isArray(source.contextTiers) || source.contextTiers.length === 0) priceReject("contextTiers");
2008
+ let previous = 0;
2009
+ price.contextTiers = source.contextTiers.map((value, index, tiers) => {
2010
+ const tier = priceRecord(value, "contextTier");
2011
+ const out = parseRates(tier, "contextTier");
2012
+ if (tier.maxContext !== void 0) {
2013
+ const max = priceNumber(tier, "maxContext", "contextTier.maxContext");
2014
+ if (!Number.isSafeInteger(max) || max <= previous || index === tiers.length - 1) priceReject("contextTier.maxContext");
2015
+ previous = max;
2016
+ out.maxContext = max;
2017
+ } else if (index !== tiers.length - 1) priceReject("contextTier.maxContext");
2018
+ return out;
2019
+ });
2020
+ }
2021
+ if (source.free !== void 0) price.free = priceBoolean(source, "free", "model.free");
2022
+ return price;
2023
+ }
2024
+ /** Parse the wire result into a {@link CommandCodePriceTable}. */
2025
+ function parsePriceTable(value) {
2026
+ const source = priceRecord(value, "result");
2027
+ const models = source.models;
2028
+ if (!Array.isArray(models)) priceReject("models");
2029
+ const peakHours = source.peakHours;
2030
+ if (!Array.isArray(peakHours)) priceReject("peakHours");
2031
+ return {
2032
+ models: models.map(parseModelPrice),
2033
+ peakHours: peakHours.map((window) => {
2034
+ if (!Array.isArray(window) || window.length !== 2) priceReject("peakHours[]");
2035
+ const [start, end] = window;
2036
+ if (typeof start !== "number" || typeof end !== "number") priceReject("peakHours[]");
2037
+ return [start, end];
2038
+ })
2039
+ };
2040
+ }
2041
+ /** The Client-face contribution for the price-table endpoint. */
2042
+ const PRICES_REMOTE_CONTRIBUTION = {
2043
+ package: USAGE_REMOTE_PACKAGE,
2044
+ descriptors: [makeRemoteDescriptor(PRICES_ENDPOINT, "prices", `${USAGE_REMOTE_PACKAGE}#CommandCodePriceTable`, { parse: parsePriceTable })]
2045
+ };
1806
2046
  //#endregion
1807
2047
  //#region src/login-wire.ts
1808
2048
  /** The canonical endpoint paths of the three login Remotes. */
@@ -2051,7 +2291,7 @@ window.__ModuleLoader__.load({
2051
2291
  }
2052
2292
  //#endregion
2053
2293
  //#region package.json
2054
- var version = "0.10.6";
2294
+ var version = "0.11.0";
2055
2295
  var repository = {
2056
2296
  "type": "git",
2057
2297
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
@@ -2554,7 +2794,8 @@ window.__ModuleLoader__.load({
2554
2794
  });
2555
2795
  }
2556
2796
  /** One window-limit row: label, used/cap, a fill bar, and the reset time. */
2557
- function UsageWindow({ label, limit: { used, cap, exceeded, resetAt }, t }) {
2797
+ function UsageWindow({ label, limit, t }) {
2798
+ const { used, cap, exceeded, resetAt } = limit;
2558
2799
  const ratio = windowRatio(used, cap);
2559
2800
  const reset = formatResetAt(resetAt);
2560
2801
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2717,29 +2958,29 @@ window.__ModuleLoader__.load({
2717
2958
  children: [
2718
2959
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
2719
2960
  label: t("usageMonthly"),
2720
- value: formatMoney(credits.monthlyCredits)
2961
+ value: credits.monthlyReported === false ? "—" : formatMoney(credits.monthlyCredits)
2721
2962
  }),
2722
2963
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
2723
2964
  label: t("usagePurchased"),
2724
- value: formatMoney(credits.purchasedCredits)
2965
+ value: credits.purchasedReported === false ? "—" : formatMoney(credits.purchasedCredits)
2725
2966
  }),
2726
2967
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageStat, {
2727
2968
  label: t("usageFree"),
2728
- value: formatMoney(credits.freeCredits)
2969
+ value: credits.freeReported === false ? "—" : formatMoney(credits.freeCredits)
2729
2970
  })
2730
2971
  ]
2731
2972
  }) : null,
2732
- credits !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2973
+ credits?.fiveHour !== void 0 || credits?.weekly !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2733
2974
  className: "cc-usageWindows",
2734
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
2975
+ children: [credits.fiveHour !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
2735
2976
  label: t("usageFiveHour"),
2736
2977
  limit: credits.fiveHour,
2737
2978
  t
2738
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
2979
+ }) : null, credits.weekly !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageWindow, {
2739
2980
  label: t("usageWeekly"),
2740
2981
  limit: credits.weekly,
2741
2982
  t
2742
- })]
2983
+ }) : null]
2743
2984
  }) : null,
2744
2985
  showPeriod || showPartial || fetchedAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2745
2986
  className: "cc-usageMeta",
@@ -2800,15 +3041,11 @@ window.__ModuleLoader__.load({
2800
3041
  * management card are hidden here immediately. Data arrives through the
2801
3042
  * `commandcode/report` Remote; the API keys never leave the Host.
2802
3043
  */
2803
- function UsageCard({ t, usage, apiKeyConfigured, removingIds, removableIds, canManage, onRefresh, onRemoveAccount }) {
3044
+ function UsageCard({ t, usage, removingIds, removableIds, canManage, onRefresh, onRemoveAccount }) {
3045
+ const { loading, shouldRefresh, noKey } = usageCardState(usage);
2804
3046
  (0, react.useEffect)(() => {
2805
- if (apiKeyConfigured && usage.status === "idle") onRefresh();
2806
- }, [
2807
- apiKeyConfigured,
2808
- usage.status,
2809
- onRefresh
2810
- ]);
2811
- const loading = usage.status === "loading";
3047
+ if (shouldRefresh) onRefresh();
3048
+ }, [shouldRefresh, onRefresh]);
2812
3049
  const report = usage.report;
2813
3050
  const [locallyRemoved, setLocallyRemoved] = (0, react.useState)([]);
2814
3051
  (0, react.useEffect)(() => {
@@ -2844,17 +3081,17 @@ window.__ModuleLoader__.load({
2844
3081
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2845
3082
  type: "button",
2846
3083
  className: "cc-usageRefresh",
2847
- disabled: loading || !apiKeyConfigured,
3084
+ disabled: loading,
2848
3085
  onClick: onRefresh,
2849
3086
  children: loading ? t("usageRefreshing") : t("usageRefresh")
2850
3087
  })
2851
3088
  ]
2852
3089
  }),
2853
- !apiKeyConfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3090
+ noKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2854
3091
  className: "cc-usageHint",
2855
3092
  children: t("usageNoKey")
2856
3093
  }) : null,
2857
- apiKeyConfigured && report === void 0 && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3094
+ report === void 0 && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2858
3095
  className: "cc-usageHint",
2859
3096
  children: t("usageLoading")
2860
3097
  }) : null,
@@ -3399,7 +3636,6 @@ window.__ModuleLoader__.load({
3399
3636
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageCard, {
3400
3637
  t,
3401
3638
  usage,
3402
- apiKeyConfigured: state.anyAccountConfigured,
3403
3639
  removingIds: state.accountsRemoving,
3404
3640
  removableIds: state.accounts.map((account) => account.id),
3405
3641
  canManage: state.writable,
@@ -3535,7 +3771,7 @@ window.__ModuleLoader__.load({
3535
3771
  return null;
3536
3772
  }
3537
3773
  /** The closed-panel style: the outlet stays mounted as the detection anchor. */
3538
- const HIDDEN_STYLE = { display: "none" };
3774
+ const HIDDEN_STYLE$1 = { display: "none" };
3539
3775
  /**
3540
3776
  * Decide the card's posture from one settings snapshot. Pure: the component
3541
3777
  * subscribes once and passes the snapshot in, so hook order never depends
@@ -3655,7 +3891,7 @@ window.__ModuleLoader__.load({
3655
3891
  ref: rootRef,
3656
3892
  className: "cc-providerCard",
3657
3893
  "data-cc-models-card": "true",
3658
- style: editorOpen ? void 0 : HIDDEN_STYLE,
3894
+ style: editorOpen ? void 0 : HIDDEN_STYLE$1,
3659
3895
  children: [
3660
3896
  editorOpen && mode.kind === "registration" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3661
3897
  className: "cc-hint",
@@ -3728,172 +3964,1976 @@ window.__ModuleLoader__.load({
3728
3964
  });
3729
3965
  }
3730
3966
  //#endregion
3731
- //#region src/client/locales.ts
3732
- const zh = {
3967
+ //#region src/client/panel-copy.ts
3968
+ /** The panel's literal string table. */
3969
+ const PANEL_COPY = {
3733
3970
  nav: "Command Code",
3734
- title: "Command Code",
3735
- intro: "配置 Command Code Provider 连接。API 密钥仅保存在本机凭据服务中,不会回显;其他字段写入用户设置,下次请求即生效。",
3736
- apiKey: "API 密钥",
3737
- apiKeyHint: "在 commandcode.ai 控制台创建。留空保存不会覆盖已存储的密钥。",
3738
- apiKeySet: "已配置",
3739
- apiKeyUnset: "未配置",
3740
- apiKeyLocked: "密钥由只读来源提供",
3741
- apiBase: "API 地址",
3742
- apiBaseHint: "默认 https://api.commandcode.ai,一般无需修改。",
3743
- workingDir: "工作目录",
3744
- workingDirHint: "可选。留空时使用占位符显示的进程工作目录;仅在需要固定路径时填写。",
3745
- requestTimeoutMs: "请求超时(毫秒)",
3746
- requestTimeoutMsHint: "等待响应首个字节的超时;默认 60000。",
3747
- streamIdleTimeoutMs: "流空闲超时(毫秒)",
3748
- streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300000(长思考模型可静默数分钟,默认值刻意放宽)。",
3749
- advancedSettings: "高级设置",
3750
- advancedSettingsHint: "API 地址、工作目录、超时与模型过滤等不常修改的选项。",
3751
- advancedOverriddenOne: "已自定义 1 项",
3752
- advancedOverriddenMany: "已自定义 {count} 项",
3753
- advancedInvalid: "高级设置中有未填好的数字,请展开修正后再保存。",
3754
- filterModelsByPlan: "隐藏套餐外模型",
3755
- filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
3756
- webSearch: "用 Command Code 承载联网搜索",
3757
- webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址),并优先于其他搜索后端;关闭则把选择权交还给之前的后端(如 modsearch),而不是强制回退到 DeepSeek 搜索。",
3758
- accountsTitle: "多账户轮换",
3759
- accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
3760
- accountAdd: "添加账户",
3761
- accountRemove: "移除",
3762
- accountLabel: "账户备注名",
3763
- accountKey: "API 密钥",
3764
- accountKeyHint: "该账户的 API 密钥。留空保存不会覆盖已存储的密钥。",
3765
- accountDefault: "默认账户",
3766
- activeAccount: "当前使用账户",
3767
- activeAccountAuto: "自动(第一个可用账户)",
3768
- activeAccountHint: "手动指定优先使用的账户,保存后下次请求即生效;所选账户耗尽时仍会自动切换到其他可用账户。",
3769
- rulesTitle: "按模型切换账户",
3770
- rulesHint: "选择模型并路由到某个账户(可多选)。命中规则的模型且该账户可用时优先使用;账户耗尽或密钥失效时仍自动回落到其他账户。规则按列表顺序匹配,第一条命中生效。",
3771
- rulesEmpty: "尚未配置规则。",
3772
- rulesCatalogFailed: "模型目录获取失败,暂时无法选择模型;已保存的规则仍会生效。",
3773
- ruleAdd: "添加规则",
3774
- ruleRemove: "移除",
3775
- ruleModel: "模型",
3776
- ruleModelPick: "选择模型…",
3777
- ruleModelCount: "已选 {count} 个模型",
3778
- ruleAccount: "目标账户",
3779
- ruleHint: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
3780
- modelSearchPlaceholder: "搜索模型…",
3781
- modelSearchEmpty: "没有匹配的模型。",
3782
- modelStale: "已下架",
3783
- visibleModelsTitle: "模型白名单",
3784
- visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
3785
- visibleModelsPick: "选择要保留的模型…",
3786
- visibleModelsCount: "已选 {count} 个模型",
3787
- visibleModelsShowAll: "显示全部",
3788
- visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
3789
- visibleModelsCleanStale: "清理失效({count})",
3790
- overridden: "已覆盖",
3791
- reset: "重置",
3792
- invalidNumber: "无效数字",
3793
- numberTooSmall: "不能小于 1(毫秒)",
3794
- numberTooLarge: "超出允许上限(2147483647 毫秒)",
3795
- readOnly: "当前配置为只读。",
3796
- unsaved: "未保存",
3797
- save: "保存",
3798
- saving: "保存中",
3799
- saved: "已保存 ✓",
3800
- saveFailed: "保存失败,请重试。",
3801
- discard: "放弃",
3802
- cancel: "取消",
3803
- show: "显示",
3804
- hide: "隐藏",
3805
- usageTitle: "账户用量",
3806
- usageRefresh: "刷新",
3807
- usageRefreshing: "刷新中…",
3808
- usageLoading: "正在获取账户用量…",
3809
- usageNoKey: "配置 API 密钥后,这里会显示账户的用量与额度状态。",
3810
- usageError: "用量获取失败",
3811
- usageRequests: "请求",
3812
- usageFailed: "失败",
3813
- usageSuccessRate: "成功率",
3814
- usageCost: "花费",
3815
- usageTokens: "Token",
3816
- usageTokensIn: "入",
3817
- usageTokensOut: "出",
3818
- usageMonthly: "月额度",
3819
- usagePurchased: "已购",
3820
- usageFree: "赠送",
3821
- usageFiveHour: "5 小时窗口",
3822
- usageWeekly: "每周窗口",
3823
- usageExceeded: "已超限",
3824
- usageReset: "重置于",
3825
- usagePartial: "部分端点数据不可用",
3826
- usageKeyClear: "清除已存密钥",
3827
- usageKeyClearStaged: "将清除(保存后生效)",
3828
- usageUndoKeyClear: "撤销清除",
3829
- usageKeyInvalid: "API 密钥无效或已过期",
3830
- usageKeyInvalidHint: "服务端拒绝了全部请求(401)。请检查该账户配置的密钥,或到 commandcode.ai 控制台重新生成。",
3831
- usageServiceUnavailable: "Command Code 服务暂时不可用",
3832
- usageServiceUnavailableHint: "服务端返回了错误(5xx),稍后点击刷新重试。",
3833
- usageNetworkError: "无法连接 Command Code 服务",
3834
- usageNetworkHint: "所有请求都没有到达服务端。请检查网络连接或 API 地址设置。",
3835
- usageUpdated: "更新于",
3836
- usagePeriodEnd: "账期截止",
3837
- usageActive: "当前使用",
3838
- usageCooldown: "限额冷却中",
3839
- usageInvalidKey: "密钥无效",
3840
- usageUnconfigured: "该账户尚未配置 API 密钥。",
3841
- updateAvailable: "可更新",
3842
- updateHint: "已发布新版本,点击查看发布说明;更新插件后刷新本页,提示会自动消失。",
3843
- loginTitle: "通过官方登录获取密钥",
3844
- loginHintIdle: "不想手动创建密钥?点击登录后浏览器会打开 commandcode.ai 授权页,完成后密钥自动写入本机凭据服务,下次请求即生效。",
3845
- loginButton: "登录 Command Code",
3846
- loginStarting: "正在启动本地回调服务…",
3847
- loginWaiting: "等待在浏览器中完成授权…",
3848
- loginOpenLink: "打开授权页面 ↗",
3849
- loginCancel: "取消登录",
3850
- loginSuccess: "已登录为",
3851
- loginUnavailable: "此环境暂不支持登录流程,请手动粘贴密钥。",
3852
- loginDenied: "授权被拒绝。可重试,或手动粘贴密钥。",
3853
- loginTimeout: "等待超时:未在窗口期内收到授权回调,请重试。",
3854
- loginInvalidKey: "获取到的密钥未通过校验(401),请重试或手动粘贴。",
3855
- loginNetwork: "无法连接 Command Code 服务校验密钥,请检查网络后重试。",
3856
- loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
3857
- loginCancelled: "登录已取消。",
3858
- loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。",
3859
- cardTitle: "Command Code",
3860
- cardRouteActive: "已启用",
3861
- cardLoadingHint: "正在读取 Command Code 配置…",
3862
- cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。"
3971
+ subtitle: "Plans, credits and quota windows",
3972
+ refresh: "Refresh",
3973
+ refreshing: "Refreshing…",
3974
+ loading: "Loading account usage…",
3975
+ noKey: "No API key configured",
3976
+ noKeyHint: "Paste a key — or sign in — under Settings → Command Code, then refresh.",
3977
+ plan: "Plan",
3978
+ credits: "Credits",
3979
+ limits: "Quota windows",
3980
+ usage: "Usage",
3981
+ monthly: "Monthly",
3982
+ monthlyLimit: "Monthly limit",
3983
+ monthlyUsed: "Monthly used",
3984
+ remaining: "Remaining",
3985
+ purchased: "Purchased",
3986
+ free: "Free",
3987
+ fiveHour: "5-hour window",
3988
+ weekly: "Weekly window",
3989
+ fiveHourShort: "5-hour",
3990
+ weeklyShort: "Weekly",
3991
+ windowUnlimited: "unlimited",
3992
+ exceeded: "Exceeded",
3993
+ exhausted: "Used up",
3994
+ resets: "Resets",
3995
+ requests: "Requests",
3996
+ failed: "failed",
3997
+ successRate: "Success rate",
3998
+ spend: "Spend",
3999
+ tokens: "Tokens",
4000
+ tokensIn: "in",
4001
+ tokensOut: "out",
4002
+ periodEnds: "Period ends",
4003
+ updated: "Updated",
4004
+ partial: "Some endpoint data unavailable",
4005
+ active: "Active",
4006
+ coolingDown: "Cooling down",
4007
+ invalidKey: "Invalid key",
4008
+ unconfigured: "Not configured",
4009
+ unavailable: "No data",
4010
+ errorInvalidKey: "API key invalid or expired",
4011
+ errorInvalidKeyHint: "The server rejected every request (401). Check the key for this account, or generate a new one in the commandcode.ai console.",
4012
+ errorServiceUnavailable: "The Command Code service is temporarily unavailable",
4013
+ errorServiceUnavailableHint: "The server returned errors (5xx). Try Refresh again in a moment.",
4014
+ errorNetwork: "Could not reach the Command Code service",
4015
+ errorNetworkHint: "No request reached the server. Check your network connection or the API base setting.",
4016
+ errorGeneric: "Could not fetch account usage"
3863
4017
  };
3864
- const en = {
3865
- nav: "Command Code",
3866
- title: "Command Code",
3867
- intro: "Configure the Command Code Provider connection. The API key is stored only in the local credential service and never echoed; other fields are written to user settings and take effect on the next request.",
3868
- apiKey: "API key",
3869
- apiKeyHint: "Create one in the commandcode.ai console. Saving with this field blank keeps the stored key.",
3870
- apiKeySet: "Configured",
3871
- apiKeyUnset: "Not configured",
3872
- apiKeyLocked: "Key provided by a read-only source",
3873
- apiBase: "API base URL",
3874
- apiBaseHint: "Defaults to https://api.commandcode.ai; usually leave as-is.",
3875
- workingDir: "Working directory",
3876
- workingDirHint: "Optional. Leave blank to use the process cwd shown as the placeholder; fill in only to pin a specific path.",
3877
- requestTimeoutMs: "Request timeout (ms)",
3878
- requestTimeoutMsHint: "Time to wait for the first response byte; default 60000.",
3879
- streamIdleTimeoutMs: "Stream idle timeout (ms)",
3880
- streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300000 (deliberately generous — long-thinking models can stay silent for minutes).",
3881
- advancedSettings: "Advanced",
3882
- advancedSettingsHint: "Rarely touched options: API base URL, working directory, timeouts, and model filtering.",
3883
- advancedOverriddenOne: "1 customized",
3884
- advancedOverriddenMany: "{count} customized",
3885
- advancedInvalid: "A number in Advanced settings is not ready to save; expand to fix it.",
3886
- filterModelsByPlan: "Hide out-of-plan models",
3887
- filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
3888
- webSearch: "Serve dsh web search with Command Code",
3889
- webSearchHint: "When on, the model-facing web_search tool is backed by Command Code (same API key and base URL as chat), winning over other search backends. Off hands the selection back to the previous backend (e.g. modsearch) instead of forcing the shipped DeepSeek search.",
3890
- accountsTitle: "Account rotation",
3891
- accountsHint: "When the active account hits its usage limit (429) or its key fails (401), requests switch to the next account; when every account is exhausted the error names the earliest window reset.",
3892
- accountAdd: "Add account",
3893
- accountRemove: "Remove",
3894
- accountLabel: "Account label",
3895
- accountKey: "API key",
3896
- accountKeyHint: "This account’s API key. Saving with the field blank keeps the stored key.",
4018
+ /** Look up one panel string. The fallback key keeps a bad call visible, never blank. */
4019
+ function panelText(key) {
4020
+ return PANEL_COPY[key] ?? key;
4021
+ }
4022
+ //#endregion
4023
+ //#region src/client/panel.ts
4024
+ /** `$1.23`, and `$0.0123` only when the amount is too small for cents to show it. */
4025
+ function money(value) {
4026
+ if (value === 0) return formatMoney(0);
4027
+ return Math.abs(value) < .01 ? formatMoneyExact(value) : formatMoney(value);
4028
+ }
4029
+ /**
4030
+ * What a figure shows when the endpoint that carries it did not report one.
4031
+ * A dash, never a zero: the panel exists to state what the account has spent,
4032
+ * so "we were not told" has to look different from "nothing".
4033
+ */
4034
+ const UNREPORTED = "—";
4035
+ /** Local reset time; empty when the endpoint reported none. */
4036
+ function resetText(ms) {
4037
+ if (ms <= 0) return "";
4038
+ return new Date(ms).toLocaleString();
4039
+ }
4040
+ /** Local short date; empty when unset. */
4041
+ function dateText(ms) {
4042
+ if (ms <= 0) return "";
4043
+ return new Date(ms).toLocaleDateString();
4044
+ }
4045
+ /** Local time-of-day; empty when unset. */
4046
+ function timeText(ms) {
4047
+ if (ms <= 0) return "";
4048
+ return new Date(ms).toLocaleTimeString();
4049
+ }
4050
+ /**
4051
+ * Consumption as a percentage, NOT clamped at 100 — an over-quota window
4052
+ * reports its real overshoot (150%), which is what the printed figure should
4053
+ * say. Bar widths clamp separately via {@link barPercent}.
4054
+ */
4055
+ function rawPercent(used, cap) {
4056
+ if (cap <= 0) return 0;
4057
+ return Math.round(used / cap * 100);
4058
+ }
4059
+ /** A percentage usable as a CSS width, clamped into [0, 100]. */
4060
+ function barPercent(percent) {
4061
+ return Math.min(100, Math.max(0, percent));
4062
+ }
4063
+ /** Build one quota-window view. */
4064
+ function windowView(label, limit) {
4065
+ const percent = rawPercent(limit.used, limit.cap);
4066
+ const capped = limit.cap > 0;
4067
+ return {
4068
+ label,
4069
+ value: capped ? `${money(limit.used)} / ${money(limit.cap)}` : money(limit.used),
4070
+ capped,
4071
+ percent,
4072
+ barPercent: barPercent(percent),
4073
+ exceeded: limit.exceeded,
4074
+ resetsAt: resetText(limit.resetAt)
4075
+ };
4076
+ }
4077
+ /**
4078
+ * Build the monthly credit view from the two endpoints that carry it.
4079
+ *
4080
+ * The limit is the PLAN's credit total, not the billing endpoint's
4081
+ * `monthlyCredits` — that field is a remaining balance (see
4082
+ * {@link PanelMonthlyView}). Unknown plans (no `monthlyCredits` on the plan
4083
+ * record) therefore still show their balances, just without a ratio.
4084
+ *
4085
+ * The trap this guards: a MISSING balance is not a consumed one. The billing
4086
+ * endpoint is one of four the report fetches in parallel, so it fails on its
4087
+ * own while the plan still arrives — and reading the absent balance as 0 would
4088
+ * turn that partial failure into `limit - 0 = limit`, i.e. a confident "100%
4089
+ * used, quota exhausted" for an account that may have spent nothing. The
4090
+ * official CLI draws no meter unless the credits payload is present at all
4091
+ * (`hasCreditsInfo`), so "unreported" is the upstream-faithful reading here
4092
+ * too. Every derived figure stays a placeholder until the balance reported.
4093
+ */
4094
+ function monthlyView(report) {
4095
+ const credits = report.credits;
4096
+ const plan = report.plan;
4097
+ if (credits === void 0 && plan === void 0) return void 0;
4098
+ const limitValue = plan?.monthlyCredits ?? null;
4099
+ const remainingReported = credits !== void 0 && credits.monthlyReported !== false;
4100
+ const remaining = remainingReported ? Math.max(0, credits.monthlyCredits) : 0;
4101
+ const known = limitValue !== null && limitValue > 0 && remainingReported;
4102
+ const used = known ? Math.max(0, limitValue - remaining) : 0;
4103
+ const percent = known ? rawPercent(used, limitValue) : 0;
4104
+ return {
4105
+ known,
4106
+ limit: limitValue !== null ? money(limitValue) : UNREPORTED,
4107
+ used: known ? money(used) : UNREPORTED,
4108
+ remaining: remainingReported ? money(remaining) : UNREPORTED,
4109
+ purchased: credits !== void 0 && credits.purchasedReported !== false ? money(Math.max(0, credits.purchasedCredits)) : UNREPORTED,
4110
+ free: credits !== void 0 && credits.freeReported !== false ? money(Math.max(0, credits.freeCredits)) : UNREPORTED,
4111
+ percent,
4112
+ barPercent: barPercent(percent),
4113
+ exhausted: known && remaining <= 0,
4114
+ periodEnds: dateText(plan?.currentPeriodEnd ?? 0)
4115
+ };
4116
+ }
4117
+ /** Build one account's view. */
4118
+ function accountView(entry) {
4119
+ const { report } = entry;
4120
+ const account = report.account;
4121
+ const plan = report.plan;
4122
+ const credits = report.credits;
4123
+ const usage = report.usage;
4124
+ const stats = [];
4125
+ if (usage !== void 0) {
4126
+ stats.push({
4127
+ label: "requests",
4128
+ value: String(usage.completedCount),
4129
+ sub: `${usage.failedCount} ${panelText("failed")}`
4130
+ });
4131
+ stats.push({
4132
+ label: "successRate",
4133
+ value: `${formatSuccessRate(usage.successRate)}%`,
4134
+ sub: ""
4135
+ });
4136
+ stats.push({
4137
+ label: "spend",
4138
+ value: formatMoneyExact(usage.totalCost),
4139
+ sub: `${formatMoney(usage.totalCredits)} credits`
4140
+ });
4141
+ stats.push({
4142
+ label: "tokens",
4143
+ value: formatTokensCompact(usage.totalTokensIn + usage.totalTokensOut),
4144
+ sub: `${formatTokensCompact(usage.totalTokensIn)} ${panelText("tokensIn")} / ${formatTokensCompact(usage.totalTokensOut)} ${panelText("tokensOut")}`
4145
+ });
4146
+ }
4147
+ const windows = [];
4148
+ if (credits?.fiveHour !== void 0) windows.push(windowView("fiveHour", credits.fiveHour));
4149
+ if (credits?.weekly !== void 0) windows.push(windowView("weekly", credits.weekly));
4150
+ let mark;
4151
+ if (entry.active) mark = "active";
4152
+ else if (entry.mark === "invalid-credential") mark = "invalidKey";
4153
+ else if (entry.cooldownUntil > 0 || entry.mark === "rate-limit") mark = "coolingDown";
4154
+ return {
4155
+ id: entry.id,
4156
+ label: entry.label,
4157
+ owner: account === void 0 ? "" : account.userName || account.name,
4158
+ planName: plan?.name ?? "",
4159
+ planStatus: plan !== void 0 && plan.status !== "" && plan.status !== "active" ? plan.status : "",
4160
+ mark,
4161
+ cooldownUntil: entry.cooldownUntil > 0 ? resetText(entry.cooldownUntil) : "",
4162
+ unconfigured: !entry.configured,
4163
+ periodEnds: dateText(plan?.currentPeriodEnd ?? 0),
4164
+ active: entry.active,
4165
+ monthly: monthlyView(report),
4166
+ windows,
4167
+ stats
4168
+ };
4169
+ }
4170
+ /** The report-level failure box, or undefined when the report is usable. */
4171
+ function failureView(state) {
4172
+ const blocked = state.report?.accounts.find((entry) => entry.report.blocked !== void 0)?.report.blocked;
4173
+ if (blocked === "invalid-key") return {
4174
+ title: "errorInvalidKey",
4175
+ hint: "errorInvalidKeyHint",
4176
+ detail: ""
4177
+ };
4178
+ if (blocked === "service-unavailable") return {
4179
+ title: "errorServiceUnavailable",
4180
+ hint: "errorServiceUnavailableHint",
4181
+ detail: ""
4182
+ };
4183
+ if (blocked === "network") return {
4184
+ title: "errorNetwork",
4185
+ hint: "errorNetworkHint",
4186
+ detail: ""
4187
+ };
4188
+ if (state.status === "error" && state.report === void 0) return {
4189
+ title: "errorGeneric",
4190
+ hint: "errorGeneric",
4191
+ detail: state.error ?? ""
4192
+ };
4193
+ }
4194
+ /**
4195
+ * Every panel string, resolved once per projection. One object with all keys
4196
+ * (rather than per-field lookups in the components) keeps the copy table and
4197
+ * the render sites in lockstep: a key cannot be read from `text` unless
4198
+ * {@link PANEL_COPY} declares it.
4199
+ */
4200
+ function panelStrings() {
4201
+ const keys = Object.keys(PANEL_COPY);
4202
+ const out = {};
4203
+ for (const key of keys) out[key] = panelText(key);
4204
+ return out;
4205
+ }
4206
+ /**
4207
+ * Project the shared usage snapshot into the panel's render tree.
4208
+ *
4209
+ * Deduplication matches the settings card: hand-edited settings can name one
4210
+ * credential twice, and removal staging hides an account before the post-save
4211
+ * refresh lands.
4212
+ */
4213
+ function buildPanelView(input) {
4214
+ const { usage } = input;
4215
+ const hidden = new Set(input.removingIds ?? []);
4216
+ const seen = /* @__PURE__ */ new Set();
4217
+ const entries = (usage.report?.accounts ?? []).filter((entry) => {
4218
+ if (hidden.has(entry.id)) return false;
4219
+ if (seen.has(entry.id)) return false;
4220
+ seen.add(entry.id);
4221
+ return true;
4222
+ });
4223
+ const accounts = entries.map(accountView);
4224
+ const selectedEntry = entries.find((entry) => entry.active) ?? entries[0];
4225
+ const selectedView = accounts.find((view) => view.id === selectedEntry?.id);
4226
+ const footerBars = [];
4227
+ const windowBars = [["fiveHourShort", selectedEntry?.report.credits?.fiveHour], ["weeklyShort", selectedEntry?.report.credits?.weekly]];
4228
+ for (const [label, window] of windowBars) {
4229
+ if (window === void 0 || window.cap <= 0) continue;
4230
+ const percent = rawPercent(window.used, window.cap);
4231
+ footerBars.push({
4232
+ label,
4233
+ percent: `${percent}%`,
4234
+ barPercent: barPercent(percent),
4235
+ warn: window.exceeded,
4236
+ detail: `${money(window.used)} / ${money(window.cap)}`
4237
+ });
4238
+ }
4239
+ const totalCost = selectedEntry?.report.usage?.totalCost;
4240
+ const cost = totalCost === void 0 ? "" : money(totalCost);
4241
+ let status = "";
4242
+ if (entries.length > 0 && !entries.some((entry) => entry.configured)) status = panelText("unconfigured");
4243
+ else if (selectedView?.mark !== void 0 && selectedView.mark !== "active") status = panelText(selectedView.mark);
4244
+ else if (selectedEntry === void 0) status = panelText("unavailable");
4245
+ const planName = selectedView !== void 0 && selectedView.planName !== "" ? selectedView.planName : panelText("nav");
4246
+ const titleParts = planName === panelText("nav") ? [planName] : [panelText("nav"), planName];
4247
+ for (const bar of footerBars) {
4248
+ const figures = bar.detail === "" ? "" : ` ${bar.detail}`;
4249
+ titleParts.push(`${panelText(bar.label)}${figures} (${bar.percent})`);
4250
+ }
4251
+ if (cost !== "") titleParts.push(`${panelText("spend")} ${cost}`);
4252
+ return {
4253
+ text: panelStrings(),
4254
+ planName,
4255
+ status,
4256
+ footerBars,
4257
+ cost,
4258
+ footTitle: titleParts.join(" · "),
4259
+ accounts,
4260
+ selectedId: selectedEntry?.id,
4261
+ selected: selectedView,
4262
+ loading: usage.status === "loading" && usage.report === void 0,
4263
+ noKey: entries.length > 0 && !entries.some((entry) => entry.configured),
4264
+ failure: failureView(usage),
4265
+ staleError: usage.status === "error" && usage.report !== void 0 ? usage.error ?? "" : void 0,
4266
+ partial: usage.report?.accounts.some((entry) => entry.report.failures.length > 0 && entry.report.blocked === void 0) ?? false ? "partial" : void 0,
4267
+ updatedAt: timeText(usage.fetchedAt ?? 0)
4268
+ };
4269
+ }
4270
+ /**
4271
+ * How often the panel re-reads the report while a surface is mounted. The
4272
+ * quota windows move slowly and one report costs four upstream calls, so this
4273
+ * is a background freshness tick, not a live meter.
4274
+ */
4275
+ const PANEL_AUTO_REFRESH_MS = 12e4;
4276
+ /** The default timer (the client bundle runs in a browser; node tests inject one). */
4277
+ const REAL_TIMER = {
4278
+ set: (callback, ms) => setTimeout(callback, ms),
4279
+ clear: (handle) => clearTimeout(handle)
4280
+ };
4281
+ /** Live mount count of {@link startPanelAutoRefresh}. */
4282
+ let references = 0;
4283
+ /** Owner ticket of the running loop, if any. */
4284
+ let activeTicket;
4285
+ /** Ticket handed to the next starter. */
4286
+ let ticketSeq = 0;
4287
+ /** Pending timer handle of the running loop. */
4288
+ let handle;
4289
+ /**
4290
+ * Start the shared auto refresh. One fetch when the surface appears (the
4291
+ * sidebar row is the point of the panel — it must be current, not wait for a
4292
+ * click), then a tick every {@link PANEL_AUTO_REFRESH_MS} while a surface
4293
+ * stays mounted.
4294
+ *
4295
+ * Reference-counted: the sidebar entry and the dashboard can be mounted at
4296
+ * once, so only the first start fetches and only the last stop halts the loop.
4297
+ * Every tick goes through `usage.refresh()`, which already collapses a
4298
+ * concurrent fetch onto the in-flight one — a tick never double-fetches
4299
+ * against the settings page's own refresh.
4300
+ *
4301
+ * @param usage - the shared usage controller.
4302
+ * @param isConfigured - whether a credential exists right now (re-read per tick).
4303
+ * @param timer - timer seam for tests.
4304
+ * @returns the disposer that drops this surface's reference.
4305
+ */
4306
+ function startPanelAutoRefresh(usage, isConfigured, timer = REAL_TIMER) {
4307
+ references += 1;
4308
+ const ticket = ++ticketSeq;
4309
+ const tick = () => {
4310
+ if (activeTicket !== ticket) return;
4311
+ if (isConfigured()) usage.refresh();
4312
+ handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
4313
+ };
4314
+ if (activeTicket === void 0) {
4315
+ activeTicket = ticket;
4316
+ handle = timer.set(tick, PANEL_AUTO_REFRESH_MS);
4317
+ if (isConfigured()) usage.refresh();
4318
+ }
4319
+ let stopped = false;
4320
+ return () => {
4321
+ if (stopped) return;
4322
+ stopped = true;
4323
+ references -= 1;
4324
+ if (references > 0) return;
4325
+ activeTicket = void 0;
4326
+ if (handle !== void 0) {
4327
+ timer.clear(handle);
4328
+ handle = void 0;
4329
+ }
4330
+ };
4331
+ }
4332
+ //#endregion
4333
+ //#region src/client/panel-view.tsx
4334
+ /**
4335
+ * React components for the Command Code plans & quota panel (browser half):
4336
+ * the sidebar footer card and the dashboard it opens in the center column.
4337
+ *
4338
+ * Both render one {@link PanelView} projected by `./panel.ts` — no fact is
4339
+ * derived here. Strings arrive as keys into `view.text`, and every one of them
4340
+ * is English, because the view is built from `./panel-copy.ts` rather than the
4341
+ * harness `ctx.locale` namespace (which follows the user's language and would
4342
+ * render this panel in Chinese on a Chinese harness — the thing this surface
4343
+ * exists to avoid).
4344
+ *
4345
+ * The footer card is the panel's home: the sidebar shell renders it in the foot
4346
+ * area directly above the Settings seat, so each quota window's own spend and
4347
+ * limit — the 5-hour and the weekly one — are on screen without opening
4348
+ * anything. Clicking it selects the `main` panel this file also renders — and
4349
+ * unlike a `sidebar.panellist` row, whose button chrome and label the SHELL
4350
+ * owns, this entry owns its whole surface and therefore calls `open()` itself.
4351
+ *
4352
+ * Styles ride the stylesheet `./panel-styles.ts` returns, injected once by
4353
+ * the client entry; classes are `ccp-` prefixed to stay clear of the settings
4354
+ * page's `cc-` set.
4355
+ *
4356
+ * @module dsh-commandcode-provider/client/panel-view
4357
+ */
4358
+ /** The panel's view, recomputed from both seats on every notification. */
4359
+ function usePanelView(props) {
4360
+ const usage = props.useCommandCodeUsage((state) => state);
4361
+ const settings = props.useCommandCodeSettings((state) => state);
4362
+ return buildPanelView({
4363
+ usage,
4364
+ apiKeyConfigured: settings.anyAccountConfigured,
4365
+ removingIds: settings.accountsRemoving
4366
+ });
4367
+ }
4368
+ /**
4369
+ * The quota ring. One glyph serves the rail button, the footer card's top row
4370
+ * and the dashboard header: a faint track plus an arc whose sweep is the
4371
+ * consumption, drawn from 12 o'clock. Circumference 2πr = 45.55 at r = 7.25.
4372
+ */
4373
+ function Ring({ percent, warn, size }) {
4374
+ const clamped = Math.min(100, Math.max(0, percent));
4375
+ const circumference = 45.55;
4376
+ const dashoffset = Math.round(circumference * (1 - clamped / 100) * 1e3) / 1e3;
4377
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4378
+ className: "ccp-glyph",
4379
+ "aria-hidden": "true",
4380
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
4381
+ viewBox: "0 0 20 20",
4382
+ width: size,
4383
+ height: size,
4384
+ focusable: "false",
4385
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4386
+ cx: "10",
4387
+ cy: "10",
4388
+ r: "7.25",
4389
+ fill: "none",
4390
+ stroke: "currentColor",
4391
+ strokeWidth: "1.5",
4392
+ opacity: "0.4"
4393
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
4394
+ cx: "10",
4395
+ cy: "10",
4396
+ r: "7.25",
4397
+ fill: "none",
4398
+ stroke: warn ? "var(--dsw-alias-state-error-primary)" : "currentColor",
4399
+ strokeWidth: "2.5",
4400
+ strokeLinecap: "round",
4401
+ strokeDasharray: String(circumference),
4402
+ strokeDashoffset: String(dashoffset),
4403
+ transform: "rotate(-90 10 10)"
4404
+ })]
4405
+ })
4406
+ });
4407
+ }
4408
+ /** One labelled bar. `compact` drops the reset line; the footer draws its own. */
4409
+ function QuotaBar({ label, value, percent, barPercent, warn, resetsAt, resetsLabel }) {
4410
+ const clamped = Math.min(100, Math.max(0, barPercent));
4411
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4412
+ className: "ccp-window",
4413
+ children: [
4414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4415
+ className: "ccp-windowHead",
4416
+ children: [
4417
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4418
+ className: "ccp-windowLabel",
4419
+ children: label
4420
+ }),
4421
+ warn !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4422
+ className: "ccp-warnTag",
4423
+ children: warn
4424
+ }) : null,
4425
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
4426
+ value !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4427
+ className: "ccp-windowValue",
4428
+ children: value
4429
+ }) : null,
4430
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4431
+ className: "ccp-windowPct",
4432
+ children: percent
4433
+ })
4434
+ ]
4435
+ }),
4436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4437
+ className: "ccp-bar",
4438
+ role: "progressbar",
4439
+ "aria-label": label,
4440
+ "aria-valuemin": 0,
4441
+ "aria-valuemax": 100,
4442
+ "aria-valuenow": clamped,
4443
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4444
+ className: warn !== "" ? "ccp-barFill ccp-barFillWarn" : "ccp-barFill",
4445
+ style: { width: `${clamped}%` }
4446
+ })
4447
+ }),
4448
+ resetsAt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4449
+ className: "ccp-windowReset",
4450
+ children: [
4451
+ resetsLabel,
4452
+ " ",
4453
+ resetsAt
4454
+ ]
4455
+ }) : null
4456
+ ]
4457
+ });
4458
+ }
4459
+ /** A labelled figure. */
4460
+ function Tile({ label, value, sub }) {
4461
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4462
+ className: "ccp-tile",
4463
+ children: [
4464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4465
+ className: "ccp-tileLabel",
4466
+ children: label
4467
+ }),
4468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4469
+ className: "ccp-tileValue",
4470
+ children: value
4471
+ }),
4472
+ sub !== void 0 && sub !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4473
+ className: "ccp-tileSub",
4474
+ children: sub
4475
+ }) : null
4476
+ ]
4477
+ });
4478
+ }
4479
+ /** One usage tile. */
4480
+ function StatTile({ stat, label }) {
4481
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
4482
+ label,
4483
+ value: stat.value,
4484
+ sub: stat.sub
4485
+ });
4486
+ }
4487
+ /** The rotation/credential badge line for one account. */
4488
+ function markText(account, text) {
4489
+ if (account.mark === void 0) return "";
4490
+ if (account.mark === "coolingDown" && account.cooldownUntil !== "") return `${text("coolingDown")} · ${account.cooldownUntil}`;
4491
+ return text(account.mark);
4492
+ }
4493
+ /** `Default account` → `D`; used for the card's monogram chip. */
4494
+ function initial(label) {
4495
+ const trimmed = label.trim();
4496
+ return trimmed === "" ? "?" : trimmed[0].toUpperCase();
4497
+ }
4498
+ /**
4499
+ * One account's full report: the monthly limit/usage bar, the two quota
4500
+ * windows, then the credit and usage totals.
4501
+ */
4502
+ function AccountCard({ account, view }) {
4503
+ const text = (key) => view.text[key] ?? key;
4504
+ const mark = markText(account, text);
4505
+ const monthly = account.monthly;
4506
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4507
+ className: "ccp-card",
4508
+ "aria-label": account.label,
4509
+ children: [
4510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4511
+ className: "ccp-cardHead",
4512
+ children: [
4513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4514
+ className: "ccp-avatar",
4515
+ "aria-hidden": "true",
4516
+ children: initial(account.label)
4517
+ }),
4518
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4519
+ className: "ccp-cardIdentity",
4520
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4521
+ className: "ccp-cardTitle",
4522
+ children: account.label
4523
+ }), account.owner !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4524
+ className: "ccp-cardOwner",
4525
+ children: account.owner
4526
+ }) : null]
4527
+ }),
4528
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
4529
+ account.planName !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4530
+ className: "ccp-badge",
4531
+ children: account.planName
4532
+ }) : null,
4533
+ account.planStatus !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4534
+ className: "ccp-badge ccp-badgeWarn",
4535
+ children: account.planStatus
4536
+ }) : null,
4537
+ mark !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4538
+ className: account.mark === "invalidKey" ? "ccp-badge ccp-badgeError" : "ccp-badgeMuted",
4539
+ children: mark
4540
+ }) : null,
4541
+ account.periodEnds !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4542
+ className: "ccp-meta",
4543
+ children: [
4544
+ text("periodEnds"),
4545
+ " ",
4546
+ account.periodEnds
4547
+ ]
4548
+ }) : null
4549
+ ]
4550
+ }),
4551
+ account.unconfigured ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4552
+ className: "ccp-hint",
4553
+ children: text("unconfigured")
4554
+ }) : null,
4555
+ monthly !== void 0 && monthly.known ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaBar, {
4556
+ label: text("monthly"),
4557
+ value: `${monthly.used} / ${monthly.limit}`,
4558
+ percent: `${monthly.percent}%`,
4559
+ barPercent: monthly.barPercent,
4560
+ warn: monthly.exhausted ? text("exhausted") : "",
4561
+ resetsAt: "",
4562
+ resetsLabel: ""
4563
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4564
+ className: "ccp-planRow",
4565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4566
+ className: "ccp-fieldLabel",
4567
+ children: text("plan")
4568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4569
+ className: "ccp-planName",
4570
+ children: account.planName !== "" ? account.planName : text("unavailable")
4571
+ })]
4572
+ }),
4573
+ monthly !== void 0 && monthly.remaining !== "—" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4574
+ className: "ccp-planRow",
4575
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4576
+ className: "ccp-fieldLabel",
4577
+ children: text("remaining")
4578
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4579
+ className: "ccp-planName",
4580
+ children: monthly.remaining
4581
+ })]
4582
+ }) : null,
4583
+ account.windows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4584
+ className: "ccp-windows",
4585
+ children: account.windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaBar, {
4586
+ label: text(window.label),
4587
+ value: window.value,
4588
+ percent: window.capped ? `${window.percent}%` : text("windowUnlimited"),
4589
+ barPercent: window.barPercent,
4590
+ warn: window.exceeded ? text("exceeded") : "",
4591
+ resetsAt: window.resetsAt,
4592
+ resetsLabel: text("resets")
4593
+ }, window.label))
4594
+ }) : null,
4595
+ monthly !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4596
+ className: "ccp-block",
4597
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
4598
+ className: "ccp-blockTitle",
4599
+ children: text("credits")
4600
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4601
+ className: "ccp-tiles",
4602
+ children: [
4603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
4604
+ label: text("monthlyLimit"),
4605
+ value: monthly.limit
4606
+ }),
4607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
4608
+ label: text("monthlyUsed"),
4609
+ value: monthly.used
4610
+ }),
4611
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
4612
+ label: text("remaining"),
4613
+ value: monthly.remaining
4614
+ }),
4615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
4616
+ label: text("purchased"),
4617
+ value: monthly.purchased
4618
+ }),
4619
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tile, {
4620
+ label: text("free"),
4621
+ value: monthly.free
4622
+ })
4623
+ ]
4624
+ })]
4625
+ }) : null,
4626
+ account.stats.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4627
+ className: "ccp-block",
4628
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
4629
+ className: "ccp-blockTitle",
4630
+ children: text("usage")
4631
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4632
+ className: "ccp-tiles",
4633
+ children: account.stats.map((stat) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatTile, {
4634
+ stat,
4635
+ label: text(stat.label)
4636
+ }, stat.label))
4637
+ })]
4638
+ }) : null
4639
+ ]
4640
+ });
4641
+ }
4642
+ /**
4643
+ * The center-column dashboard, registered into the layout's keyed `main` slot
4644
+ * under the same id the footer card selects, so the two are one navigation
4645
+ * entry: the card shows the plan, the monthly bar and the quota window; the
4646
+ * panel shows everything, including the per-account breakdown.
4647
+ */
4648
+ function CommandCodePanel(props) {
4649
+ const view = usePanelView(props);
4650
+ const text = (key) => view.text[key] ?? key;
4651
+ const [selectedId, setSelectedId] = (0, react.useState)(void 0);
4652
+ const startAutoRefresh = props.startAutoRefresh;
4653
+ (0, react.useEffect)(() => startAutoRefresh(), [startAutoRefresh]);
4654
+ const accounts = view.accounts;
4655
+ const selected = accounts.find((account) => account.id === selectedId) ?? view.selected ?? accounts[0];
4656
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4657
+ className: "ccp-main",
4658
+ role: "region",
4659
+ "aria-label": text("nav"),
4660
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4661
+ className: "ccp-mainInner",
4662
+ children: [
4663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
4664
+ className: "ccp-header",
4665
+ children: [
4666
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4667
+ className: "ccp-headerText",
4668
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
4669
+ className: "ccp-title",
4670
+ children: text("nav")
4671
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4672
+ className: "ccp-subtitle",
4673
+ children: text("subtitle")
4674
+ })]
4675
+ }),
4676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
4677
+ view.updatedAt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4678
+ className: "ccp-meta",
4679
+ children: [
4680
+ text("updated"),
4681
+ " ",
4682
+ view.updatedAt
4683
+ ]
4684
+ }) : null,
4685
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
4686
+ variant: "ghost",
4687
+ size: "sm",
4688
+ disabled: view.loading,
4689
+ onClick: () => props.refresh(),
4690
+ children: view.loading ? text("refreshing") : text("refresh")
4691
+ })
4692
+ ]
4693
+ }),
4694
+ view.noKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4695
+ className: "ccp-notice",
4696
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4697
+ className: "ccp-noticeTitle",
4698
+ children: text("noKey")
4699
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4700
+ className: "ccp-noticeHint",
4701
+ children: text("noKeyHint")
4702
+ })]
4703
+ }) : null,
4704
+ view.failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4705
+ className: "ccp-notice ccp-noticeError",
4706
+ role: "alert",
4707
+ children: [
4708
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4709
+ className: "ccp-noticeTitle",
4710
+ children: text(view.failure.title)
4711
+ }),
4712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4713
+ className: "ccp-noticeHint",
4714
+ children: text(view.failure.hint)
4715
+ }),
4716
+ view.failure.detail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4717
+ className: "ccp-noticeDetail",
4718
+ children: view.failure.detail
4719
+ }) : null
4720
+ ]
4721
+ }) : null,
4722
+ !view.noKey && view.failure === void 0 && accounts.length === 0 && view.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4723
+ className: "ccp-hint",
4724
+ children: text("loading")
4725
+ }) : null,
4726
+ view.staleError !== void 0 && view.staleError !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4727
+ className: "ccp-hint",
4728
+ role: "status",
4729
+ children: [
4730
+ text("errorGeneric"),
4731
+ " — ",
4732
+ view.staleError
4733
+ ]
4734
+ }) : null,
4735
+ accounts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4736
+ className: "ccp-tabs",
4737
+ role: "tablist",
4738
+ "aria-label": text("nav"),
4739
+ children: accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4740
+ type: "button",
4741
+ role: "tab",
4742
+ "aria-selected": selected?.id === account.id,
4743
+ className: selected?.id === account.id ? "ccp-tab ccp-tabActive" : "ccp-tab",
4744
+ onClick: () => setSelectedId(account.id),
4745
+ children: account.label
4746
+ }, account.id))
4747
+ }) : null,
4748
+ selected !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountCard, {
4749
+ account: selected,
4750
+ view
4751
+ }) : null,
4752
+ view.partial !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4753
+ className: "ccp-hint",
4754
+ children: text(view.partial)
4755
+ }) : null
4756
+ ]
4757
+ })
4758
+ });
4759
+ }
4760
+ /**
4761
+ * The sidebar footer card, registered into `sidebar.footer.action` — the list
4762
+ * the shell renders in the sidebar's foot area directly ABOVE the Settings
4763
+ * seat, so the panel reads as a bottom-pinned sibling of Settings rather than
4764
+ * a global panel icon at the top of the column.
4765
+ *
4766
+ * The shell wraps nothing here, so this component owns the surface: the
4767
+ * button, its chrome and its accessible name. In the expanded column it draws
4768
+ * the title row, then one block per quota window (5-hour, then weekly) — the
4769
+ * window's own spend and limit (`$1.32 / $6.00`), its percentage and its bar —
4770
+ * and nothing else: the card's figures are the two windows the account runs
4771
+ * into, so the period total stays in the tooltip rather than taking a third
4772
+ * line. In the 56px rail it collapses to a 36px icon button carrying the ring,
4773
+ * matching the shell's own rail geometry. `wide` comes from the shell as an
4774
+ * owner prop — unlike the old `sidebar.panellist` row, this slot really does
4775
+ * supply it.
4776
+ */
4777
+ function CommandCodeFooterEntry(props) {
4778
+ const view = usePanelView(props);
4779
+ const startAutoRefresh = props.startAutoRefresh;
4780
+ (0, react.useEffect)(() => startAutoRefresh(), [startAutoRefresh]);
4781
+ const text = (key) => view.text[key] ?? key;
4782
+ const headline = view.footerBars[0];
4783
+ const title = view.footTitle;
4784
+ if (!props.wide) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4785
+ type: "button",
4786
+ className: "ccp-railButton",
4787
+ "aria-label": title,
4788
+ title,
4789
+ onClick: () => props.open(),
4790
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ring, {
4791
+ percent: headline?.barPercent ?? 0,
4792
+ warn: headline?.warn ?? false,
4793
+ size: 18
4794
+ })
4795
+ });
4796
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4797
+ type: "button",
4798
+ className: "ccp-foot",
4799
+ "aria-label": title,
4800
+ title,
4801
+ onClick: () => props.open(),
4802
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4803
+ className: "ccp-footTop",
4804
+ children: [
4805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ring, {
4806
+ percent: headline?.barPercent ?? 0,
4807
+ warn: headline?.warn ?? false,
4808
+ size: 16
4809
+ }),
4810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4811
+ className: "ccp-footName",
4812
+ children: text("nav")
4813
+ }),
4814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
4815
+ view.status !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4816
+ className: "ccp-badgeMuted",
4817
+ children: view.status
4818
+ }) : view.planName !== text("nav") ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4819
+ className: "ccp-badge",
4820
+ children: view.planName
4821
+ }) : null
4822
+ ]
4823
+ }), view.footerBars.map((bar) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4824
+ className: "ccp-footRow",
4825
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4826
+ className: "ccp-footHead",
4827
+ children: [
4828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4829
+ className: "ccp-footLabel",
4830
+ children: text(bar.label)
4831
+ }),
4832
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "ccp-spacer" }),
4833
+ bar.detail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4834
+ className: "ccp-footAmount",
4835
+ children: bar.detail
4836
+ }) : null,
4837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4838
+ className: "ccp-footPct",
4839
+ children: bar.percent
4840
+ })
4841
+ ]
4842
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4843
+ className: "ccp-footBar",
4844
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4845
+ className: bar.warn ? "ccp-footFill ccp-footFillWarn" : "ccp-footFill",
4846
+ style: { width: `${bar.barPercent}%` }
4847
+ })
4848
+ })]
4849
+ }, bar.label))]
4850
+ });
4851
+ }
4852
+ //#endregion
4853
+ //#region src/cost-facts.ts
4854
+ const zeroCostTokens = () => ({
4855
+ uncachedInputTokens: 0,
4856
+ outputTokens: 0,
4857
+ cacheReadTokens: 0,
4858
+ cacheWriteTokens: 0
4859
+ });
4860
+ const COST_TOKEN_KEYS = [
4861
+ "uncachedInputTokens",
4862
+ "outputTokens",
4863
+ "cacheReadTokens",
4864
+ "cacheWriteTokens"
4865
+ ];
4866
+ function peakHour(at, windows) {
4867
+ const time = new Date(at);
4868
+ return time.getUTCDay() !== 0 && time.getUTCDay() !== 6 && windows.some(([start, end]) => time.getUTCHours() >= start && time.getUTCHours() < end);
4869
+ }
4870
+ /**
4871
+ * All prompt billing buckets determine a request's band, never cumulative session input.
4872
+ *
4873
+ * A tiered model returns the matching band and does NOT then apply `peak`: the
4874
+ * page publishes those two dimensions independently, no row carries both today,
4875
+ * and the generator would report a tiered row on every sync — so the combination
4876
+ * becoming real is visible before it reaches a user. This ordering is a latent
4877
+ * choice, not a verified upstream rule.
4878
+ */
4879
+ function requestRates(price, at, contextTokens, table) {
4880
+ const tier = price.contextTiers?.find((tier) => tier.maxContext === void 0 || contextTokens <= tier.maxContext);
4881
+ if (tier !== void 0) return tier;
4882
+ return price.peak !== void 0 && at !== null && peakHour(at, table.peakHours) ? price.peak : price;
4883
+ }
4884
+ /** Cache version and wire guard: changing bands/rates must refold historical groups. */
4885
+ function pricingKey(table) {
4886
+ const rates = (r) => [
4887
+ r.inputCost,
4888
+ r.outputCost,
4889
+ r.cacheReadCost,
4890
+ r.cacheWriteCost ?? null
4891
+ ];
4892
+ const canonical = JSON.stringify([
4893
+ "commandCodeCost-v1",
4894
+ table.peakHours,
4895
+ table.models.map((p) => [
4896
+ p.id,
4897
+ p.slug,
4898
+ p.free === true,
4899
+ rates(p),
4900
+ p.peak ? rates(p.peak) : null,
4901
+ p.contextTiers?.map((t) => [t.maxContext ?? null, rates(t)]) ?? null
4902
+ ])
4903
+ ]);
4904
+ let hash = 2166136261;
4905
+ for (const c of canonical) hash = Math.imul(hash ^ c.charCodeAt(0), 16777619) >>> 0;
4906
+ return hash;
4907
+ }
4908
+ //#endregion
4909
+ //#region src/client/session-cost.ts
4910
+ /**
4911
+ * Session-cost view model for the composer readout.
4912
+ *
4913
+ * Deliberately JSX-free and React-free, mirroring `./panel.ts`: it turns the
4914
+ * session's token accounting plus the Host's price table into one presentation
4915
+ * value AND the exact text the two surfaces of the harness's token-usage UI
4916
+ * receive — the amount appended to the shipped pill, and the rows appended to
4917
+ * the shipped usage dialog. Node tests therefore drive the whole calculation,
4918
+ * and every user-visible string, without a DOM.
4919
+ *
4920
+ * The figures it prices are the session's DURABLE cumulative buckets, which is
4921
+ * what the composer's own "N tokens" pill reads too (`tokenUsage`), so the two
4922
+ * surfaces can never disagree about how much was used. Dollars are computed
4923
+ * here because Command Code publishes per-token rates and bills against
4924
+ * dollar-denominated windows; the account's own reported `totalCost` is a
4925
+ * billing PERIOD figure, not this session's.
4926
+ *
4927
+ * Three rules are load-bearing:
4928
+ *
4929
+ * 1. **Only Command Code usage is priced.** A session served by another
4930
+ * provider must render nothing, never a Command Code estimate.
4931
+ * 2. **A missing rate is never invented.** The pricing page publishes
4932
+ * input/output/cache-read rates for every model but a cache-WRITE rate for
4933
+ * only some, so unpriced cache-write tokens are surfaced as such rather than
4934
+ * charged at a guessed multiple of the input rate.
4935
+ * 3. **Unpriceable means invisible.** No usage, no model, no table, an unknown
4936
+ * model, or all-zero buckets renders nothing at all — a confident `$0.00`
4937
+ * would be a lie, and this module never returns one.
4938
+ *
4939
+ * @module dsh-commandcode-provider/client/session-cost
4940
+ */
4941
+ /** Tokens per published rate unit — the pricing page quotes USD per million. */
4942
+ const TOKENS_PER_RATE_UNIT = 1e6;
4943
+ /** The provider route whose usage this surface prices. */
4944
+ const COMMANDCODE_PROVIDER = "commandcode";
4945
+ /**
4946
+ * The English copy for this surface.
4947
+ *
4948
+ * A plain constant, NOT the `settings.commandcode` locale namespace: like the
4949
+ * plans & quota panel, the readout stays English on a Chinese harness. Do not
4950
+ * route it through `ctx.locale`.
4951
+ */
4952
+ const SESSION_COST_COPY = {
4953
+ /** Shown instead of an amount when the model costs nothing on every plan. */
4954
+ free: "Free",
4955
+ /**
4956
+ * The separator the readout prefixes itself with, so the cost reads as the
4957
+ * last item of the token-usage pill's text run rather than a control beside
4958
+ * it. Rendered with the same colour and margins the shipped pill uses
4959
+ * between its own items.
4960
+ */
4961
+ separator: "·",
4962
+ /** The heading the tooltip leads with. */
4963
+ panelTitle: "Session cost",
4964
+ /** Marks an estimate from published rates rather than an invoice. */
4965
+ approximate: "≈",
4966
+ /** Tooltip line for the unpriced cache-write tokens. */
4967
+ unpricedCacheWrite: "cache write tokens have no published rate",
4968
+ /** Tooltip line naming the rate half in force. */
4969
+ peakRates: "peak rates",
4970
+ /** Tooltip line naming the rate half in force. */
4971
+ offPeakRates: "off-peak rates",
4972
+ /** Tooltip line explaining the approximate marker. */
4973
+ approximateNote: "estimate from published rates, not the provider invoice",
4974
+ /** Row/tooltip label for uncached prompt tokens. */
4975
+ uncachedInput: "uncached input",
4976
+ /** Row/tooltip label for completion tokens. */
4977
+ output: "output",
4978
+ /** Row/tooltip label for cache-served input tokens. */
4979
+ cacheRead: "cache read",
4980
+ /** Row/tooltip label for cache-written input tokens. */
4981
+ cacheWrite: "cache write"
4982
+ };
4983
+ /** A finite, non-negative count — anything else reads as absent. */
4984
+ function count(value) {
4985
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
4986
+ }
4987
+ /**
4988
+ * Index a price table for lookup. Rows are keyed by catalog id and by pricing
4989
+ * slug, both exact and lowercased, because a session reports a catalog id while
4990
+ * a row no catalog model claims is served under the page's slug.
4991
+ */
4992
+ function indexTable(table) {
4993
+ const index = /* @__PURE__ */ new Map();
4994
+ for (const price of table.models) for (const key of [price.id, price.slug]) {
4995
+ if (typeof key !== "string" || key === "") continue;
4996
+ if (!index.has(key)) index.set(key, price);
4997
+ const lower = key.toLowerCase();
4998
+ if (!index.has(lower)) index.set(lower, price);
4999
+ }
5000
+ return index;
5001
+ }
5002
+ function costOf(usage, rates, free) {
5003
+ if (free) return {
5004
+ uncachedInput: 0,
5005
+ cacheRead: 0,
5006
+ cacheWrite: 0,
5007
+ output: 0,
5008
+ total: 0,
5009
+ unpricedCacheWriteTokens: 0
5010
+ };
5011
+ const perUnit = (tokens, rate) => tokens * rate / TOKENS_PER_RATE_UNIT;
5012
+ const uncachedInput = perUnit(count(usage.uncachedInputTokens), rates.inputCost);
5013
+ const output = perUnit(count(usage.outputTokens), rates.outputCost);
5014
+ const cacheRead = perUnit(count(usage.cacheReadTokens), rates.cacheReadCost);
5015
+ const cacheWriteTokens = count(usage.cacheWriteTokens);
5016
+ const cacheWrite = rates.cacheWriteCost === void 0 ? void 0 : perUnit(cacheWriteTokens, rates.cacheWriteCost);
5017
+ return {
5018
+ uncachedInput,
5019
+ cacheRead,
5020
+ cacheWrite,
5021
+ output,
5022
+ total: uncachedInput + output + cacheRead + (cacheWrite ?? 0),
5023
+ unpricedCacheWriteTokens: rates.cacheWriteCost === void 0 ? cacheWriteTokens : 0
5024
+ };
5025
+ }
5026
+ /**
5027
+ * Amount text for a session cost.
5028
+ *
5029
+ * The panel's `money()` convention (2 decimals, 4 below a cent) is right for
5030
+ * billing windows, but a live session total starts far below a cent, where
5031
+ * `toFixed(4)` would print a flat `$0.0000` — which reads as broken rather than
5032
+ * as small. So a total under $0.0001 is stated as a bound instead.
5033
+ */
5034
+ function sessionCostAmount(total) {
5035
+ if (!Number.isFinite(total) || total <= 0) return formatMoney(0);
5036
+ if (total < 1e-4) return `<$0.0001`;
5037
+ return total < .01 ? formatMoneyExact(total) : formatMoney(total);
5038
+ }
5039
+ /** One `label value` clause of the tooltip. */
5040
+ function clause(label, tokens) {
5041
+ return tokens > 0 ? `${label} ${formatTokensCompact(tokens)}` : void 0;
5042
+ }
5043
+ /**
5044
+ * Build the composer readout, or undefined when there is nothing honest to show.
5045
+ *
5046
+ * Undefined is the correct answer for a session with no usage yet, a session
5047
+ * another provider served, a model the price table does not know, and a table
5048
+ * that has not landed — the pill simply is not there.
5049
+ */
5050
+ function buildSessionCostView(input) {
5051
+ const { usage, table, facts } = input;
5052
+ if (!usage || !table || !facts || facts.pricingKey !== pricingKey(table)) return void 0;
5053
+ const totals = zeroCostTokens();
5054
+ const index = indexTable(table);
5055
+ const breakdown = {
5056
+ uncachedInput: 0,
5057
+ cacheRead: 0,
5058
+ cacheWrite: 0,
5059
+ output: 0,
5060
+ total: 0,
5061
+ unpricedCacheWriteTokens: 0
5062
+ };
5063
+ let free = true;
5064
+ let peak = false;
5065
+ let partial = false;
5066
+ let priced = false;
5067
+ const notes = [];
5068
+ for (const group of facts.groups) {
5069
+ for (const key of COST_TOKEN_KEYS) totals[key] += group.tokens[key];
5070
+ if (group.provider !== COMMANDCODE_PROVIDER) {
5071
+ partial = true;
5072
+ continue;
5073
+ }
5074
+ const price = index.get(group.model);
5075
+ if (!price) {
5076
+ partial = true;
5077
+ continue;
5078
+ }
5079
+ const isFree = price.free === true;
5080
+ free &&= isFree;
5081
+ if (price.peak && group.at === null) {
5082
+ partial = true;
5083
+ notes.push("request time unavailable; off-peak lower bound");
5084
+ }
5085
+ const rates = requestRates(price, group.at, group.contextTokens, table);
5086
+ const part = costOf(group.tokens, rates, isFree);
5087
+ priced ||= isFree || part.total > 0;
5088
+ for (const key of [
5089
+ "uncachedInput",
5090
+ "cacheRead",
5091
+ "output",
5092
+ "total",
5093
+ "unpricedCacheWriteTokens"
5094
+ ]) breakdown[key] += part[key];
5095
+ breakdown.cacheWrite = (breakdown.cacheWrite ?? 0) + (part.cacheWrite ?? 0);
5096
+ if (price.peak) {
5097
+ const isPeak = group.at !== null && peakHour(group.at, table.peakHours);
5098
+ peak ||= isPeak;
5099
+ notes.push(isPeak ? SESSION_COST_COPY.peakRates : SESSION_COST_COPY.offPeakRates);
5100
+ }
5101
+ }
5102
+ if (COST_TOKEN_KEYS.some((key) => totals[key] !== count(usage[key])) || !priced) return void 0;
5103
+ const { total, unpricedCacheWriteTokens } = breakdown;
5104
+ partial ||= unpricedCacheWriteTokens > 0;
5105
+ free &&= !partial;
5106
+ if (partial) notes.push("Command Code priced subtotal only; unpriced or other-provider usage excluded");
5107
+ notes.push(SESSION_COST_COPY.approximateNote);
5108
+ const uncachedInput = totals.uncachedInputTokens;
5109
+ const output = totals.outputTokens;
5110
+ const cacheRead = totals.cacheReadTokens;
5111
+ const cacheWrite = totals.cacheWriteTokens;
5112
+ if (uncachedInput === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return void 0;
5113
+ if (!free && total <= 0) return void 0;
5114
+ const value = free ? SESSION_COST_COPY.free : `${partial ? "≥" : SESSION_COST_COPY.approximate}${sessionCostAmount(total)}`;
5115
+ const money = (amount) => amount === void 0 ? void 0 : sessionCostAmount(amount);
5116
+ const rows = [
5117
+ {
5118
+ key: "uncachedInput",
5119
+ label: SESSION_COST_COPY.uncachedInput,
5120
+ tokens: uncachedInput,
5121
+ costText: money(breakdown.uncachedInput)
5122
+ },
5123
+ {
5124
+ key: "cacheRead",
5125
+ label: SESSION_COST_COPY.cacheRead,
5126
+ tokens: cacheRead,
5127
+ costText: money(breakdown.cacheRead)
5128
+ },
5129
+ {
5130
+ key: "cacheWrite",
5131
+ label: SESSION_COST_COPY.cacheWrite,
5132
+ tokens: cacheWrite,
5133
+ costText: unpricedCacheWriteTokens === cacheWrite && cacheWrite > 0 ? void 0 : money(breakdown.cacheWrite)
5134
+ },
5135
+ {
5136
+ key: "output",
5137
+ label: SESSION_COST_COPY.output,
5138
+ tokens: output,
5139
+ costText: money(breakdown.output)
5140
+ }
5141
+ ];
5142
+ if (unpricedCacheWriteTokens > 0) notes.push(SESSION_COST_COPY.unpricedCacheWrite);
5143
+ return {
5144
+ total,
5145
+ value,
5146
+ title: [
5147
+ `${SESSION_COST_COPY.panelTitle} ${free ? SESSION_COST_COPY.free : sessionCostAmount(total)}`,
5148
+ clause(SESSION_COST_COPY.uncachedInput, uncachedInput),
5149
+ clause(SESSION_COST_COPY.output, output),
5150
+ clause(SESSION_COST_COPY.cacheRead, cacheRead),
5151
+ clause(SESSION_COST_COPY.cacheWrite, cacheWrite),
5152
+ ...notes
5153
+ ].filter((part) => part !== void 0).join(" · "),
5154
+ rows,
5155
+ notes: [...new Set(notes)],
5156
+ free,
5157
+ peak,
5158
+ unpricedCacheWriteTokens,
5159
+ approximate: !free
5160
+ };
5161
+ }
5162
+ /** Split the appended run into the two nodes the display creates. */
5163
+ function sessionCostPillRun(view) {
5164
+ return {
5165
+ separator: SESSION_COST_COPY.separator,
5166
+ value: view.value
5167
+ };
5168
+ }
5169
+ /**
5170
+ * Decorate the harness's usage dialog, row by row.
5171
+ *
5172
+ * The row sequence mirrors the shipped component's conditions exactly — the
5173
+ * cache-hit row exists while there is billed prompt input, the cache-write row
5174
+ * while those tokens are non-zero — because the display layer matches what it
5175
+ * finds positionally. A shape it cannot confirm is a shape it does not touch.
5176
+ *
5177
+ * A free model decorates nothing: every row's cost is zero by definition, the
5178
+ * pill already says `Free`, and a column of `$0.00` would be noise. A bucket
5179
+ * whose rate the page does not publish is likewise left unfilled rather than
5180
+ * filled with an invented number.
5181
+ */
5182
+ function sessionCostRowDecorations(view) {
5183
+ const bucket = (key) => view.rows.find((row) => row.key === key)?.tokens ?? 0;
5184
+ const price = (key) => {
5185
+ if (view.free) return void 0;
5186
+ return view.rows.find((row) => row.key === key)?.costText;
5187
+ };
5188
+ const uncachedInput = bucket("uncachedInput");
5189
+ const cacheRead = bucket("cacheRead");
5190
+ const cacheWrite = bucket("cacheWrite");
5191
+ const output = bucket("output");
5192
+ const plan = [];
5193
+ if (uncachedInput + cacheRead + cacheWrite > 0) plan.push({
5194
+ row: "cacheHit",
5195
+ tokens: void 0,
5196
+ amount: void 0,
5197
+ hidden: false
5198
+ });
5199
+ plan.push({
5200
+ row: "uncachedInput",
5201
+ tokens: uncachedInput,
5202
+ amount: price("uncachedInput"),
5203
+ hidden: false
5204
+ });
5205
+ plan.push({
5206
+ row: "cacheRead",
5207
+ tokens: cacheRead,
5208
+ amount: price("cacheRead"),
5209
+ hidden: false
5210
+ });
5211
+ if (cacheWrite !== 0) {
5212
+ const amount = price("cacheWrite");
5213
+ plan.push({
5214
+ row: "cacheWrite",
5215
+ tokens: cacheWrite,
5216
+ amount,
5217
+ hidden: amount === void 0
5218
+ });
5219
+ }
5220
+ plan.push({
5221
+ row: "output",
5222
+ tokens: output,
5223
+ amount: price("output"),
5224
+ hidden: false
5225
+ });
5226
+ return plan;
5227
+ }
5228
+ //#endregion
5229
+ //#region src/client/session-cost-display.ts
5230
+ /** The shipped composer stats row (dsh-client-ui-chat `StatsPills`). */
5231
+ const STATS_ROOT = "[data-composer-stats]";
5232
+ /**
5233
+ * The shipped token-usage dialog's `<dl>`. Unique in the whole of
5234
+ * `dsh-client-ui-chat`: the per-message turn-usage panel has its own dialog with
5235
+ * different markup, so the composer's is unambiguous.
5236
+ */
5237
+ const USAGE_DIALOG = "[data-session-stats-usage]";
5238
+ /**
5239
+ * The usage dialog's trigger. Both pills announce a dialog, but the token one is
5240
+ * rendered LAST — and when no step carries timing the time pill is not a button
5241
+ * at all, so "last" still selects the token pill in both cases.
5242
+ */
5243
+ const DIALOG_TRIGGER = "button[aria-haspopup=\"dialog\"]";
5244
+ /**
5245
+ * Stable id for the hidden node the appended cost is described by. The pill's
5246
+ * own `aria-label` is computed by the harness on every render and cannot be
5247
+ * extended, so a description is how the cost reaches assistive technology.
5248
+ */
5249
+ const A11Y_ID = "dsh-commandcode-session-cost";
5250
+ /** How many consecutive misses before the missing anchor is worth a console line. */
5251
+ const MISSES_BEFORE_WARNING = 3;
5252
+ /**
5253
+ * The default observer: `childList` on `body`, and deliberately NOT `subtree`, so
5254
+ * streaming text inside the page never wakes it while the panel the harness
5255
+ * portals straight onto `body` does.
5256
+ */
5257
+ const observeBodyChildren = (target, listener) => {
5258
+ if (typeof MutationObserver === "undefined") return void 0;
5259
+ const observer = new MutationObserver(listener);
5260
+ observer.observe(target, { childList: true });
5261
+ return () => observer.disconnect();
5262
+ };
5263
+ /**
5264
+ * Owns the nodes injected into the shipped token-usage UI.
5265
+ *
5266
+ * One instance per mounted entry. `sync()` is cheap and idempotent: it re-reads
5267
+ * both targets on every call, which is what lets it self-heal when React
5268
+ * remounts the row or replaces the dialog, and it rewrites a node only when its
5269
+ * text actually changed.
5270
+ */
5271
+ var SessionCostDisplay = class {
5272
+ doc;
5273
+ scope;
5274
+ observe;
5275
+ view = void 0;
5276
+ /** Detaches the observer installed by `start()`. */
5277
+ detach = void 0;
5278
+ /** The button we appended into, while it is still connected. */
5279
+ pillHost = void 0;
5280
+ /** Our appended root, and the two nodes whose text changes. */
5281
+ pillRoot = void 0;
5282
+ pillValue = void 0;
5283
+ pillA11y = void 0;
5284
+ /** Whether WE set `aria-describedby` on the button (so only we take it back). */
5285
+ described = false;
5286
+ /** The dialog we decorated, the price nodes we own, and the cells we hid. */
5287
+ dialogHost = void 0;
5288
+ dialogPrices = /* @__PURE__ */ new Map();
5289
+ dialogHidden = /* @__PURE__ */ new Set();
5290
+ pillMisses = 0;
5291
+ constructor(options) {
5292
+ this.doc = options.doc;
5293
+ this.scope = options.scope;
5294
+ this.observe = options.observe ?? observeBodyChildren;
5295
+ }
5296
+ /** Begin watching for the shipped usage dialog opening and closing. */
5297
+ start() {
5298
+ if (this.detach !== void 0) return;
5299
+ const body = this.doc.body;
5300
+ if (body === null || body === void 0) return;
5301
+ this.detach = this.observe(body, () => this.apply());
5302
+ }
5303
+ /**
5304
+ * Hand the display the current figure.
5305
+ *
5306
+ * Undefined — nothing priceable about this session — removes everything this
5307
+ * module injected, leaving both shipped surfaces exactly as they ship.
5308
+ */
5309
+ sync(view) {
5310
+ this.view = view;
5311
+ this.apply();
5312
+ }
5313
+ /** Remove every injected node and stop observing. Safe to call twice. */
5314
+ dispose() {
5315
+ this.detach?.();
5316
+ this.detach = void 0;
5317
+ this.removePill();
5318
+ this.clearDialog();
5319
+ this.view = void 0;
5320
+ }
5321
+ /** Re-apply the last known view to whatever both targets are right now. */
5322
+ apply() {
5323
+ this.applyPill();
5324
+ this.applyDialog();
5325
+ }
5326
+ applyPill() {
5327
+ const view = this.view;
5328
+ if (view === void 0) {
5329
+ this.removePill();
5330
+ return;
5331
+ }
5332
+ if (this.pillHost?.isConnected !== true) {
5333
+ const button = this.resolvePillButton();
5334
+ if (button === null) {
5335
+ this.pillMisses += 1;
5336
+ if (this.pillMisses === MISSES_BEFORE_WARNING) console.warn(`[dsh-commandcode-provider] no ${STATS_ROOT} row to append the session cost to; the figure stays in the usage dialog only`);
5337
+ return;
5338
+ }
5339
+ this.pillMisses = 0;
5340
+ this.removePill();
5341
+ this.buildPill(button, sessionCostPillRun(view));
5342
+ }
5343
+ if (this.pillValue !== void 0) {
5344
+ const amount = sessionCostPillRun(view).value;
5345
+ if (this.pillValue.textContent !== amount) this.pillValue.textContent = amount;
5346
+ }
5347
+ if (this.pillRoot !== void 0) {
5348
+ if (this.pillRoot.title !== view.title) this.pillRoot.title = view.title;
5349
+ const approximate = view.approximate ? "true" : null;
5350
+ if (this.pillRoot.getAttribute("data-approximate") !== approximate) {
5351
+ if (approximate === null) this.pillRoot.removeAttribute("data-approximate");
5352
+ else this.pillRoot.setAttribute("data-approximate", approximate);
5353
+ }
5354
+ }
5355
+ if (this.pillA11y !== void 0) {
5356
+ const described = `${SESSION_COST_COPY.panelTitle} ${view.value}`;
5357
+ if (this.pillA11y.textContent !== described) this.pillA11y.textContent = described;
5358
+ }
5359
+ }
5360
+ /** The shipped token pill, scoped to this entry's own composer. */
5361
+ resolvePillButton() {
5362
+ const root = (this.scope() ?? this.doc).querySelector(STATS_ROOT);
5363
+ if (root === null) return null;
5364
+ const triggers = root.querySelectorAll(DIALOG_TRIGGER);
5365
+ return triggers.length === 0 ? null : triggers[triggers.length - 1] ?? null;
5366
+ }
5367
+ /**
5368
+ * Create and append the cost run. The button's child list is static
5369
+ * (`[svg, label]`), so appending once is enough for it to stay last.
5370
+ */
5371
+ buildPill(button, run) {
5372
+ const doc = this.doc;
5373
+ const root = doc.createElement("span");
5374
+ root.setAttribute("data-composer-session-cost", "");
5375
+ const separator = doc.createElement("span");
5376
+ separator.textContent = run.separator;
5377
+ separator.setAttribute("aria-hidden", "true");
5378
+ separator.style.color = "var(--dsw-alias-separator-primary)";
5379
+ separator.style.margin = "0 6px 0 0";
5380
+ const value = doc.createElement("span");
5381
+ value.textContent = run.value;
5382
+ value.style.fontWeight = "500";
5383
+ const a11y = doc.createElement("span");
5384
+ a11y.id = A11Y_ID;
5385
+ a11y.style.display = "none";
5386
+ root.appendChild(separator);
5387
+ root.appendChild(value);
5388
+ root.appendChild(a11y);
5389
+ button.appendChild(root);
5390
+ if (!button.hasAttribute("aria-describedby")) {
5391
+ button.setAttribute("aria-describedby", A11Y_ID);
5392
+ this.described = true;
5393
+ }
5394
+ this.pillHost = button;
5395
+ this.pillRoot = root;
5396
+ this.pillValue = value;
5397
+ this.pillA11y = a11y;
5398
+ }
5399
+ removePill() {
5400
+ if (this.pillRoot !== void 0 && this.pillRoot.parentNode !== null) this.pillRoot.parentNode.removeChild(this.pillRoot);
5401
+ if (this.described) {
5402
+ this.pillHost?.removeAttribute("aria-describedby");
5403
+ this.described = false;
5404
+ }
5405
+ this.pillHost = void 0;
5406
+ this.pillRoot = void 0;
5407
+ this.pillValue = void 0;
5408
+ this.pillA11y = void 0;
5409
+ }
5410
+ applyDialog() {
5411
+ const view = this.view;
5412
+ const host = view === void 0 ? null : this.resolveDialog();
5413
+ if (host === null || view === void 0) {
5414
+ this.clearDialog();
5415
+ return;
5416
+ }
5417
+ this.pruneDialog(host);
5418
+ const plan = sessionCostRowDecorations(view);
5419
+ const pairs = dialogPairs(host);
5420
+ if (!dialogShapeMatches(pairs, plan, this.dialogPrices)) {
5421
+ this.clearDialog();
5422
+ return;
5423
+ }
5424
+ const hidden = /* @__PURE__ */ new Set();
5425
+ for (const [index, row] of plan.entries()) {
5426
+ const pair = pairs[index];
5427
+ if (pair === void 0) continue;
5428
+ const span = this.dialogPrices.get(pair.dd);
5429
+ if (row.hidden) {
5430
+ if (pair.dt.style.display !== "none") pair.dt.style.display = "none";
5431
+ if (pair.dd.style.display !== "none") pair.dd.style.display = "none";
5432
+ hidden.add(pair.dt);
5433
+ hidden.add(pair.dd);
5434
+ this.removeDialogPrice(pair.dd, span);
5435
+ continue;
5436
+ }
5437
+ if (pair.dt.style.display === "none") pair.dt.style.display = "";
5438
+ if (pair.dd.style.display === "none") pair.dd.style.display = "";
5439
+ if (row.amount === void 0) {
5440
+ this.removeDialogPrice(pair.dd, span);
5441
+ continue;
5442
+ }
5443
+ const price = span ?? this.createDialogPrice(pair.dd, row.row);
5444
+ if (price.parentNode !== pair.dd) pair.dd.appendChild(price);
5445
+ if (price.textContent !== row.amount) price.textContent = row.amount;
5446
+ }
5447
+ for (const cell of [...this.dialogHidden]) {
5448
+ if (hidden.has(cell)) continue;
5449
+ if (cell.style.display === "none") cell.style.display = "";
5450
+ this.dialogHidden.delete(cell);
5451
+ }
5452
+ for (const cell of hidden) this.dialogHidden.add(cell);
5453
+ this.dialogHost = host;
5454
+ }
5455
+ /** Forget the cells and prices a re-rendered dialog took with it. */
5456
+ pruneDialog(host) {
5457
+ for (const [dd, price] of [...this.dialogPrices]) {
5458
+ if (dd.parentNode === host) continue;
5459
+ if (price.parentNode !== null) price.parentNode.removeChild(price);
5460
+ this.dialogPrices.delete(dd);
5461
+ }
5462
+ for (const cell of [...this.dialogHidden]) {
5463
+ if (cell.parentNode === host) continue;
5464
+ if (cell.style.display === "none") cell.style.display = "";
5465
+ this.dialogHidden.delete(cell);
5466
+ }
5467
+ }
5468
+ /** The price node for one shipped value cell, appended as its last child. */
5469
+ createDialogPrice(dd, row) {
5470
+ const price = this.doc.createElement("span");
5471
+ price.setAttribute("data-session-cost-price", row);
5472
+ price.style.marginLeft = "6px";
5473
+ price.style.display = "inline-block";
5474
+ price.style.minWidth = "56px";
5475
+ price.style.textAlign = "right";
5476
+ price.style.fontWeight = "500";
5477
+ dd.appendChild(price);
5478
+ this.dialogPrices.set(dd, price);
5479
+ return price;
5480
+ }
5481
+ removeDialogPrice(dd, price) {
5482
+ if (price === void 0) return;
5483
+ if (price.parentNode !== null) price.parentNode.removeChild(price);
5484
+ this.dialogPrices.delete(dd);
5485
+ }
5486
+ /**
5487
+ * The shipped usage dialog, or null while it is closed.
5488
+ *
5489
+ * The dialog is portaled onto `body`, so unlike the pill it cannot be scoped
5490
+ * from this entry; DSH renders one composer, and the attribute is unique in
5491
+ * the chat client, so a document-level lookup is exact. Should a future build
5492
+ * mount two composers at once, both dialogs would describe whichever session
5493
+ * this entry belongs to — noted rather than defended against.
5494
+ */
5495
+ resolveDialog() {
5496
+ if (this.dialogHost?.isConnected === true) return this.dialogHost;
5497
+ this.clearDialog();
5498
+ return this.doc.querySelector(USAGE_DIALOG);
5499
+ }
5500
+ /** Give the dialog back: every price removed, every hidden row restored. */
5501
+ clearDialog() {
5502
+ for (const price of this.dialogPrices.values()) if (price.parentNode !== null) price.parentNode.removeChild(price);
5503
+ this.dialogPrices.clear();
5504
+ for (const cell of this.dialogHidden) if (cell.style.display === "none") cell.style.display = "";
5505
+ this.dialogHidden.clear();
5506
+ this.dialogHost = void 0;
5507
+ }
5508
+ };
5509
+ /** The shipped dialog's `dt`/`dd` pairs, in document order. */
5510
+ function dialogPairs(host) {
5511
+ const pairs = [];
5512
+ let label = null;
5513
+ for (const node of Array.from(host.childNodes)) {
5514
+ const tag = tagNameOf(node);
5515
+ if (tag === "DT") label = node;
5516
+ else if (tag === "DD" && label !== null) {
5517
+ pairs.push({
5518
+ dt: label,
5519
+ dd: node
5520
+ });
5521
+ label = null;
5522
+ }
5523
+ }
5524
+ return pairs;
5525
+ }
5526
+ /** `tagName` upper-cased, or an empty string for a non-element node. */
5527
+ function tagNameOf(node) {
5528
+ const tag = node.tagName;
5529
+ return typeof tag === "string" ? tag.toUpperCase() : "";
5530
+ }
5531
+ /**
5532
+ * Whether the dialog really holds the rows this session's buckets predict.
5533
+ *
5534
+ * The shipped labels belong to the `chat` locale — they are `Uncached input` in
5535
+ * English and something else entirely in Chinese — so rows are matched by
5536
+ * POSITION, and this is what makes that safe: the row count must agree, the
5537
+ * cache-hit row must be the percentage it is, and every other value must carry
5538
+ * exactly the token count of the bucket predicted for it. A mismatch means the
5539
+ * dialog is not what this view describes, and nothing is decorated.
5540
+ */
5541
+ function dialogShapeMatches(pairs, plan, prices) {
5542
+ if (pairs.length !== plan.length) return false;
5543
+ return plan.every((row, index) => {
5544
+ const pair = pairs[index];
5545
+ if (pair === void 0) return false;
5546
+ const text = hostValueText(pair.dd, prices.get(pair.dd));
5547
+ if (row.tokens === void 0) return text.includes("%");
5548
+ return digitsOf(text) === String(row.tokens);
5549
+ });
5550
+ }
5551
+ /**
5552
+ * A value cell's own text, without the price this module appended to it — the
5553
+ * digits of `3,206,544 tok` are the host's, the digits of `$0.07` are ours.
5554
+ */
5555
+ function hostValueText(dd, price) {
5556
+ let text = "";
5557
+ for (const node of Array.from(dd.childNodes)) {
5558
+ if (node === price) continue;
5559
+ text += node.textContent ?? "";
5560
+ }
5561
+ return text;
5562
+ }
5563
+ /** Every digit of a display string, so locale grouping cannot break the match. */
5564
+ function digitsOf(text) {
5565
+ return text.replace(/\D/g, "");
5566
+ }
5567
+ //#endregion
5568
+ //#region src/client/session-cost-view.tsx
5569
+ /** The dock outlet this entry renders inside, i.e. what it can scope itself from. */
5570
+ const DOCK_ANCHOR = "[data-slot=\"conversation.composer.dock\"]";
5571
+ /** Module-level constant so the marker's `style` prop never diffs. */
5572
+ const HIDDEN_STYLE = { display: "none" };
5573
+ /** Log a missing seat once per page, so a silent no-op stays diagnosable. */
5574
+ let warnedMissingSeat = false;
5575
+ /**
5576
+ * The composer's session-cost entry. Guards the two seats before rendering the
5577
+ * mount: a dsh that does not supply the projection seat to dock occupants gets
5578
+ * an absent cost and one console line, never a render crash — which the renderer
5579
+ * would answer by abdicating the entry with no visible trace.
5580
+ */
5581
+ function CommandCodeSessionCost(props) {
5582
+ if (typeof props.useProjection !== "function" || typeof props.useCommandCodePrices !== "function") {
5583
+ if (!warnedMissingSeat) {
5584
+ warnedMissingSeat = true;
5585
+ console.error("[dsh-commandcode-provider] the composer does not supply the projection/hook seats the session-cost readout needs; the readout stays hidden");
5586
+ }
5587
+ return null;
5588
+ }
5589
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionCostEntry, { ...props });
5590
+ }
5591
+ /** The mount: one injection lifetime, one hidden marker. Every hook lives here. */
5592
+ function SessionCostEntry(props) {
5593
+ const markerRef = (0, react.useRef)(null);
5594
+ const displayRef = (0, react.useRef)(null);
5595
+ const prices = props.useCommandCodePrices((state) => state);
5596
+ const view = buildSessionCostView({
5597
+ usage: props.useProjection("tokenUsage"),
5598
+ selection: props.useProjection("modelSelection"),
5599
+ facts: props.useProjection("commandCodeCost"),
5600
+ table: prices.table,
5601
+ now: Date.now()
5602
+ });
5603
+ (0, react.useEffect)(() => {
5604
+ if (typeof document === "undefined") return;
5605
+ const display = new SessionCostDisplay({
5606
+ doc: document,
5607
+ scope: () => markerRef.current?.closest(DOCK_ANCHOR)?.parentElement ?? null
5608
+ });
5609
+ displayRef.current = display;
5610
+ display.start();
5611
+ return () => {
5612
+ displayRef.current = null;
5613
+ display.dispose();
5614
+ };
5615
+ }, []);
5616
+ (0, react.useEffect)(() => {
5617
+ displayRef.current?.sync(view);
5618
+ });
5619
+ if (view === void 0) return null;
5620
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5621
+ ref: markerRef,
5622
+ style: HIDDEN_STYLE,
5623
+ "data-ccp-session-cost-anchor": ""
5624
+ });
5625
+ }
5626
+ //#endregion
5627
+ //#region src/client/panel-styles.ts
5628
+ /**
5629
+ * Stylesheet for the Command Code plans & quota panel (the sidebar footer card
5630
+ * and the dashboard it opens).
5631
+ *
5632
+ * Returned as a string rather than injected here so the modules stay free of
5633
+ * DOM side effects at import time — the client entry installs it once, keyed
5634
+ * by the same `data-plugin-css` attribute the settings-page stylesheet uses,
5635
+ * and removes it again when the plugin's fiber unwinds.
5636
+ *
5637
+ * Every colour comes from a harness theme alias with a neutral fallback, so
5638
+ * the panel follows the active theme (light/dark and any brand pack) without
5639
+ * hardcoded values. Classes are `ccp-` prefixed to stay clear of the settings
5640
+ * page's `cc-` set.
5641
+ *
5642
+ * @module dsh-commandcode-provider/client/panel-styles
5643
+ */
5644
+ /**
5645
+ * Stylesheet id (the `data-plugin-css` value that makes injection idempotent).
5646
+ *
5647
+ * The package prefix must match the one the settings-page stylesheet uses
5648
+ * (`injectPageCss` in `./index.ts`) and this package's real name: an id is the
5649
+ * injection's identity, so a stale fork prefix would let a second copy of the
5650
+ * plugin inject the same rules twice and would misreport the owner in the DOM.
5651
+ */
5652
+ const PANEL_CSS_ID = "@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css";
5653
+ /** The panel stylesheet. */
5654
+ const PANEL_CSS = `
5655
+ /* ------------------------------------------------- sidebar footer card */
5656
+ /* The shell's foot area renders this list ABOVE the Settings seat, so the card
5657
+ is the sidebar's bottom-most content. The shell supplies no chrome: the entry
5658
+ is the button. It is deliberately quiet — a surface that sits beside Settings
5659
+ should read as part of the column, not as a call to action — with one hover
5660
+ step and a hairline border.
5661
+
5662
+ The shell's container is a flex ROW whose occupants (this card and ui-cordis's
5663
+ footer chip) each declare a full-width line and shrink-proof flex, so as a row
5664
+ it would overflow the column. Both were written for a full-width line, which
5665
+ is exactly what a column gives them. Matched by the CSS-module class STEM —
5666
+ never a hashed name — so a dsh that renames it degrades to the shell's own
5667
+ row rather than breaking. */
5668
+ [class*="_footerActions"]{flex-direction:column}
5669
+ .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}
5670
+ .ccp-foot:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
5671
+ .ccp-foot:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
5672
+ .ccp-footTop{align-items:center;gap:8px;min-width:0;display:flex}
5673
+ .ccp-footName{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);min-width:0;overflow:hidden;font-size:13px;font-weight:500;line-height:20px}
5674
+ /* One block per quota window: a head line carrying the window's own spend and
5675
+ limit, then the FULL-WIDTH bar under it. Stacking the two lets the card show
5676
+ the dollar figures — the reason this surface exists — without squeezing the
5677
+ bar into what is left beside them. Mirrors the dashboard's own window block. */
5678
+ .ccp-footRow{flex-direction:column;gap:4px;min-width:0;display:flex}
5679
+ .ccp-footHead{align-items:baseline;gap:8px;min-width:0;display:flex}
5680
+ .ccp-footLabel{flex:1;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
5681
+ .ccp-footAmount{flex:none;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums;white-space:nowrap}
5682
+ /* The card's markup must stay PHRASING content — it renders inside the shell's
5683
+ own button — so these bars are spans, not divs. That makes display:block
5684
+ load-bearing on BOTH: an inline box ignores width and height outright, so
5685
+ without it the 5px track still painted (a flex item is blockified by its
5686
+ container) while the fill collapsed to 0x0 and the bar showed no usage. */
5687
+ .ccp-footBar{display:block;background:var(--dsw-alias-bg-layer-2);border-radius:999px;height:5px;overflow:hidden}
5688
+ .ccp-footFill{display:block;background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
5689
+ .ccp-footFillWarn{background:var(--dsw-alias-state-error-primary)}
5690
+ .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}
5691
+
5692
+ /* The 56px rail: one icon button on the shell's own rail geometry (36px cell),
5693
+ so the collapsed column keeps a single 18px glyph like its siblings. */
5694
+ .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}
5695
+ .ccp-railButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}
5696
+ .ccp-railButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
5697
+
5698
+ /* The ring glyph. Sized entirely by its own width/height attribute, so the
5699
+ footer row, the rail button and the dashboard can each ask for their own. */
5700
+ .ccp-glyph{flex:none;justify-content:center;align-items:center;display:inline-flex;color:var(--dsw-alias-brand-primary)}
5701
+
5702
+ /* ------------------------------------------------------------ dashboard */
5703
+ /* The center column in the layout frame: fill it, scroll the content column,
5704
+ and cap the reading width like the harness's own panels. */
5705
+ .ccp-main{background:var(--dsw-alias-bg-layer-1);width:100%;height:100%;overflow:auto;display:block}
5706
+ .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)}
5707
+ .ccp-header{align-items:center;gap:10px;display:flex;flex-wrap:wrap}
5708
+ .ccp-headerText{flex-direction:column;gap:2px;display:flex;min-width:0}
5709
+ .ccp-title{margin:0;font-size:18px;font-weight:600;line-height:1.4}
5710
+ .ccp-subtitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
5711
+ .ccp-spacer{flex:1}
5712
+ .ccp-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;font-variant-numeric:tabular-nums}
5713
+ .ccp-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
5714
+
5715
+ /* Notices: the no-key guidance, a blocked report, and a stale-data error. */
5716
+ .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}
5717
+ .ccp-noticeError{border-color:var(--dsw-alias-state-error-primary)}
5718
+ .ccp-noticeTitle{margin:0;font-size:13px;font-weight:600;line-height:1.5}
5719
+ .ccp-noticeError .ccp-noticeTitle{color:var(--dsw-alias-state-error-primary)}
5720
+ .ccp-noticeHint{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}
5721
+ .ccp-noticeDetail{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;word-break:break-word}
5722
+
5723
+ .ccp-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:14px;padding:16px 18px;flex-direction:column;gap:16px;display:flex}
5724
+ .ccp-cardHead{align-items:center;gap:10px;display:flex;flex-wrap:wrap}
5725
+ .ccp-avatar{flex:none;width:28px;height:28px;color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-module-platform);border-radius:50%;justify-content:center;align-items:center;font-size:12px;font-weight:600;line-height:1;display:inline-flex}
5726
+ .ccp-cardIdentity{flex-direction:column;gap:1px;min-width:0;display:flex}
5727
+ .ccp-cardTitle{font-size:13px;font-weight:600;line-height:1.4}
5728
+ .ccp-cardOwner{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px}
5729
+ .ccp-block{flex-direction:column;gap:8px;display:flex}
5730
+ .ccp-blockTitle{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:600;line-height:1.5;text-transform:uppercase;letter-spacing:.04em}
5731
+ .ccp-planRow{align-items:center;gap:8px;display:flex;flex-wrap:wrap}
5732
+ .ccp-fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
5733
+ .ccp-planName{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:1.5}
5734
+
5735
+ /* Stat tiles: the monthly credits and the usage totals share one grid. */
5736
+ .ccp-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
5737
+ .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}
5738
+ .ccp-tileLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}
5739
+ .ccp-tileValue{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4;font-variant-numeric:tabular-nums}
5740
+ .ccp-tileSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
5741
+
5742
+ /* Quota bars: the monthly bar and the two windows stack in one column, each a
5743
+ label row plus the track. */
5744
+ .ccp-windows{flex-direction:column;gap:14px;display:flex}
5745
+ .ccp-window{flex-direction:column;gap:6px;display:flex}
5746
+ .ccp-windowHead{align-items:baseline;gap:8px;display:flex}
5747
+ .ccp-windowLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:1.5}
5748
+ .ccp-windowValue{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;font-variant-numeric:tabular-nums;white-space:nowrap}
5749
+ .ccp-windowPct{color:var(--dsw-alias-label-primary);min-width:38px;text-align:right;font-size:12px;font-weight:600;line-height:1.5;font-variant-numeric:tabular-nums}
5750
+ .ccp-warnTag{white-space:nowrap;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));border-radius:999px;padding:0 8px;font-size:11px;font-weight:600;line-height:17px}
5751
+ .ccp-bar{overflow:hidden;background:var(--dsw-alias-bg-layer-1);border-radius:999px;height:8px}
5752
+ .ccp-barFill{background:var(--dsw-alias-brand-primary);border-radius:999px;height:100%;transition:width .3s ease}
5753
+ .ccp-barFillWarn{background:var(--dsw-alias-state-error-primary)}
5754
+ .ccp-windowReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:1.5}
5755
+
5756
+ /* Badges. */
5757
+ .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}
5758
+ .ccp-badgeError{background:transparent;color:var(--dsw-alias-state-error-primary)}
5759
+ .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))}
5760
+ .ccp-badgeMuted{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;max-width:220px;overflow:hidden;text-overflow:ellipsis}
5761
+
5762
+ /* Account switch: plain buttons, like the settings page's usage carousel. */
5763
+ .ccp-tabs{flex-wrap:wrap;gap:6px;display:flex}
5764
+ .ccp-tab{align-items:center;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:2px 10px;font-size:12px;line-height:18px;display:inline-flex;gap:6px}
5765
+ .ccp-tab:hover:not(.ccp-tabActive){color:var(--dsw-alias-label-primary)}
5766
+ .ccp-tabActive{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary)}
5767
+
5768
+ @media (prefers-reduced-motion:reduce){.ccp-footFill,.ccp-barFill{transition:none}}
5769
+ `;
5770
+ //#endregion
5771
+ //#region src/client/locales.ts
5772
+ const zh = {
5773
+ nav: "Command Code",
5774
+ title: "Command Code",
5775
+ intro: "配置 Command Code Provider 连接。API 密钥仅保存在本机凭据服务中,不会回显;其他字段写入用户设置,下次请求即生效。",
5776
+ apiKey: "API 密钥",
5777
+ apiKeyHint: "在 commandcode.ai 控制台创建。留空保存不会覆盖已存储的密钥。",
5778
+ apiKeySet: "已配置",
5779
+ apiKeyUnset: "未配置",
5780
+ apiKeyLocked: "密钥由只读来源提供",
5781
+ apiBase: "API 地址",
5782
+ apiBaseHint: "默认 https://api.commandcode.ai,一般无需修改。",
5783
+ workingDir: "工作目录",
5784
+ workingDirHint: "可选。留空时使用占位符显示的进程工作目录;仅在需要固定路径时填写。",
5785
+ requestTimeoutMs: "请求超时(毫秒)",
5786
+ requestTimeoutMsHint: "等待响应首个字节的超时;默认 60000。",
5787
+ streamIdleTimeoutMs: "流空闲超时(毫秒)",
5788
+ streamIdleTimeoutMsHint: "生成流停滞多久视为断连;默认 300000(长思考模型可静默数分钟,默认值刻意放宽)。",
5789
+ advancedSettings: "高级设置",
5790
+ advancedSettingsHint: "API 地址、工作目录、超时与模型过滤等不常修改的选项。",
5791
+ advancedOverriddenOne: "已自定义 1 项",
5792
+ advancedOverriddenMany: "已自定义 {count} 项",
5793
+ advancedInvalid: "高级设置中有未填好的数字,请展开修正后再保存。",
5794
+ filterModelsByPlan: "隐藏套餐外模型",
5795
+ filterModelsByPlanHint: "开启后,模型选择器只列出当前套餐可用的模型;账户持有按需余额时会显示全部。",
5796
+ webSearch: "用 Command Code 承载联网搜索",
5797
+ webSearchHint: "开启后,dsh 的 web_search 工具由 Command Code 承担(复用同一个 API key 与地址),并优先于其他搜索后端;关闭则把选择权交还给之前的后端(如 modsearch),而不是强制回退到 DeepSeek 搜索。",
5798
+ accountsTitle: "多账户轮换",
5799
+ accountsHint: "当前账户达到用量限额(429)或密钥失效(401)时,请求自动切换到下一个账户;全部耗尽时会提示最早的重置时间。",
5800
+ accountAdd: "添加账户",
5801
+ accountRemove: "移除",
5802
+ accountLabel: "账户备注名",
5803
+ accountKey: "API 密钥",
5804
+ accountKeyHint: "该账户的 API 密钥。留空保存不会覆盖已存储的密钥。",
5805
+ accountDefault: "默认账户",
5806
+ activeAccount: "当前使用账户",
5807
+ activeAccountAuto: "自动(第一个可用账户)",
5808
+ activeAccountHint: "手动指定优先使用的账户,保存后下次请求即生效;所选账户耗尽时仍会自动切换到其他可用账户。",
5809
+ rulesTitle: "按模型切换账户",
5810
+ rulesHint: "选择模型并路由到某个账户(可多选)。命中规则的模型且该账户可用时优先使用;账户耗尽或密钥失效时仍自动回落到其他账户。规则按列表顺序匹配,第一条命中生效。",
5811
+ rulesEmpty: "尚未配置规则。",
5812
+ rulesCatalogFailed: "模型目录获取失败,暂时无法选择模型;已保存的规则仍会生效。",
5813
+ ruleAdd: "添加规则",
5814
+ ruleRemove: "移除",
5815
+ ruleModel: "模型",
5816
+ ruleModelPick: "选择模型…",
5817
+ ruleModelCount: "已选 {count} 个模型",
5818
+ ruleAccount: "目标账户",
5819
+ ruleHint: "从下拉列表勾选要路由的模型(可多选),再选择目标账户。",
5820
+ modelSearchPlaceholder: "搜索模型…",
5821
+ modelSearchEmpty: "没有匹配的模型。",
5822
+ modelStale: "已下架",
5823
+ visibleModelsTitle: "模型白名单",
5824
+ visibleModelsHint: "勾选要保留的模型,模型选择器就只列出这些;一个都不勾选时则显示全部模型。保存后,下次打开模型选择器生效。",
5825
+ visibleModelsPick: "选择要保留的模型…",
5826
+ visibleModelsCount: "已选 {count} 个模型",
5827
+ visibleModelsShowAll: "显示全部",
5828
+ visibleModelsStaleHint: "有 {count} 个已选模型在目录中找不到了(可能已下架),不影响其他模型;可清理或保留。",
5829
+ visibleModelsCleanStale: "清理失效({count})",
5830
+ overridden: "已覆盖",
5831
+ reset: "重置",
5832
+ invalidNumber: "无效数字",
5833
+ numberTooSmall: "不能小于 1(毫秒)",
5834
+ numberTooLarge: "超出允许上限(2147483647 毫秒)",
5835
+ readOnly: "当前配置为只读。",
5836
+ unsaved: "未保存",
5837
+ save: "保存",
5838
+ saving: "保存中",
5839
+ saved: "已保存 ✓",
5840
+ saveFailed: "保存失败,请重试。",
5841
+ discard: "放弃",
5842
+ cancel: "取消",
5843
+ show: "显示",
5844
+ hide: "隐藏",
5845
+ usageTitle: "账户用量",
5846
+ usageRefresh: "刷新",
5847
+ usageRefreshing: "刷新中…",
5848
+ usageLoading: "正在获取账户用量…",
5849
+ usageNoKey: "配置 API 密钥后,这里会显示账户的用量与额度状态。",
5850
+ usageError: "用量获取失败",
5851
+ usageRequests: "请求",
5852
+ usageFailed: "失败",
5853
+ usageSuccessRate: "成功率",
5854
+ usageCost: "花费",
5855
+ usageTokens: "Token",
5856
+ usageTokensIn: "入",
5857
+ usageTokensOut: "出",
5858
+ usageMonthly: "月额度",
5859
+ usagePurchased: "已购",
5860
+ usageFree: "赠送",
5861
+ usageFiveHour: "5 小时窗口",
5862
+ usageWeekly: "每周窗口",
5863
+ usageExceeded: "已超限",
5864
+ usageReset: "重置于",
5865
+ usagePartial: "部分端点数据不可用",
5866
+ usageKeyClear: "清除已存密钥",
5867
+ usageKeyClearStaged: "将清除(保存后生效)",
5868
+ usageUndoKeyClear: "撤销清除",
5869
+ usageKeyInvalid: "API 密钥无效或已过期",
5870
+ usageKeyInvalidHint: "服务端拒绝了全部请求(401)。请检查该账户配置的密钥,或到 commandcode.ai 控制台重新生成。",
5871
+ usageServiceUnavailable: "Command Code 服务暂时不可用",
5872
+ usageServiceUnavailableHint: "服务端返回了错误(5xx),稍后点击刷新重试。",
5873
+ usageNetworkError: "无法连接 Command Code 服务",
5874
+ usageNetworkHint: "所有请求都没有到达服务端。请检查网络连接或 API 地址设置。",
5875
+ usageUpdated: "更新于",
5876
+ usagePeriodEnd: "账期截止",
5877
+ usageActive: "当前使用",
5878
+ usageCooldown: "限额冷却中",
5879
+ usageInvalidKey: "密钥无效",
5880
+ usageUnconfigured: "该账户尚未配置 API 密钥。",
5881
+ updateAvailable: "可更新",
5882
+ updateHint: "已发布新版本,点击查看发布说明;更新插件后刷新本页,提示会自动消失。",
5883
+ loginTitle: "通过官方登录获取密钥",
5884
+ loginHintIdle: "不想手动创建密钥?点击登录后浏览器会打开 commandcode.ai 授权页,完成后密钥自动写入本机凭据服务,下次请求即生效。",
5885
+ loginButton: "登录 Command Code",
5886
+ loginStarting: "正在启动本地回调服务…",
5887
+ loginWaiting: "等待在浏览器中完成授权…",
5888
+ loginOpenLink: "打开授权页面 ↗",
5889
+ loginCancel: "取消登录",
5890
+ loginSuccess: "已登录为",
5891
+ loginUnavailable: "此环境暂不支持登录流程,请手动粘贴密钥。",
5892
+ loginDenied: "授权被拒绝。可重试,或手动粘贴密钥。",
5893
+ loginTimeout: "等待超时:未在窗口期内收到授权回调,请重试。",
5894
+ loginInvalidKey: "获取到的密钥未通过校验(401),请重试或手动粘贴。",
5895
+ loginNetwork: "无法连接 Command Code 服务校验密钥,请检查网络后重试。",
5896
+ loginStoreFailed: "密钥无法写入本机凭据服务,请手动粘贴。",
5897
+ loginCancelled: "登录已取消。",
5898
+ loginFailedGeneric: "登录失败,请重试或手动粘贴密钥。",
5899
+ cardTitle: "Command Code",
5900
+ cardRouteActive: "已启用",
5901
+ cardLoadingHint: "正在读取 Command Code 配置…",
5902
+ cardRegistrationHint: "此卡片随 Command Code 插件注册,需要较新版本的 DeepSeek Harness 才会显示完整内容。"
5903
+ };
5904
+ const en = {
5905
+ nav: "Command Code",
5906
+ title: "Command Code",
5907
+ intro: "Configure the Command Code Provider connection. The API key is stored only in the local credential service and never echoed; other fields are written to user settings and take effect on the next request.",
5908
+ apiKey: "API key",
5909
+ apiKeyHint: "Create one in the commandcode.ai console. Saving with this field blank keeps the stored key.",
5910
+ apiKeySet: "Configured",
5911
+ apiKeyUnset: "Not configured",
5912
+ apiKeyLocked: "Key provided by a read-only source",
5913
+ apiBase: "API base URL",
5914
+ apiBaseHint: "Defaults to https://api.commandcode.ai; usually leave as-is.",
5915
+ workingDir: "Working directory",
5916
+ workingDirHint: "Optional. Leave blank to use the process cwd shown as the placeholder; fill in only to pin a specific path.",
5917
+ requestTimeoutMs: "Request timeout (ms)",
5918
+ requestTimeoutMsHint: "Time to wait for the first response byte; default 60000.",
5919
+ streamIdleTimeoutMs: "Stream idle timeout (ms)",
5920
+ streamIdleTimeoutMsHint: "How long a stalled stream is treated as dead; default 300000 (deliberately generous — long-thinking models can stay silent for minutes).",
5921
+ advancedSettings: "Advanced",
5922
+ advancedSettingsHint: "Rarely touched options: API base URL, working directory, timeouts, and model filtering.",
5923
+ advancedOverriddenOne: "1 customized",
5924
+ advancedOverriddenMany: "{count} customized",
5925
+ advancedInvalid: "A number in Advanced settings is not ready to save; expand to fix it.",
5926
+ filterModelsByPlan: "Hide out-of-plan models",
5927
+ filterModelsByPlanHint: "When on, the model picker lists only models your subscription includes; any on-demand credit balance shows the full catalog.",
5928
+ webSearch: "Serve dsh web search with Command Code",
5929
+ webSearchHint: "When on, the model-facing web_search tool is backed by Command Code (same API key and base URL as chat), winning over other search backends. Off hands the selection back to the previous backend (e.g. modsearch) instead of forcing the shipped DeepSeek search.",
5930
+ accountsTitle: "Account rotation",
5931
+ accountsHint: "When the active account hits its usage limit (429) or its key fails (401), requests switch to the next account; when every account is exhausted the error names the earliest window reset.",
5932
+ accountAdd: "Add account",
5933
+ accountRemove: "Remove",
5934
+ accountLabel: "Account label",
5935
+ accountKey: "API key",
5936
+ accountKeyHint: "This account’s API key. Saving with the field blank keeps the stored key.",
3897
5937
  accountDefault: "Default account",
3898
5938
  activeAccount: "Active account",
3899
5939
  activeAccountAuto: "Auto (first usable account)",
@@ -4134,6 +6174,38 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
4134
6174
  document.head.appendChild(tag);
4135
6175
  }
4136
6176
  /**
6177
+ * Install the plans & quota panel's stylesheet and return its disposer, for
6178
+ * `ctx.effect` to own. Keyed by its own `data-plugin-css` id, so the injection
6179
+ * is idempotent even if a second surface asks for it later.
6180
+ */
6181
+ function injectPanelCss() {
6182
+ if (typeof document === "undefined") return () => {};
6183
+ if (document.querySelector(`style[data-plugin-css="@mars-sea/dsh-commandcode-provider/CommandCodePanel.module.css"]`) !== null) return () => {};
6184
+ const tag = document.createElement("style");
6185
+ tag.dataset.plugin = "@mars-sea/dsh-commandcode-provider";
6186
+ tag.dataset.pluginCss = PANEL_CSS_ID;
6187
+ tag.textContent = PANEL_CSS;
6188
+ document.head.appendChild(tag);
6189
+ return () => {
6190
+ tag.remove();
6191
+ };
6192
+ }
6193
+ /**
6194
+ * Plans & quota panel id. It is the layout's `MainPanelId`: one string shared
6195
+ * by the `sidebar.footer.action` card and the `main` slot cell, so the card
6196
+ * selects this panel and nothing else. `dsh-client-ui-layout` is not a
6197
+ * dependency of this bundle (its type is only a brand over `string`), so the
6198
+ * brand is applied at the call site instead of importing the package.
6199
+ */
6200
+ const PANEL_ID = "commandcode-panel";
6201
+ /**
6202
+ * The composer figure's entry id in `conversation.composer.dock`. Its own id,
6203
+ * not the shipped `stats` cell's: reusing `stats` would REPLACE the tokens /
6204
+ * cache-hit / throughput readout rather than inject into it, and that readout
6205
+ * is the harness's to format (see `./session-cost-display.ts`).
6206
+ */
6207
+ const SESSION_COST_ID = "commandcode-session-cost";
6208
+ /**
4137
6209
  * Client plugin body. Gates on the services shared by both client generations
4138
6210
  * (`slots`, `locale`, `connection`, `remote`, `settingsScope`). Current clients
4139
6211
  * mount the page after `remote.credentials` appears; legacy clients mount it
@@ -4176,11 +6248,13 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
4176
6248
  }), "dsh-commandcode-provider: credential invalidations");
4177
6249
  let usageNamespace;
4178
6250
  let usageMountError;
6251
+ let pricesController;
4179
6252
  const contribution = {
4180
6253
  package: USAGE_REMOTE_CONTRIBUTION.package,
4181
6254
  descriptors: [
4182
6255
  ...USAGE_REMOTE_CONTRIBUTION.descriptors,
4183
6256
  ...MODELS_REMOTE_CONTRIBUTION.descriptors,
6257
+ ...PRICES_REMOTE_CONTRIBUTION.descriptors,
4184
6258
  ...LOGIN_REMOTE_CONTRIBUTION.descriptors
4185
6259
  ]
4186
6260
  };
@@ -4196,6 +6270,7 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
4196
6270
  ctx.inject(["remote.commandcode"], (namespaceCtx) => {
4197
6271
  usageNamespace = namespaceCtx.remote.commandcode;
4198
6272
  controller.refreshCatalog();
6273
+ pricesController?.reload();
4199
6274
  namespaceCtx.effect(() => () => {
4200
6275
  usageNamespace = void 0;
4201
6276
  }, "dsh-commandcode-provider: usage namespace");
@@ -4225,6 +6300,22 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
4225
6300
  error: { message: usageMountError ?? "commandcode/models remote is not mounted" }
4226
6301
  };
4227
6302
  return namespace.models();
6303
+ },
6304
+ prices: async () => {
6305
+ const namespace = usageNamespace;
6306
+ if (namespace === void 0) return {
6307
+ ok: false,
6308
+ error: { message: usageMountError ?? "commandcode/prices remote is not mounted" }
6309
+ };
6310
+ const call = namespace.prices;
6311
+ if (typeof call !== "function") return {
6312
+ ok: false,
6313
+ error: {
6314
+ message: "the Host serves no commandcode/prices endpoint",
6315
+ permanent: true
6316
+ }
6317
+ };
6318
+ return call.call(namespace);
4228
6319
  }
4229
6320
  };
4230
6321
  modelsRemote = () => usageRemote.models();
@@ -4232,6 +6323,10 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
4232
6323
  ctx.effect(() => () => usageController.dispose(), "dsh-commandcode-provider: usage controller");
4233
6324
  const usageStore = createSnapshotStore(usageController.state());
4234
6325
  usageController.subscribe(() => usageStore.set(usageController.state()));
6326
+ pricesController = new CommandCodePricesController(usageRemote);
6327
+ ctx.effect(() => () => pricesController?.dispose(), "dsh-commandcode-provider: price table");
6328
+ const pricesStore = createSnapshotStore(pricesController.state());
6329
+ pricesController.subscribe(() => pricesStore.set(pricesController.state()));
4235
6330
  const loginRemote = {
4236
6331
  loginBegin: async () => {
4237
6332
  if (usageNamespace === void 0) return {
@@ -4323,6 +6418,54 @@ select.cc-input{appearance:none;-webkit-appearance:none;-moz-appearance:none;box
4323
6418
  cancelLogin: () => void loginController.cancel()
4324
6419
  })
4325
6420
  }, CommandCodeProviderCard));
6421
+ const panelFace = () => ({
6422
+ hooks: {
6423
+ commandCodeUsage: usageStore,
6424
+ commandCodeSettings: store
6425
+ },
6426
+ refresh: () => {
6427
+ pricesController?.ensure();
6428
+ usageController.refresh();
6429
+ },
6430
+ startAutoRefresh: () => startPanelAutoRefresh(usageController, () => true),
6431
+ open: () => {
6432
+ const layout = ctx.get("layout");
6433
+ if (typeof layout?.selectPanel === "function") layout.selectPanel(PANEL_ID);
6434
+ }
6435
+ });
6436
+ ctx.effect(() => injectPanelCss(), "dsh-commandcode-provider: panel styles");
6437
+ try {
6438
+ ctx.slots.inject("main", () => ctx.slots.register({
6439
+ name: "main",
6440
+ key: PANEL_ID,
6441
+ inject: panelFace
6442
+ }, CommandCodePanel));
6443
+ } catch (error) {
6444
+ console.error("[dsh-commandcode-provider] could not register the plans & quota panel:", error);
6445
+ }
6446
+ ctx.inject(["layout"], (layoutCtx) => {
6447
+ if (typeof layoutCtx.get("layout")?.selectPanel !== "function") return;
6448
+ try {
6449
+ layoutCtx.slots.inject("sidebar.footer.action", () => layoutCtx.slots.register({
6450
+ name: "sidebar.footer.action",
6451
+ id: PANEL_ID,
6452
+ order: 1,
6453
+ inject: panelFace
6454
+ }, CommandCodeFooterEntry));
6455
+ } catch (error) {
6456
+ console.error("[dsh-commandcode-provider] could not register the sidebar footer card:", error);
6457
+ }
6458
+ });
6459
+ const sessionCostFace = () => ({ hooks: { commandCodePrices: pricesStore } });
6460
+ try {
6461
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
6462
+ name: "conversation.composer.dock",
6463
+ id: SESSION_COST_ID,
6464
+ inject: sessionCostFace
6465
+ }, CommandCodeSessionCost));
6466
+ } catch (error) {
6467
+ console.error("[dsh-commandcode-provider] could not register the composer session-cost readout:", error);
6468
+ }
4326
6469
  }
4327
6470
  const inject = [
4328
6471
  "slots",