@ychris12138/dsh-usage-stats 0.2.10 → 0.3.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
@@ -31,6 +31,15 @@ window.__ModuleLoader__.load({
31
31
  ".usg_badgeOk{color:var(--dsw-alias-state-success-primary)}",
32
32
  ".usg_badgeBad{color:var(--dsw-alias-state-error-primary)}",
33
33
  ".usg_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}",
34
+ ".usg_sessionPill{box-sizing:border-box;max-width:220px;height:28px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-fill-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;align-items:center;gap:5px;padding:0 8px;font:inherit;font-size:11px;line-height:16px;cursor:pointer;display:inline-flex;white-space:nowrap;overflow:hidden}",
35
+ ".usg_sessionPill:hover{background:var(--dsw-alias-interactive-bg-hover)}",
36
+ ".usg_sessionPill:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
37
+ ".usg_sessionPill[data-tone=normal]{color:var(--dsw-alias-label-secondary)}",
38
+ ".usg_sessionPill[data-tone=warning]{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));border-color:var(--dsw-alias-state-warn-secondary,var(--dsw-alias-border-l2))}",
39
+ ".usg_sessionPill[data-tone=critical]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary,var(--dsw-alias-border-l2))}",
40
+ ".usg_sessionPillProvider{max-width:88px;color:var(--dsw-alias-label-primary);font-weight:500;text-overflow:ellipsis;overflow:hidden}",
41
+ ".usg_sessionPillSeparator{color:var(--dsw-alias-label-caption);flex:none}",
42
+ ".usg_sessionPillValue{max-width:112px;font-variant-numeric:tabular-nums;text-overflow:ellipsis;overflow:hidden}",
34
43
  ".usg_layer.usg_rail{width:36px;height:36px;margin:0}",
35
44
  ".usg_layer.usg_rail .usg_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}",
36
45
  ".usg_layer.usg_rail .usg_footerButtons{flex-direction:column;gap:2px}",
@@ -59,6 +68,9 @@ window.__ModuleLoader__.load({
59
68
  ".usg_providerPickerLabel{color:var(--dsw-alias-label-tertiary);flex:none}",
60
69
  ".usg_providerSelect{box-sizing:border-box;min-width:0;flex:1;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-base));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 6px;font:inherit;font-size:12px;line-height:18px}",
61
70
  ".usg_accountGrid{flex-direction:column;gap:8px;display:flex}",
71
+ ".usg_exportLinks{display:flex;flex-wrap:wrap;gap:6px}",
72
+ ".usg_exportLink{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:7px;padding:4px 7px;font-size:11px;line-height:16px;text-decoration:none}",
73
+ ".usg_exportLink:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
62
74
  ".usg_accountCard{--usg-providerAccent:#1f6feb;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:linear-gradient(135deg,color-mix(in srgb,var(--usg-providerAccent) 8%,transparent),transparent 42%);border-radius:12px;padding:10px 11px;display:flex;flex-direction:column;gap:9px}",
63
75
  ".usg_accountCard[data-provider=deepseek],.usg_accountCard[data-provider=deepseek-official]{--usg-providerAccent:#1f6feb}",
64
76
  ".usg_accountCard[data-provider=opencode-go]{--usg-providerAccent:#00a67d}",
@@ -73,6 +85,9 @@ window.__ModuleLoader__.load({
73
85
  ".usg_accountPlan{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:14px;overflow:hidden}",
74
86
  ".usg_accountStatus{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-fill-l2);border-radius:999px;padding:2px 7px;font-size:10px;line-height:16px;white-space:nowrap}",
75
87
  ".usg_accountStatus[data-status=ok]{color:var(--usg-providerAccent);background:color-mix(in srgb,var(--usg-providerAccent) 12%,transparent)}",
88
+ ".usg_accountHealth{border-top:1px solid var(--dsw-alias-border-l1);grid-template-columns:auto minmax(0,1fr);gap:3px 10px;margin:2px 0 0;padding-top:7px;display:grid}",
89
+ ".usg_accountHealth dt{color:var(--dsw-alias-label-caption);font-size:10px;line-height:15px}",
90
+ ".usg_accountHealth dd{color:var(--dsw-alias-label-secondary);min-width:0;margin:0;font-size:10px;line-height:15px;text-align:right;overflow-wrap:anywhere}",
76
91
  ".usg_quotaList{flex-direction:column;gap:8px;display:flex}",
77
92
  ".usg_quotaRow{display:flex;flex-direction:column;gap:4px}",
78
93
  ".usg_quotaMeta{align-items:baseline;gap:8px;display:flex}",
@@ -155,6 +170,10 @@ window.__ModuleLoader__.load({
155
170
  badgeOk: "usg_badgeOk",
156
171
  badgeBad: "usg_badgeBad",
157
172
  badgeCount: "usg_badgeCount",
173
+ sessionPill: "usg_sessionPill",
174
+ sessionPillProvider: "usg_sessionPillProvider",
175
+ sessionPillSeparator: "usg_sessionPillSeparator",
176
+ sessionPillValue: "usg_sessionPillValue",
158
177
  panel: "usg_panel",
159
178
  header: "usg_header",
160
179
  headerLeft: "usg_headerLeft",
@@ -171,6 +190,8 @@ window.__ModuleLoader__.load({
171
190
  providerPickerLabel: "usg_providerPickerLabel",
172
191
  providerSelect: "usg_providerSelect",
173
192
  accountGrid: "usg_accountGrid",
193
+ exportLinks: "usg_exportLinks",
194
+ exportLink: "usg_exportLink",
174
195
  accountCard: "usg_accountCard",
175
196
  accountHead: "usg_accountHead",
176
197
  accountMark: "usg_accountMark",
@@ -178,6 +199,7 @@ window.__ModuleLoader__.load({
178
199
  accountName: "usg_accountName",
179
200
  accountPlan: "usg_accountPlan",
180
201
  accountStatus: "usg_accountStatus",
202
+ accountHealth: "usg_accountHealth",
181
203
  quotaList: "usg_quotaList",
182
204
  quotaRow: "usg_quotaRow",
183
205
  quotaMeta: "usg_quotaMeta",
@@ -301,6 +323,21 @@ window.__ModuleLoader__.load({
301
323
  }
302
324
  }
303
325
 
326
+ function budgetWindowText(period, window, translate) {
327
+ if (window === null || typeof window !== "object" || window.limit === null || window.limit === void 0) return null;
328
+ const spent = window.costComplete === true ? fmtCurrency(window.estimatedSpend, window.currency) : "—";
329
+ const limit = fmtCurrency(window.limit, window.currency);
330
+ const level = ["normal", "warning", "critical"].includes(window.level)
331
+ ? translate(`budget.level.${window.level}`)
332
+ : translate("budget.level.unknown");
333
+ return translate("budget.summary", {
334
+ period: translate(`budget.period.${period}`),
335
+ spent,
336
+ limit,
337
+ level
338
+ });
339
+ }
340
+
304
341
  /**
305
342
  * Collapsed-badge account value, derived from the unified v0.2.0 account
306
343
  * snapshot. Balance providers show a real-currency amount; subscription /
@@ -367,13 +404,25 @@ window.__ModuleLoader__.load({
367
404
  return !inside(layer) && !inside(panel);
368
405
  }
369
406
 
407
+ const SAFE_DIAGNOSTIC_REASONS = new Set([
408
+ "dns-resolution-failed",
409
+ "timeout",
410
+ "rate-limited",
411
+ "unauthorized",
412
+ "upstream-invalid-json",
413
+ "upstream-not-json",
414
+ "upstream-too-large",
415
+ "upstream-invalid-response",
416
+ "blocked-network",
417
+ "all-addresses-unreachable",
418
+ "no-validated-address",
419
+ "sub2api-balance-shape-unrecognized",
420
+ "unknown"
421
+ ]);
422
+
370
423
  /** Defense-in-depth filter for server-provided, secret-free diagnostic reasons. */
371
424
  function safeDiagnosticReason(reason) {
372
- if (typeof reason !== "string") return null;
373
- const value = reason.trim();
374
- if (value === "" || value.length > 160) return null;
375
- if (/(authorization|bearer\s|api[_-]?key|cookie|token\s*[=:]|sk-[a-z0-9])/i.test(value)) return null;
376
- return value;
425
+ return typeof reason === "string" && SAFE_DIAGNOSTIC_REASONS.has(reason) ? reason : null;
377
426
  }
378
427
 
379
428
  function diagnosticReasonText(reason, translate) {
@@ -383,8 +432,15 @@ window.__ModuleLoader__.load({
383
432
  if (value === "all-addresses-unreachable") return translate("account.reason.allAddressesUnreachable");
384
433
  if (value === "upstream-not-json") return translate("account.reason.upstreamNotJson");
385
434
  if (value === "upstream-invalid-json") return translate("account.reason.upstreamInvalidJson");
435
+ if (value === "upstream-too-large") return translate("account.reason.upstreamTooLarge");
436
+ if (value === "upstream-invalid-response") return translate("account.reason.upstreamInvalidResponse");
437
+ if (value === "timeout") return translate("account.reason.timeout");
438
+ if (value === "rate-limited") return translate("account.reason.rateLimited");
439
+ if (value === "unauthorized") return translate("account.reason.unauthorized");
440
+ if (value === "blocked-network") return translate("account.reason.blockedNetwork");
441
+ if (value === "no-validated-address") return translate("account.reason.noValidatedAddress");
386
442
  if (value === "sub2api-balance-shape-unrecognized") return translate("account.reason.sub2apiBalanceShapeUnrecognized");
387
- return value;
443
+ return translate("account.reason.unknown");
388
444
  }
389
445
 
390
446
  /**
@@ -412,6 +468,264 @@ window.__ModuleLoader__.load({
412
468
  return payload;
413
469
  }
414
470
 
471
+ //#region CurrentSessionPill
472
+ /**
473
+ * Compact event signature from the formal composer slot's point-in-time
474
+ * session snapshot. These fields change at turn/message boundaries where
475
+ * the server-side currentRoute can change, without reacting to every input
476
+ * keystroke or adding a timer/DOM observer.
477
+ */
478
+ function sessionContextSignalOf(session) {
479
+ const nodes = Array.isArray(session?.nodes) ? session.nodes.length : 0;
480
+ const order = Array.isArray(session?.chat?.order) ? session.chat.order.length : 0;
481
+ return `${session?.running === true ? 1 : 0}:${nodes}:${order}:${session?.partial === null || session?.partial === void 0 ? 0 : 1}:${session?.removed === true ? 1 : 0}`;
482
+ }
483
+
484
+ /** Event signature from DSH's existing per-session model selector store. */
485
+ function modelSelectionSignalOf(snapshot) {
486
+ const provider = typeof snapshot?.current?.provider === "string" ? snapshot.current.provider : "";
487
+ const model = typeof snapshot?.current?.model === "string" ? snapshot.current.model : "";
488
+ return JSON.stringify([provider, model]);
489
+ }
490
+
491
+ /** Stable signature for DSH's native cumulative tokenUsage projection. */
492
+ function tokenUsageSignalOf(projection) {
493
+ return JSON.stringify([
494
+ projection?.uncachedInputTokens,
495
+ projection?.outputTokens,
496
+ projection?.cacheReadTokens,
497
+ projection?.cacheWriteTokens
498
+ ]);
499
+ }
500
+
501
+ function projectionBucketsOf(projection) {
502
+ const pairs = [
503
+ ["inputTokens", projection?.uncachedInputTokens],
504
+ ["outputTokens", projection?.outputTokens],
505
+ ["cacheReadTokens", projection?.cacheReadTokens],
506
+ ["cacheWriteTokens", projection?.cacheWriteTokens]
507
+ ];
508
+ const buckets = {};
509
+ for (const [key, value] of pairs) {
510
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return null;
511
+ buckets[key] = value;
512
+ }
513
+ return buckets;
514
+ }
515
+
516
+ /**
517
+ * The browser never prices cumulative tokens. It displays the server's
518
+ * event-time estimate only when its folded buckets exactly match DSH's
519
+ * native current-session projection.
520
+ */
521
+ function currentSessionCostLabel(session, projection) {
522
+ const buckets = projectionBucketsOf(projection);
523
+ if (session === null || typeof session !== "object" || buckets === null) return "—";
524
+ for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
525
+ if (session[key] !== buckets[key]) return "—";
526
+ }
527
+ if (session.costComplete !== true || typeof session.estimatedCost !== "number" || !Number.isFinite(session.estimatedCost)
528
+ || typeof session.currency !== "string" || session.currency === "") return "—";
529
+ return `≈${fmtCurrency(session.estimatedCost, session.currency)}`;
530
+ }
531
+
532
+ /**
533
+ * Resolve one explicit DSH session through the server-owned identity and
534
+ * account protocols. The account endpoint reads AccountService's existing
535
+ * cache; this function owns no provider mapping, credential, or cache.
536
+ */
537
+ async function loadSessionPillSnapshot(sessionId, request = fetchJson, selectedRoute = null) {
538
+ if (typeof sessionId !== "string" || sessionId === "") return null;
539
+ const selectionQuery = typeof selectedRoute?.provider === "string" && selectedRoute.provider !== ""
540
+ && typeof selectedRoute.model === "string" && selectedRoute.model !== ""
541
+ ? `&provider=${encodeURIComponent(selectedRoute.provider)}&model=${encodeURIComponent(selectedRoute.model)}`
542
+ : "";
543
+ const contextPayload = await request(`/api/usage-stats/session-context?session=${encodeURIComponent(sessionId)}${selectionQuery}`);
544
+ if (contextPayload?.ok !== true || contextPayload.context === null || typeof contextPayload.context !== "object") return null;
545
+ const context = contextPayload.context;
546
+ const accountId = typeof context.accountId === "string" && context.accountId !== ""
547
+ ? context.accountId
548
+ : typeof context.providerId === "string" && context.providerId !== "" ? context.providerId : null;
549
+ if (accountId === null) return null;
550
+ try {
551
+ const accountPayload = await request(`/api/usage-stats/account?provider=${encodeURIComponent(accountId)}&activity=active`);
552
+ if (accountPayload?.ok === true && accountPayload.account !== null && typeof accountPayload.account === "object") {
553
+ return { context, account: accountPayload.account };
554
+ }
555
+ return {
556
+ context,
557
+ account: null,
558
+ status: accountPayload?.error === "unknown-provider" ? "unsupported" : "unavailable"
559
+ };
560
+ } catch {
561
+ return { context, account: null, status: "unavailable" };
562
+ }
563
+ }
564
+
565
+ function percentLabel(value) {
566
+ return `${value.toFixed(value % 1 === 0 ? 0 : 1)}%`;
567
+ }
568
+
569
+ /** Reduce a server snapshot to display-only fields; never retain secrets. */
570
+ function sessionPillViewOf(snapshot, translate, now = Date.now()) {
571
+ if (snapshot === null || snapshot === void 0 || snapshot.context === null || typeof snapshot.context !== "object") return null;
572
+ const account = snapshot.account !== null && typeof snapshot.account === "object" ? snapshot.account : null;
573
+ const providerId = typeof snapshot.context.accountId === "string" && snapshot.context.accountId !== ""
574
+ ? snapshot.context.accountId
575
+ : typeof snapshot.context.providerId === "string" && snapshot.context.providerId !== "" ? snapshot.context.providerId : null;
576
+ if (providerId === null) return null;
577
+ const providerLabel = typeof account?.displayName === "string" && account.displayName !== ""
578
+ ? account.displayName
579
+ : typeof snapshot.context.providerId === "string" && snapshot.context.providerId !== "" ? snapshot.context.providerId : providerId;
580
+ let status = typeof account?.status === "string" ? account.status : typeof snapshot.status === "string" ? snapshot.status : "unavailable";
581
+ const costLabel = currentSessionCostLabel(snapshot.context.session, snapshot.tokenUsage);
582
+ if (account?.stale === true && status === "ok") status = "unavailable";
583
+ if (status !== "ok") {
584
+ const accountValue = status === "unsupported"
585
+ ? translate("sessionPill.status.unsupported")
586
+ : subscriptionStatusLabel(status, translate);
587
+ const value = `${accountValue} · ${costLabel}`;
588
+ return {
589
+ providerId,
590
+ providerLabel,
591
+ status,
592
+ value,
593
+ tone: "neutral",
594
+ ariaLabel: translate("sessionPill.aria", { provider: providerLabel, value })
595
+ };
596
+ }
597
+
598
+ let value = null;
599
+ let auxiliaryLabel = "";
600
+ if (account?.mode === "subscription") {
601
+ const windows = Array.isArray(account.windows)
602
+ ? account.windows.filter((entry) => entry !== null && typeof entry === "object" && typeof entry.remainingPercent === "number" && Number.isFinite(entry.remainingPercent))
603
+ : [];
604
+ const tightest = windows.reduce((current, entry) => current === null || entry.remainingPercent < current.remainingPercent ? entry : current, null);
605
+ if (tightest !== null) {
606
+ value = `${quotaLabel(tightest.kind, translate)} · ${percentLabel(tightest.remainingPercent)}`;
607
+ auxiliaryLabel = resetLabel(tightest.resetsAt, translate, now);
608
+ }
609
+ } else if (account?.balance?.unlimited === true) {
610
+ value = "∞";
611
+ } else if (account?.balance !== null && account?.balance !== void 0 && account.balance.remaining !== null && account.balance.remaining !== void 0 && Number.isFinite(Number(account.balance.remaining))) {
612
+ value = fmtCurrency(account.balance.remaining, account.balance.currency);
613
+ }
614
+
615
+ if (value === null) {
616
+ const invalid = translate("account.status.invalidResponse");
617
+ const invalidValue = `${invalid} · ${costLabel}`;
618
+ return {
619
+ providerId,
620
+ providerLabel,
621
+ status: "invalid-response",
622
+ value: invalidValue,
623
+ tone: "neutral",
624
+ ariaLabel: translate("sessionPill.aria", { provider: providerLabel, value: invalidValue })
625
+ };
626
+ }
627
+ const alertLevel = account?.alert?.level;
628
+ const tone = alertLevel === "normal" || alertLevel === "warning" || alertLevel === "critical" ? alertLevel : "neutral";
629
+ value = `${value} · ${costLabel}`;
630
+ const accessibleValue = auxiliaryLabel === "" ? value : `${value} · ${auxiliaryLabel}`;
631
+ return {
632
+ providerId,
633
+ providerLabel,
634
+ status,
635
+ value,
636
+ tone,
637
+ ariaLabel: translate("sessionPill.aria", { provider: providerLabel, value: accessibleValue })
638
+ };
639
+ }
640
+
641
+ const usageStatsPanelOpeners = /* @__PURE__ */ new Set();
642
+
643
+ function subscribeUsageStatsPanel(opener) {
644
+ if (typeof opener !== "function") return () => {};
645
+ usageStatsPanelOpeners.add(opener);
646
+ return () => usageStatsPanelOpeners.delete(opener);
647
+ }
648
+
649
+ function requestUsageStatsPanel(providerId) {
650
+ const openers = [...usageStatsPanelOpeners];
651
+ openers[openers.length - 1]?.(providerId);
652
+ }
653
+
654
+ function enableFooterActionWrapping(host) {
655
+ const hostStyle = window.getComputedStyle(host);
656
+ if (!hostStyle.display.includes("flex")
657
+ || hostStyle.flexDirection.startsWith("column")
658
+ || hostStyle.flexWrap !== "nowrap") return void 0;
659
+ const previous = host.style.flexWrap;
660
+ host.style.flexWrap = "wrap";
661
+ return () => {
662
+ host.style.flexWrap = previous;
663
+ };
664
+ }
665
+
666
+ /** Pure compact pill view, separated for render/click regression tests. */
667
+ function CurrentSessionPillView({ snapshot, translate, onOpen = requestUsageStatsPanel, now = Date.now() }) {
668
+ const view = sessionPillViewOf(snapshot, translate, now);
669
+ if (view === null) return null;
670
+ return react_jsx_runtime.jsxs("button", {
671
+ type: "button",
672
+ className: S.sessionPill,
673
+ "data-current-session-pill": true,
674
+ "data-provider": view.providerId,
675
+ "data-status": view.status,
676
+ "data-tone": view.tone,
677
+ "aria-label": view.ariaLabel,
678
+ title: view.ariaLabel,
679
+ onClick: () => onOpen(view.providerId),
680
+ children: [
681
+ react_jsx_runtime.jsx("span", { className: S.sessionPillProvider, children: view.providerLabel }),
682
+ react_jsx_runtime.jsx("span", { className: S.sessionPillSeparator, "aria-hidden": true, children: "·" }),
683
+ react_jsx_runtime.jsx("span", { className: S.sessionPillValue, children: view.value })
684
+ ]
685
+ });
686
+ }
687
+
688
+ /** Formal `conversation.input.right` slot occupant. */
689
+ function CurrentSessionPill({ sessionId, session, modelDirectory, useProjection = () => void 0, request = fetchJson, t }) {
690
+ const translate = (key, params) => interpolate(t !== void 0 ? t(key) : key, params);
691
+ const [loaded, setLoaded] = react.useState({ key: null, snapshot: null });
692
+ const loaderRef = react.useRef(null);
693
+ if (loaderRef.current === null) loaderRef.current = createLoader();
694
+ const signal = sessionContextSignalOf(session);
695
+ const tokenUsage = useProjection("tokenUsage");
696
+ const tokenSignal = tokenUsageSignalOf(tokenUsage);
697
+ const subscribeModelSelection = react.useCallback((notify) => typeof modelDirectory?.subscribe === "function"
698
+ ? modelDirectory.subscribe(notify)
699
+ : () => {}, [modelDirectory]);
700
+ const getModelSelectionSignal = react.useCallback(() => modelSelectionSignalOf(
701
+ typeof modelDirectory?.getSnapshot === "function" ? modelDirectory.getSnapshot() : null
702
+ ), [modelDirectory]);
703
+ const modelSignal = react.useSyncExternalStore(subscribeModelSelection, getModelSelectionSignal, getModelSelectionSignal);
704
+ const requestKey = JSON.stringify([sessionId, signal, modelSignal, tokenSignal]);
705
+ const [selectedProvider, selectedModel] = JSON.parse(modelSignal);
706
+ const selectedRoute = selectedProvider !== "" && selectedModel !== ""
707
+ ? { provider: selectedProvider, model: selectedModel }
708
+ : null;
709
+ react.useEffect(() => {
710
+ const seq = loaderRef.current.start();
711
+ let mounted = true;
712
+ setLoaded({ key: requestKey, snapshot: null });
713
+ loadSessionPillSnapshot(sessionId, request, selectedRoute).then((next) => {
714
+ if (mounted && loaderRef.current.isCurrent(seq)) setLoaded({ key: requestKey, snapshot: next });
715
+ }).catch(() => {
716
+ if (mounted && loaderRef.current.isCurrent(seq)) setLoaded({ key: requestKey, snapshot: null });
717
+ });
718
+ return () => {
719
+ mounted = false;
720
+ };
721
+ }, [sessionId, signal, modelSignal, tokenSignal, request, requestKey, selectedRoute?.provider, selectedRoute?.model]);
722
+ return react_jsx_runtime.jsx(CurrentSessionPillView, {
723
+ snapshot: loaded.key === requestKey && loaded.snapshot !== null ? { ...loaded.snapshot, tokenUsage } : null,
724
+ translate
725
+ });
726
+ }
727
+ //#endregion
728
+
415
729
  /**
416
730
  * Build one month's calendar heatmap: weeks as rows (Mon-first), only
417
731
  * the month's own days, padded with null placeholders. Cell tokens come
@@ -473,6 +787,57 @@ window.__ModuleLoader__.load({
473
787
  //#endregion
474
788
 
475
789
  //#region UsageStatsPanel
790
+ const SELECTED_PROVIDER_STORAGE_KEY = "@ychris12138/dsh-usage-stats:selected-provider:v1";
791
+
792
+ function browserStorage() {
793
+ try {
794
+ return typeof window === "object" ? window.localStorage ?? null : null;
795
+ } catch {
796
+ return null;
797
+ }
798
+ }
799
+
800
+ function validStoredProvider(value) {
801
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && !value.includes("\0") ? value : null;
802
+ }
803
+
804
+ function readSelectedProvider(storage = browserStorage()) {
805
+ try {
806
+ const stored = storage?.getItem?.(SELECTED_PROVIDER_STORAGE_KEY) ?? null;
807
+ const normalized = validStoredProvider(stored);
808
+ if (stored !== null && normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
809
+ return normalized;
810
+ } catch {
811
+ return null;
812
+ }
813
+ }
814
+
815
+ function writeSelectedProvider(providerId, storage = browserStorage()) {
816
+ try {
817
+ const normalized = validStoredProvider(providerId);
818
+ if (normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
819
+ else storage?.setItem?.(SELECTED_PROVIDER_STORAGE_KEY, normalized);
820
+ } catch {
821
+ // Storage may be disabled or quota-limited. Selection remains usable
822
+ // for the current mount and silently falls back on the next one.
823
+ }
824
+ }
825
+
826
+ function reconcileSelectedProvider(current, providers, storage = browserStorage()) {
827
+ if (Array.isArray(providers) && providers.some((provider) => provider?.id === current)) return current;
828
+ if (current !== null) writeSelectedProvider(null, storage);
829
+ if (!Array.isArray(providers) || providers.length === 0) return null;
830
+ return providers.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
831
+ ?? providers.find((provider) => provider.id === "deepseek")?.id
832
+ ?? providers.find((provider) => provider.configured)?.id
833
+ ?? providers[0].id;
834
+ }
835
+
836
+ function authoritativeProviderList(payload) {
837
+ if (payload?.ok !== true) return null;
838
+ return Array.isArray(payload.providers) ? payload.providers : [];
839
+ }
840
+
476
841
  /**
477
842
  * Sidebar footer action: badge + floating panel with balance and usage.
478
843
  * @param props - `wide` from the sidebar shell, `t` bound by the slot runtime.
@@ -485,8 +850,9 @@ window.__ModuleLoader__.load({
485
850
  const [selectedDay, setSelectedDay] = react.useState(null);
486
851
  const [viewMonth, setViewMonth] = react.useState(() => currentMonthKey());
487
852
  const [providers, setProviders] = react.useState([]);
488
- const [providersLoaded, setProvidersLoaded] = react.useState(false);
489
- const [selectedProvider, setSelectedProvider] = react.useState(null);
853
+ const [providersAuthoritative, setProvidersAuthoritative] = react.useState(false);
854
+ const storageRef = react.useRef(browserStorage());
855
+ const [selectedProvider, setSelectedProvider] = react.useState(() => readSelectedProvider(storageRef.current));
490
856
  const [account, setAccount] = react.useState(null);
491
857
  const [accountLoading, setAccountLoading] = react.useState(false);
492
858
  const [accountError, setAccountError] = react.useState(null);
@@ -500,6 +866,20 @@ window.__ModuleLoader__.load({
500
866
  if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
501
867
  const providerChoices = react.useMemo(() => buildProviderChoices(providers), [providers]);
502
868
  const selectedProviderInfo = providerChoices.find((provider) => provider.id === selectedProvider) ?? null;
869
+ const selectProvider = react.useCallback((providerId) => {
870
+ const normalized = validStoredProvider(providerId);
871
+ setSelectedProvider(normalized);
872
+ writeSelectedProvider(normalized, storageRef.current);
873
+ }, []);
874
+ const openForProvider = react.useCallback((providerId) => {
875
+ if (validStoredProvider(providerId) !== null) selectProvider(providerId);
876
+ setOpen(true);
877
+ }, [selectProvider]);
878
+
879
+ // The composer pill and sidebar panel are separate slot roots. A tiny
880
+ // in-module subscription bridge opens this existing panel without DOM
881
+ // clicks, a new panel implementation, or another account state/cache.
882
+ react.useEffect(() => subscribeUsageStatsPanel(openForProvider), [openForProvider]);
503
883
 
504
884
  const loadUsage = react.useCallback(() => {
505
885
  const seq = usageLoaderRef.current.start();
@@ -521,17 +901,14 @@ window.__ModuleLoader__.load({
521
901
  const loadProviders = react.useCallback(() => {
522
902
  fetchJson("/api/usage-stats/providers").then((payload) => {
523
903
  if (!mountedRef.current) return;
524
- if (payload.ok !== true) {
525
- setProvidersLoaded(true);
526
- return;
527
- }
528
- const list = Array.isArray(payload.providers) ? payload.providers : [];
904
+ const list = authoritativeProviderList(payload);
905
+ if (list === null) return;
529
906
  setProviders(list);
530
- setProvidersLoaded(true);
531
- }).catch(() => { setProvidersLoaded(true); });
907
+ setProvidersAuthoritative(true);
908
+ }).catch(() => {});
532
909
  }, []);
533
910
 
534
- const loadAccount = react.useCallback((providerId, force = false) => {
911
+ const loadAccount = react.useCallback((providerId, force = false, activity = null) => {
535
912
  const seq = accountLoaderRef.current.start();
536
913
  setAccountLoading(true);
537
914
  setAccountError(null);
@@ -541,7 +918,8 @@ window.__ModuleLoader__.load({
541
918
  setAccountError("no providers");
542
919
  return;
543
920
  }
544
- const query = `?provider=${encodeURIComponent(target)}${force ? "&refresh=1" : ""}`;
921
+ const activityQuery = activity === "detail" ? "&activity=detail" : "";
922
+ const query = `?provider=${encodeURIComponent(target)}${force ? "&refresh=1" : ""}${activityQuery}`;
545
923
  fetchJson(`/api/usage-stats/account${query}`).then((payload) => {
546
924
  if (!mountedRef.current || !accountLoaderRef.current.isCurrent(seq)) return;
547
925
  if (payload.ok !== true) {
@@ -565,13 +943,12 @@ window.__ModuleLoader__.load({
565
943
  };
566
944
  }, []);
567
945
 
568
- // The host footer-actions container lays sidebar.footer.action list
569
- // registrations out in a ROW, so two full-width entries squeeze onto
570
- // one line and the later one (us) is pushed out of view (#21). Switch
571
- // the container to column stacking while mounted and restore its
572
- // previous inline value on unmount. Walking up (instead of targeting
573
- // the host's hashed class) keeps this working across dsh versions,
574
- // and a container that is already columnar is left untouched.
946
+ // The shared footer-actions host lays full-width registrations out in a
947
+ // row. Let those actions wrap onto separate lines for #21 without
948
+ // changing the row main axis used by sibling plugins (#82). Walking up
949
+ // avoids depending on the host's hashed CSS-module class. Existing
950
+ // column/wrap layouts are left untouched, and our inline wrap override
951
+ // is restored on unmount.
575
952
  react.useEffect(() => {
576
953
  let host = layerRef.current?.parentElement ?? null;
577
954
  for (let depth = 0; host !== null && depth < 3; depth += 1) {
@@ -579,13 +956,7 @@ window.__ModuleLoader__.load({
579
956
  host = host.parentElement;
580
957
  }
581
958
  if (host === null) return void 0;
582
- const hostStyle = window.getComputedStyle(host);
583
- if (!hostStyle.display.includes("flex") || hostStyle.flexDirection === "column") return void 0;
584
- const previous = host.style.flexDirection;
585
- host.style.flexDirection = "column";
586
- return () => {
587
- host.style.flexDirection = previous;
588
- };
959
+ return enableFooterActionWrapping(host);
589
960
  }, []);
590
961
 
591
962
 
@@ -611,15 +982,9 @@ window.__ModuleLoader__.load({
611
982
  // Keep exactly one valid provider selected across independent provider
612
983
  // and subscription responses. DeepSeek remains the initial preference.
613
984
  react.useEffect(() => {
614
- if (!providersLoaded || providerChoices.length === 0) return;
615
- setSelectedProvider((current) => {
616
- if (current !== null && providerChoices.some((provider) => provider.id === current)) return current;
617
- return providerChoices.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
618
- ?? providerChoices.find((provider) => provider.id === "deepseek")?.id
619
- ?? providerChoices.find((provider) => provider.configured)?.id
620
- ?? providerChoices[0].id;
621
- });
622
- }, [providerChoices, providersLoaded]);
985
+ if (!providersAuthoritative) return;
986
+ setSelectedProvider((current) => reconcileSelectedProvider(current, providerChoices, storageRef.current));
987
+ }, [providerChoices, providersAuthoritative]);
623
988
 
624
989
  react.useEffect(() => {
625
990
  if (!open) return;
@@ -634,18 +999,18 @@ window.__ModuleLoader__.load({
634
999
  }, [open, loadUsage, loadProviders]);
635
1000
 
636
1001
  // Fetch the selected account for BOTH the panel and the collapsed
637
- // sidebar badge. The server refreshes all providers in the background
638
- // (five-minute cache); the client re-reads that cache every five
639
- // minutes (no refresh=1 the upstream fetch stays server-owned) so
640
- // the badge/panel never stays on a stale balance or quota value.
1002
+ // sidebar badge. The server owns the adaptive upstream schedule; this
1003
+ // pre-existing shared panel path only re-reads that cache every five
1004
+ // minutes (no refresh=1), and marks an open detail view as active.
641
1005
  react.useEffect(() => {
642
1006
  if (selectedProvider === null) return;
643
- loadAccount(selectedProvider);
644
- const cacheTimer = window.setInterval(() => loadAccount(selectedProvider), 300000);
1007
+ const activity = open ? "detail" : null;
1008
+ loadAccount(selectedProvider, false, activity);
1009
+ const cacheTimer = window.setInterval(() => loadAccount(selectedProvider, false, activity), 300000);
645
1010
  return () => {
646
1011
  window.clearInterval(cacheTimer);
647
1012
  };
648
- }, [selectedProvider, loadAccount]);
1013
+ }, [selectedProvider, loadAccount, open]);
649
1014
 
650
1015
  const dayMap = react.useMemo(() => {
651
1016
  const map = new Map();
@@ -702,6 +1067,10 @@ window.__ModuleLoader__.load({
702
1067
 
703
1068
  const selectedEntry = selectedDay !== null ? dayMap.get(selectedDay) ?? null : null;
704
1069
  const badgeCount = stats !== null ? fmtCompact(stats.dayTokens) : null;
1070
+ const budgetRows = usage?.budgets === null || usage?.budgets === void 0 ? [] : [
1071
+ ["daily", usage.budgets.daily],
1072
+ ["monthly", usage.budgets.monthly]
1073
+ ].map(([period, window]) => ({ period, window, text: budgetWindowText(period, window, translate) })).filter((entry) => entry.text !== null);
705
1074
 
706
1075
  // Collapsed-badge account value, derived from the unified v0.2.0
707
1076
  // account snapshot. Balance providers show a real-currency amount;
@@ -723,7 +1092,7 @@ window.__ModuleLoader__.load({
723
1092
  const retry = () => {
724
1093
  loadUsage();
725
1094
  loadProviders();
726
- if (selectedProvider !== null) loadAccount(selectedProvider, true);
1095
+ if (selectedProvider !== null) loadAccount(selectedProvider, true, open ? "detail" : null);
727
1096
  };
728
1097
 
729
1098
  const updatedLabel = refreshedAt === null ? "" : translate("panel.updatedAt", {
@@ -799,9 +1168,9 @@ window.__ModuleLoader__.load({
799
1168
  children: react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("account.title") })
800
1169
  }),
801
1170
  react_jsx_runtime.jsx(ProviderPicker, {
802
- providers: providerChoices,
803
- selectedProvider,
804
- onSelect: (id) => setSelectedProvider(id),
1171
+ providers: providerChoices,
1172
+ selectedProvider,
1173
+ onSelect: selectProvider,
805
1174
  translate
806
1175
  }),
807
1176
  selectedProviderInfo === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("account.loading") }) : react_jsx_runtime.jsx("div", {
@@ -812,7 +1181,7 @@ window.__ModuleLoader__.load({
812
1181
  accountLoading,
813
1182
  accountError,
814
1183
  translate,
815
- onRetry: () => loadAccount(selectedProvider, true)
1184
+ onRetry: () => loadAccount(selectedProvider, true, "detail")
816
1185
  }, selectedProviderInfo.id)
817
1186
  }),
818
1187
  react_jsx_runtime.jsx("section", {
@@ -837,9 +1206,9 @@ window.__ModuleLoader__.load({
837
1206
  react_jsx_runtime.jsxs("div", {
838
1207
  className: S.statsRow,
839
1208
  children: [
840
- react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.dayTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.today") })] }),
841
- react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.monthTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.month") })] }),
842
- react_jsx_runtime.jsx("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.total) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.total") })] })
1209
+ react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.dayTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.today") })] }),
1210
+ react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.monthTokens) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.month") })] }),
1211
+ react_jsx_runtime.jsxs("div", { className: S.stat, children: [react_jsx_runtime.jsx("span", { className: S.statValue, children: fmt(stats.total) }), react_jsx_runtime.jsx("span", { className: S.statLabel, children: translate("usage.total") })] })
843
1212
  ]
844
1213
  }),
845
1214
  react_jsx_runtime.jsx("p", {
@@ -849,10 +1218,16 @@ window.__ModuleLoader__.load({
849
1218
  translate("usage.hit.today"),
850
1219
  ": ",
851
1220
  react_jsx_runtime.jsx("b", { children: fmtHit(stats.todayHit) })
852
- ]
853
- })
1221
+ ]
854
1222
  })
855
- ]
1223
+ }),
1224
+ ...budgetRows.map((entry) => react_jsx_runtime.jsx("p", {
1225
+ className: S.hitCaption,
1226
+ "data-budget-period": entry.period,
1227
+ "data-budget-level": entry.window.level,
1228
+ children: entry.text
1229
+ }, entry.period))
1230
+ ]
856
1231
  }),
857
1232
  usage !== null && usageError === null && react_jsx_runtime.jsxs("section", {
858
1233
  className: S.section,
@@ -898,7 +1273,7 @@ window.__ModuleLoader__.load({
898
1273
  })
899
1274
  ]
900
1275
  }),
901
- recent.length > 0 && react_jsx_runtime.jsxs("section", {
1276
+ recent.length > 0 && react_jsx_runtime.jsxs("section", {
902
1277
  className: S.section,
903
1278
  children: [
904
1279
  react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("usage.recent") }),
@@ -923,8 +1298,22 @@ window.__ModuleLoader__.load({
923
1298
  })
924
1299
  })
925
1300
  ]
926
- }),
927
- updatedLabel !== "" && react_jsx_runtime.jsx("p", { className: S.footerNote, children: updatedLabel })
1301
+ }),
1302
+ react_jsx_runtime.jsxs("section", {
1303
+ className: S.section,
1304
+ children: [
1305
+ react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("export.title") }),
1306
+ react_jsx_runtime.jsxs("div", {
1307
+ className: S.exportLinks,
1308
+ children: [
1309
+ react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export/daily.csv", download: "dsh-usage-daily.csv", "data-export-format": "daily-csv", children: translate("export.dailyCsv") }),
1310
+ react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export/sessions.csv", download: "dsh-usage-sessions.csv", "data-export-format": "sessions-csv", children: translate("export.sessionsCsv") }),
1311
+ react_jsx_runtime.jsx("a", { className: S.exportLink, href: "/api/usage-stats/export.json", download: "dsh-usage-stats.json", "data-export-format": "json", children: translate("export.json") })
1312
+ ]
1313
+ })
1314
+ ]
1315
+ }),
1316
+ updatedLabel !== "" && react_jsx_runtime.jsx("p", { className: S.footerNote, children: updatedLabel })
928
1317
  ]
929
1318
  })
930
1319
  ]
@@ -1043,6 +1432,7 @@ window.__ModuleLoader__.load({
1043
1432
  if (status === "invalid-response") return translate("account.status.invalidResponse");
1044
1433
  if (status === "blocked") return translate("account.status.blocked");
1045
1434
  if (status === "unsupported") return translate("account.status.unsupported");
1435
+ if (status === "unknown") return translate("account.status.unknown");
1046
1436
  return translate("subscription.status.unavailable");
1047
1437
  }
1048
1438
 
@@ -1056,12 +1446,69 @@ window.__ModuleLoader__.load({
1056
1446
  return kind;
1057
1447
  }
1058
1448
 
1059
- function resetLabel(resetsAt, translate) {
1449
+ function durationLabel(totalMinutes, translate) {
1450
+ if (totalMinutes >= 1440) {
1451
+ const days = Math.floor(totalMinutes / 1440);
1452
+ const hours = Math.floor(totalMinutes % 1440 / 60);
1453
+ return translate("duration.daysHours", { days, hours });
1454
+ }
1455
+ if (totalMinutes >= 60) {
1456
+ const hours = Math.floor(totalMinutes / 60);
1457
+ const minutes = totalMinutes % 60;
1458
+ return translate("duration.hoursMinutes", { hours, minutes });
1459
+ }
1460
+ return translate("duration.minutes", { minutes: totalMinutes });
1461
+ }
1462
+
1463
+ function formatResetCountdown(resetsAt, now, translate) {
1060
1464
  if (typeof resetsAt !== "string") return "";
1061
1465
  const date = new Date(resetsAt);
1062
1466
  if (Number.isNaN(date.getTime())) return "";
1063
- return translate("subscription.resets", {
1064
- time: date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
1467
+ const diffMs = date.getTime() - now;
1468
+ if (diffMs <= 0) return translate("subscription.resetDue");
1469
+ const totalMinutes = Math.max(1, Math.ceil(diffMs / 60000));
1470
+ return translate("subscription.resets", { time: durationLabel(totalMinutes, translate) });
1471
+ }
1472
+
1473
+ function resetLabel(resetsAt, translate, now = Date.now()) {
1474
+ return formatResetCountdown(resetsAt, now, translate);
1475
+ }
1476
+
1477
+ function provenanceLabel(provenance, translate) {
1478
+ if (provenance === "official") return translate("account.provenance.official");
1479
+ if (provenance === "provider") return translate("account.provenance.provider");
1480
+ if (provenance === "configured") return translate("account.provenance.configured");
1481
+ if (provenance === "experimental") return translate("account.provenance.experimental");
1482
+ return translate("account.provenance.unknown");
1483
+ }
1484
+
1485
+ function healthTimeLabel(value) {
1486
+ if (typeof value !== "number" || !Number.isFinite(value)) return "";
1487
+ const date = new Date(value);
1488
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
1489
+ }
1490
+
1491
+ function healthAgeLabel(value, translate) {
1492
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return "";
1493
+ return durationLabel(Math.floor(value / 60000), translate);
1494
+ }
1495
+
1496
+ function AccountHealth({ account, translate }) {
1497
+ if (account === null || account === void 0 || typeof account !== "object") return null;
1498
+ const rows = [];
1499
+ const attempted = healthTimeLabel(account.lastAttemptAt);
1500
+ const succeeded = healthTimeLabel(account.lastSuccessAt);
1501
+ const age = healthAgeLabel(account.ageMs, translate);
1502
+ if (attempted !== "") rows.push(["account.health.lastAttempt", attempted]);
1503
+ if (succeeded !== "") rows.push(["account.health.lastSuccess", succeeded]);
1504
+ if (age !== "") rows.push(["account.health.age", age]);
1505
+ rows.push(["account.health.provenance", provenanceLabel(account.provenance, translate)]);
1506
+ return react_jsx_runtime.jsx("dl", {
1507
+ className: S.accountHealth,
1508
+ children: rows.flatMap(([label, value]) => [
1509
+ react_jsx_runtime.jsx("dt", { children: translate(label) }, `${label}-term`),
1510
+ react_jsx_runtime.jsx("dd", { children: value }, `${label}-value`)
1511
+ ])
1065
1512
  });
1066
1513
  }
1067
1514
 
@@ -1115,7 +1562,8 @@ window.__ModuleLoader__.load({
1115
1562
  const mode = account?.mode ?? provider.accountMode ?? "balance";
1116
1563
  const subscriptionMode = mode === "subscription";
1117
1564
  const status = accountLoading && account === null ? "loading" : account?.status ?? "unavailable";
1118
- const statusText = status === "loading" ? translate("account.status.loading")
1565
+ const statusText = account?.stale === true ? translate("account.status.stale")
1566
+ : status === "loading" ? translate("account.status.loading")
1119
1567
  : status === "blocked" ? translate("account.status.blocked")
1120
1568
  : status === "unsupported" ? translate("account.status.unsupported")
1121
1569
  : subscriptionStatusLabel(status, translate);
@@ -1133,6 +1581,7 @@ window.__ModuleLoader__.load({
1133
1581
  "data-provider": provider.id,
1134
1582
  "data-adapter": provider.adapter ?? provider.accountMode ?? "",
1135
1583
  "data-account-mode": mode,
1584
+ "data-account-stale": account?.stale === true,
1136
1585
  children: [
1137
1586
  react_jsx_runtime.jsxs("div", {
1138
1587
  className: S.accountHead,
@@ -1159,7 +1608,8 @@ window.__ModuleLoader__.load({
1159
1608
  ? react_jsx_runtime.jsx("p", { className: S.quotaEmpty, children: translate("subscription.loading") })
1160
1609
  : react_jsx_runtime.jsx(SubscriptionContent, { provider: account ?? { status: "unavailable", windows: [] }, translate })
1161
1610
  : react_jsx_runtime.jsx(BalanceContent, { balance: account?.balance ?? null, state: balanceState, message: balanceMessage, translate, onRetry }),
1162
- reasonText !== null && status !== "ok" ? react_jsx_runtime.jsx("p", { className: S.note, children: reasonText }) : null
1611
+ react_jsx_runtime.jsx(AccountHealth, { account, translate }),
1612
+ reasonText !== null && status !== "ok" ? react_jsx_runtime.jsx("p", { className: S.note, children: reasonText }) : null
1163
1613
  ]
1164
1614
  });
1165
1615
  }
@@ -1318,20 +1768,41 @@ window.__ModuleLoader__.load({
1318
1768
  const zh = {
1319
1769
  "panel.title": "用量与余额",
1320
1770
  "panel.badge": "用量/余额",
1771
+ "sessionPill.aria": "{provider} 账户:{value}",
1772
+ "sessionPill.status.unsupported": "不支持",
1321
1773
  "account.title": "供应商账户",
1322
1774
  "account.provider": "当前供应商",
1323
1775
  "account.balanceMode": "API 余额",
1324
1776
  "account.loading": "正在加载供应商…",
1325
1777
  "account.status.loading": "查询中",
1778
+ "account.status.stale": "数据已过期",
1326
1779
  "account.status.blocked": "已阻止",
1327
1780
  "account.status.unsupported": "不支持余额",
1328
1781
  "account.status.invalidResponse": "响应异常",
1782
+ "account.status.unknown": "未知",
1329
1783
  "account.invalidResponse": "供应商返回了无法识别的额度数据。",
1330
1784
  "account.reason.dnsResolutionFailed": "无法解析供应商域名。",
1331
1785
  "account.reason.allAddressesUnreachable": "已验证的网络地址均无法连接。",
1332
1786
  "account.reason.upstreamNotJson": "上游返回的不是 JSON",
1333
1787
  "account.reason.upstreamInvalidJson": "上游返回了无法解析的 JSON",
1334
1788
  "account.reason.sub2apiBalanceShapeUnrecognized": "Sub2API 余额接口返回了未识别的数据结构。",
1789
+ "account.reason.timeout": "账户请求超时。",
1790
+ "account.reason.rateLimited": "供应商限制了账户查询频率。",
1791
+ "account.reason.unauthorized": "账户凭据未通过验证。",
1792
+ "account.reason.upstreamTooLarge": "上游响应超过安全大小限制。",
1793
+ "account.reason.upstreamInvalidResponse": "上游返回了无法识别的响应。",
1794
+ "account.reason.blockedNetwork": "账户请求被本地网络安全策略阻止。",
1795
+ "account.reason.noValidatedAddress": "没有可安全连接的供应商地址。",
1796
+ "account.reason.unknown": "账户查询失败,未提供可安全展示的诊断信息。",
1797
+ "account.health.lastAttempt": "上次尝试",
1798
+ "account.health.lastSuccess": "上次成功",
1799
+ "account.health.age": "数据年龄",
1800
+ "account.health.provenance": "数据来源",
1801
+ "account.provenance.official": "官方接口",
1802
+ "account.provenance.provider": "供应商接口",
1803
+ "account.provenance.configured": "自定义配置",
1804
+ "account.provenance.experimental": "实验性探测",
1805
+ "account.provenance.unknown": "未知",
1335
1806
  "account.blocked": "账户查询被本地安全策略阻止,请检查 HTTPS、同源和私网访问设置。",
1336
1807
  "balance.title": "账户余额",
1337
1808
  "balance.provider": "供应商",
@@ -1362,7 +1833,11 @@ window.__ModuleLoader__.load({
1362
1833
  "subscription.window.quota": "总额度",
1363
1834
  "subscription.window.mcp": "MCP 月度额度",
1364
1835
  "subscription.used": "已用 {value}%",
1365
- "subscription.resets": "{time} 重置",
1836
+ "subscription.resets": "{time}后重置",
1837
+ "subscription.resetDue": "已到重置时间",
1838
+ "duration.minutes": "{minutes} 分钟",
1839
+ "duration.hoursMinutes": "{hours} 小时 {minutes} 分钟",
1840
+ "duration.daysHours": "{days} 天 {hours} 小时",
1366
1841
  "subscription.notConfigured": "配置 {refs} 后显示真实订阅比例。",
1367
1842
  "subscription.unauthorized": "凭据已失效,请更新后重试。",
1368
1843
  "subscription.rateLimited": "供应商暂时限制查询,请稍后重试。",
@@ -1386,6 +1861,17 @@ window.__ModuleLoader__.load({
1386
1861
  "usage.cacheRead": "缓存读",
1387
1862
  "usage.unknownModel": "未知模型",
1388
1863
  "usage.noModels": "这一天没有分模型数据。",
1864
+ "export.title": "安全导出",
1865
+ "export.dailyCsv": "每日 CSV",
1866
+ "export.sessionsCsv": "会话 CSV",
1867
+ "export.json": "完整 JSON",
1868
+ "budget.period.daily": "今日估算",
1869
+ "budget.period.monthly": "本月估算",
1870
+ "budget.level.normal": "正常",
1871
+ "budget.level.warning": "接近预算",
1872
+ "budget.level.critical": "已超预算",
1873
+ "budget.level.unknown": "费用不完整",
1874
+ "budget.summary": "{period}:{spent} / {limit} · {level}",
1389
1875
  "month.year": "{year}年{month}",
1390
1876
  "action.refresh": "刷新",
1391
1877
  "action.retry": "重试",
@@ -1406,20 +1892,41 @@ window.__ModuleLoader__.load({
1406
1892
  const en = {
1407
1893
  "panel.title": "Usage & Balance",
1408
1894
  "panel.badge": "Usage/Balance",
1895
+ "sessionPill.aria": "{provider} account: {value}",
1896
+ "sessionPill.status.unsupported": "Unsupported",
1409
1897
  "account.title": "Provider account",
1410
1898
  "account.provider": "Current provider",
1411
1899
  "account.balanceMode": "API balance",
1412
1900
  "account.loading": "Loading providers…",
1413
1901
  "account.status.loading": "Loading",
1902
+ "account.status.stale": "Stale data",
1414
1903
  "account.status.blocked": "Blocked",
1415
1904
  "account.status.unsupported": "Balance unsupported",
1416
1905
  "account.status.invalidResponse": "Invalid response",
1906
+ "account.status.unknown": "Unknown",
1417
1907
  "account.invalidResponse": "The provider returned unrecognized quota data.",
1418
1908
  "account.reason.dnsResolutionFailed": "The provider hostname could not be resolved.",
1419
1909
  "account.reason.allAddressesUnreachable": "None of the validated network addresses could be reached.",
1420
1910
  "account.reason.upstreamNotJson": "The upstream response was not JSON",
1421
1911
  "account.reason.upstreamInvalidJson": "The upstream returned unparsable JSON",
1422
1912
  "account.reason.sub2apiBalanceShapeUnrecognized": "The Sub2API balance endpoint returned an unrecognized data shape.",
1913
+ "account.reason.timeout": "The account request timed out.",
1914
+ "account.reason.rateLimited": "The provider rate limited account checks.",
1915
+ "account.reason.unauthorized": "The account credential was rejected.",
1916
+ "account.reason.upstreamTooLarge": "The upstream response exceeded the safety size limit.",
1917
+ "account.reason.upstreamInvalidResponse": "The upstream returned an unrecognized response.",
1918
+ "account.reason.blockedNetwork": "The account request was blocked by the local network policy.",
1919
+ "account.reason.noValidatedAddress": "No validated provider address was available.",
1920
+ "account.reason.unknown": "The account check failed without a safe diagnostic detail.",
1921
+ "account.health.lastAttempt": "Last attempt",
1922
+ "account.health.lastSuccess": "Last success",
1923
+ "account.health.age": "Data age",
1924
+ "account.health.provenance": "Source",
1925
+ "account.provenance.official": "Official API",
1926
+ "account.provenance.provider": "Provider API",
1927
+ "account.provenance.configured": "Configured",
1928
+ "account.provenance.experimental": "Experimental",
1929
+ "account.provenance.unknown": "Unknown",
1423
1930
  "account.blocked": "The account query was blocked by the local security policy. Check HTTPS, same-origin, and private-network settings.",
1424
1931
  "balance.title": "Account balance",
1425
1932
  "balance.provider": "Provider",
@@ -1450,7 +1957,11 @@ window.__ModuleLoader__.load({
1450
1957
  "subscription.window.quota": "Total quota",
1451
1958
  "subscription.window.mcp": "Monthly MCP quota",
1452
1959
  "subscription.used": "{value}% used",
1453
- "subscription.resets": "Resets {time}",
1960
+ "subscription.resets": "Resets in {time}",
1961
+ "subscription.resetDue": "Reset due",
1962
+ "duration.minutes": "{minutes}m",
1963
+ "duration.hoursMinutes": "{hours}h {minutes}m",
1964
+ "duration.daysHours": "{days}d {hours}h",
1454
1965
  "subscription.notConfigured": "Configure {refs} to show live subscription usage.",
1455
1966
  "subscription.unauthorized": "The credential has expired; update it and retry.",
1456
1967
  "subscription.rateLimited": "The provider is rate limiting checks; retry later.",
@@ -1474,6 +1985,17 @@ window.__ModuleLoader__.load({
1474
1985
  "usage.cacheRead": "Cache read",
1475
1986
  "usage.unknownModel": "Unknown model",
1476
1987
  "usage.noModels": "No per-model data for this day.",
1988
+ "export.title": "Secret-free export",
1989
+ "export.dailyCsv": "Daily CSV",
1990
+ "export.sessionsCsv": "Sessions CSV",
1991
+ "export.json": "Full JSON",
1992
+ "budget.period.daily": "Today estimated",
1993
+ "budget.period.monthly": "This month estimated",
1994
+ "budget.level.normal": "Normal",
1995
+ "budget.level.warning": "Near budget",
1996
+ "budget.level.critical": "Over budget",
1997
+ "budget.level.unknown": "Incomplete cost",
1998
+ "budget.summary": "{period}: {spent} / {limit} · {level}",
1477
1999
  "month.year": "{month} {year}",
1478
2000
  "action.refresh": "Refresh",
1479
2001
  "action.retry": "Retry",
@@ -1498,7 +2020,8 @@ window.__ModuleLoader__.load({
1498
2020
  const inject = ["slots", "locale"];
1499
2021
 
1500
2022
  /**
1501
- * Client plugin body: register the dictionaries and the sidebar footer action.
2023
+ * Client plugin body: register dictionaries, the sidebar account panel, and
2024
+ * the current-session pill beside the host's model selector.
1502
2025
  * @param ctx - client root context.
1503
2026
  */
1504
2027
  function apply(ctx) {
@@ -1509,12 +2032,26 @@ window.__ModuleLoader__.load({
1509
2032
  locale: NS,
1510
2033
  order: 10
1511
2034
  }, UsageStatsPanel));
2035
+ // The model-directory service is optional so older/mutated DSH clients
2036
+ // keep the sidebar panel even when the inline slot cannot be supported.
2037
+ ctx.inject(["slots", "modelDirectories"], (scope) => {
2038
+ const models = scope.modelDirectories;
2039
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
2040
+ name: "conversation.input.right",
2041
+ id: "usage-stats-current-session-pill",
2042
+ locale: NS,
2043
+ order: 10,
2044
+ inject: (sessionId) => ({ modelDirectory: models.directoryFor(sessionId).store })
2045
+ }, CurrentSessionPill));
2046
+ });
1512
2047
  }
1513
2048
  //#endregion
1514
2049
 
1515
2050
  exports.apply = apply;
1516
2051
  exports.inject = inject;
1517
2052
  exports.UsageStatsPanel = UsageStatsPanel;
2053
+ exports.CurrentSessionPill = CurrentSessionPill;
2054
+ exports.CurrentSessionPillView = CurrentSessionPillView;
1518
2055
  exports.DayDetail = DayDetail;
1519
2056
  exports.ProviderAccountCard = ProviderAccountCard;
1520
2057
  exports.MonthHeatmap = MonthHeatmap;
@@ -1525,10 +2062,26 @@ window.__ModuleLoader__.load({
1525
2062
  exports.modelLabelOf = modelLabelOf;
1526
2063
  exports.fmt = fmt;
1527
2064
  exports.fmtCurrency = fmtCurrency;
2065
+ exports.budgetWindowText = budgetWindowText;
1528
2066
  exports.badgeAccountValue = badgeAccountValue;
1529
2067
  exports.badgeWarnOf = badgeWarnOf;
1530
2068
  exports.shouldDismissPanel = shouldDismissPanel;
1531
2069
  exports.safeDiagnosticReason = safeDiagnosticReason;
2070
+ exports.formatResetCountdown = formatResetCountdown;
2071
+ exports.loadSessionPillSnapshot = loadSessionPillSnapshot;
2072
+ exports.modelSelectionSignalOf = modelSelectionSignalOf;
2073
+ exports.tokenUsageSignalOf = tokenUsageSignalOf;
2074
+ exports.currentSessionCostLabel = currentSessionCostLabel;
2075
+ exports.requestUsageStatsPanel = requestUsageStatsPanel;
2076
+ exports.sessionContextSignalOf = sessionContextSignalOf;
2077
+ exports.sessionPillViewOf = sessionPillViewOf;
2078
+ exports.subscribeUsageStatsPanel = subscribeUsageStatsPanel;
2079
+ exports.enableFooterActionWrapping = enableFooterActionWrapping;
2080
+ exports.SELECTED_PROVIDER_STORAGE_KEY = SELECTED_PROVIDER_STORAGE_KEY;
2081
+ exports.authoritativeProviderList = authoritativeProviderList;
2082
+ exports.readSelectedProvider = readSelectedProvider;
2083
+ exports.reconcileSelectedProvider = reconcileSelectedProvider;
2084
+ exports.writeSelectedProvider = writeSelectedProvider;
1532
2085
  return module.exports;
1533
2086
  }
1534
2087
  });