@ychris12138/dsh-usage-stats 0.2.9 → 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,9 +68,13 @@ 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}",
77
+ ".usg_accountCard[data-provider=ollama],.usg_accountCard[data-adapter=ollama]{--usg-providerAccent:#d97706}",
65
78
  ".usg_accountCard[data-provider=zai],.usg_accountCard[data-provider=zai-coding-cn]{--usg-providerAccent:#7656e8}",
66
79
  ".usg_accountCard[data-provider=openrouter]{--usg-providerAccent:#6366f1}",
67
80
  ".usg_accountCard[data-provider=moonshotai],.usg_accountCard[data-provider=moonshotai-cn],.usg_accountCard[data-provider=kimi],.usg_accountCard[data-provider=kimi-coding]{--usg-providerAccent:#e07a1f}",
@@ -72,6 +85,9 @@ window.__ModuleLoader__.load({
72
85
  ".usg_accountPlan{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;line-height:14px;overflow:hidden}",
73
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}",
74
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}",
75
91
  ".usg_quotaList{flex-direction:column;gap:8px;display:flex}",
76
92
  ".usg_quotaRow{display:flex;flex-direction:column;gap:4px}",
77
93
  ".usg_quotaMeta{align-items:baseline;gap:8px;display:flex}",
@@ -154,6 +170,10 @@ window.__ModuleLoader__.load({
154
170
  badgeOk: "usg_badgeOk",
155
171
  badgeBad: "usg_badgeBad",
156
172
  badgeCount: "usg_badgeCount",
173
+ sessionPill: "usg_sessionPill",
174
+ sessionPillProvider: "usg_sessionPillProvider",
175
+ sessionPillSeparator: "usg_sessionPillSeparator",
176
+ sessionPillValue: "usg_sessionPillValue",
157
177
  panel: "usg_panel",
158
178
  header: "usg_header",
159
179
  headerLeft: "usg_headerLeft",
@@ -170,6 +190,8 @@ window.__ModuleLoader__.load({
170
190
  providerPickerLabel: "usg_providerPickerLabel",
171
191
  providerSelect: "usg_providerSelect",
172
192
  accountGrid: "usg_accountGrid",
193
+ exportLinks: "usg_exportLinks",
194
+ exportLink: "usg_exportLink",
173
195
  accountCard: "usg_accountCard",
174
196
  accountHead: "usg_accountHead",
175
197
  accountMark: "usg_accountMark",
@@ -177,6 +199,7 @@ window.__ModuleLoader__.load({
177
199
  accountName: "usg_accountName",
178
200
  accountPlan: "usg_accountPlan",
179
201
  accountStatus: "usg_accountStatus",
202
+ accountHealth: "usg_accountHealth",
180
203
  quotaList: "usg_quotaList",
181
204
  quotaRow: "usg_quotaRow",
182
205
  quotaMeta: "usg_quotaMeta",
@@ -300,6 +323,21 @@ window.__ModuleLoader__.load({
300
323
  }
301
324
  }
302
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
+
303
341
  /**
304
342
  * Collapsed-badge account value, derived from the unified v0.2.0 account
305
343
  * snapshot. Balance providers show a real-currency amount; subscription /
@@ -366,13 +404,25 @@ window.__ModuleLoader__.load({
366
404
  return !inside(layer) && !inside(panel);
367
405
  }
368
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
+
369
423
  /** Defense-in-depth filter for server-provided, secret-free diagnostic reasons. */
370
424
  function safeDiagnosticReason(reason) {
371
- if (typeof reason !== "string") return null;
372
- const value = reason.trim();
373
- if (value === "" || value.length > 160) return null;
374
- if (/(authorization|bearer\s|api[_-]?key|cookie|token\s*[=:]|sk-[a-z0-9])/i.test(value)) return null;
375
- return value;
425
+ return typeof reason === "string" && SAFE_DIAGNOSTIC_REASONS.has(reason) ? reason : null;
376
426
  }
377
427
 
378
428
  function diagnosticReasonText(reason, translate) {
@@ -382,8 +432,15 @@ window.__ModuleLoader__.load({
382
432
  if (value === "all-addresses-unreachable") return translate("account.reason.allAddressesUnreachable");
383
433
  if (value === "upstream-not-json") return translate("account.reason.upstreamNotJson");
384
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");
385
442
  if (value === "sub2api-balance-shape-unrecognized") return translate("account.reason.sub2apiBalanceShapeUnrecognized");
386
- return value;
443
+ return translate("account.reason.unknown");
387
444
  }
388
445
 
389
446
  /**
@@ -411,6 +468,264 @@ window.__ModuleLoader__.load({
411
468
  return payload;
412
469
  }
413
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
+
414
729
  /**
415
730
  * Build one month's calendar heatmap: weeks as rows (Mon-first), only
416
731
  * the month's own days, padded with null placeholders. Cell tokens come
@@ -472,6 +787,57 @@ window.__ModuleLoader__.load({
472
787
  //#endregion
473
788
 
474
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
+
475
841
  /**
476
842
  * Sidebar footer action: badge + floating panel with balance and usage.
477
843
  * @param props - `wide` from the sidebar shell, `t` bound by the slot runtime.
@@ -484,8 +850,9 @@ window.__ModuleLoader__.load({
484
850
  const [selectedDay, setSelectedDay] = react.useState(null);
485
851
  const [viewMonth, setViewMonth] = react.useState(() => currentMonthKey());
486
852
  const [providers, setProviders] = react.useState([]);
487
- const [providersLoaded, setProvidersLoaded] = react.useState(false);
488
- 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));
489
856
  const [account, setAccount] = react.useState(null);
490
857
  const [accountLoading, setAccountLoading] = react.useState(false);
491
858
  const [accountError, setAccountError] = react.useState(null);
@@ -499,6 +866,20 @@ window.__ModuleLoader__.load({
499
866
  if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
500
867
  const providerChoices = react.useMemo(() => buildProviderChoices(providers), [providers]);
501
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]);
502
883
 
503
884
  const loadUsage = react.useCallback(() => {
504
885
  const seq = usageLoaderRef.current.start();
@@ -520,17 +901,14 @@ window.__ModuleLoader__.load({
520
901
  const loadProviders = react.useCallback(() => {
521
902
  fetchJson("/api/usage-stats/providers").then((payload) => {
522
903
  if (!mountedRef.current) return;
523
- if (payload.ok !== true) {
524
- setProvidersLoaded(true);
525
- return;
526
- }
527
- const list = Array.isArray(payload.providers) ? payload.providers : [];
904
+ const list = authoritativeProviderList(payload);
905
+ if (list === null) return;
528
906
  setProviders(list);
529
- setProvidersLoaded(true);
530
- }).catch(() => { setProvidersLoaded(true); });
907
+ setProvidersAuthoritative(true);
908
+ }).catch(() => {});
531
909
  }, []);
532
910
 
533
- const loadAccount = react.useCallback((providerId, force = false) => {
911
+ const loadAccount = react.useCallback((providerId, force = false, activity = null) => {
534
912
  const seq = accountLoaderRef.current.start();
535
913
  setAccountLoading(true);
536
914
  setAccountError(null);
@@ -540,7 +918,8 @@ window.__ModuleLoader__.load({
540
918
  setAccountError("no providers");
541
919
  return;
542
920
  }
543
- 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}`;
544
923
  fetchJson(`/api/usage-stats/account${query}`).then((payload) => {
545
924
  if (!mountedRef.current || !accountLoaderRef.current.isCurrent(seq)) return;
546
925
  if (payload.ok !== true) {
@@ -564,13 +943,12 @@ window.__ModuleLoader__.load({
564
943
  };
565
944
  }, []);
566
945
 
567
- // The host footer-actions container lays sidebar.footer.action list
568
- // registrations out in a ROW, so two full-width entries squeeze onto
569
- // one line and the later one (us) is pushed out of view (#21). Switch
570
- // the container to column stacking while mounted and restore its
571
- // previous inline value on unmount. Walking up (instead of targeting
572
- // the host's hashed class) keeps this working across dsh versions,
573
- // 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.
574
952
  react.useEffect(() => {
575
953
  let host = layerRef.current?.parentElement ?? null;
576
954
  for (let depth = 0; host !== null && depth < 3; depth += 1) {
@@ -578,13 +956,7 @@ window.__ModuleLoader__.load({
578
956
  host = host.parentElement;
579
957
  }
580
958
  if (host === null) return void 0;
581
- const hostStyle = window.getComputedStyle(host);
582
- if (!hostStyle.display.includes("flex") || hostStyle.flexDirection === "column") return void 0;
583
- const previous = host.style.flexDirection;
584
- host.style.flexDirection = "column";
585
- return () => {
586
- host.style.flexDirection = previous;
587
- };
959
+ return enableFooterActionWrapping(host);
588
960
  }, []);
589
961
 
590
962
 
@@ -610,15 +982,9 @@ window.__ModuleLoader__.load({
610
982
  // Keep exactly one valid provider selected across independent provider
611
983
  // and subscription responses. DeepSeek remains the initial preference.
612
984
  react.useEffect(() => {
613
- if (!providersLoaded || providerChoices.length === 0) return;
614
- setSelectedProvider((current) => {
615
- if (current !== null && providerChoices.some((provider) => provider.id === current)) return current;
616
- return providerChoices.find((provider) => provider.id === "deepseek-official" && provider.configured)?.id
617
- ?? providerChoices.find((provider) => provider.id === "deepseek")?.id
618
- ?? providerChoices.find((provider) => provider.configured)?.id
619
- ?? providerChoices[0].id;
620
- });
621
- }, [providerChoices, providersLoaded]);
985
+ if (!providersAuthoritative) return;
986
+ setSelectedProvider((current) => reconcileSelectedProvider(current, providerChoices, storageRef.current));
987
+ }, [providerChoices, providersAuthoritative]);
622
988
 
623
989
  react.useEffect(() => {
624
990
  if (!open) return;
@@ -633,18 +999,18 @@ window.__ModuleLoader__.load({
633
999
  }, [open, loadUsage, loadProviders]);
634
1000
 
635
1001
  // Fetch the selected account for BOTH the panel and the collapsed
636
- // sidebar badge. The server refreshes all providers in the background
637
- // (five-minute cache); the client re-reads that cache every five
638
- // minutes (no refresh=1 the upstream fetch stays server-owned) so
639
- // 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.
640
1005
  react.useEffect(() => {
641
1006
  if (selectedProvider === null) return;
642
- loadAccount(selectedProvider);
643
- 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);
644
1010
  return () => {
645
1011
  window.clearInterval(cacheTimer);
646
1012
  };
647
- }, [selectedProvider, loadAccount]);
1013
+ }, [selectedProvider, loadAccount, open]);
648
1014
 
649
1015
  const dayMap = react.useMemo(() => {
650
1016
  const map = new Map();
@@ -701,6 +1067,10 @@ window.__ModuleLoader__.load({
701
1067
 
702
1068
  const selectedEntry = selectedDay !== null ? dayMap.get(selectedDay) ?? null : null;
703
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);
704
1074
 
705
1075
  // Collapsed-badge account value, derived from the unified v0.2.0
706
1076
  // account snapshot. Balance providers show a real-currency amount;
@@ -722,7 +1092,7 @@ window.__ModuleLoader__.load({
722
1092
  const retry = () => {
723
1093
  loadUsage();
724
1094
  loadProviders();
725
- if (selectedProvider !== null) loadAccount(selectedProvider, true);
1095
+ if (selectedProvider !== null) loadAccount(selectedProvider, true, open ? "detail" : null);
726
1096
  };
727
1097
 
728
1098
  const updatedLabel = refreshedAt === null ? "" : translate("panel.updatedAt", {
@@ -798,9 +1168,9 @@ window.__ModuleLoader__.load({
798
1168
  children: react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("account.title") })
799
1169
  }),
800
1170
  react_jsx_runtime.jsx(ProviderPicker, {
801
- providers: providerChoices,
802
- selectedProvider,
803
- onSelect: (id) => setSelectedProvider(id),
1171
+ providers: providerChoices,
1172
+ selectedProvider,
1173
+ onSelect: selectProvider,
804
1174
  translate
805
1175
  }),
806
1176
  selectedProviderInfo === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("account.loading") }) : react_jsx_runtime.jsx("div", {
@@ -811,7 +1181,7 @@ window.__ModuleLoader__.load({
811
1181
  accountLoading,
812
1182
  accountError,
813
1183
  translate,
814
- onRetry: () => loadAccount(selectedProvider, true)
1184
+ onRetry: () => loadAccount(selectedProvider, true, "detail")
815
1185
  }, selectedProviderInfo.id)
816
1186
  }),
817
1187
  react_jsx_runtime.jsx("section", {
@@ -836,9 +1206,9 @@ window.__ModuleLoader__.load({
836
1206
  react_jsx_runtime.jsxs("div", {
837
1207
  className: S.statsRow,
838
1208
  children: [
839
- 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") })] }),
840
- 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") })] }),
841
- 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") })] })
842
1212
  ]
843
1213
  }),
844
1214
  react_jsx_runtime.jsx("p", {
@@ -848,10 +1218,16 @@ window.__ModuleLoader__.load({
848
1218
  translate("usage.hit.today"),
849
1219
  ": ",
850
1220
  react_jsx_runtime.jsx("b", { children: fmtHit(stats.todayHit) })
851
- ]
852
- })
1221
+ ]
853
1222
  })
854
- ]
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
+ ]
855
1231
  }),
856
1232
  usage !== null && usageError === null && react_jsx_runtime.jsxs("section", {
857
1233
  className: S.section,
@@ -897,7 +1273,7 @@ window.__ModuleLoader__.load({
897
1273
  })
898
1274
  ]
899
1275
  }),
900
- recent.length > 0 && react_jsx_runtime.jsxs("section", {
1276
+ recent.length > 0 && react_jsx_runtime.jsxs("section", {
901
1277
  className: S.section,
902
1278
  children: [
903
1279
  react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("usage.recent") }),
@@ -922,8 +1298,22 @@ window.__ModuleLoader__.load({
922
1298
  })
923
1299
  })
924
1300
  ]
925
- }),
926
- 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 })
927
1317
  ]
928
1318
  })
929
1319
  ]
@@ -960,6 +1350,7 @@ window.__ModuleLoader__.load({
960
1350
  "deepseek-official": "DS",
961
1351
  deepseek: "DS",
962
1352
  "opencode-go": "GO",
1353
+ ollama: "OL",
963
1354
  openrouter: "OR",
964
1355
  moonshotai: "K",
965
1356
  "moonshotai-cn": "K",
@@ -968,7 +1359,7 @@ window.__ModuleLoader__.load({
968
1359
  zai: "Z",
969
1360
  "zai-coding-cn": "Z"
970
1361
  };
971
- return known[provider.id] ?? String(provider.displayName ?? provider.id).replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase();
1362
+ return known[provider.id] ?? known[provider.adapter] ?? String(provider.displayName ?? provider.id).replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase();
972
1363
  }
973
1364
 
974
1365
  /** Balance-mode body rendered inside the shared provider account frame. */
@@ -1041,6 +1432,7 @@ window.__ModuleLoader__.load({
1041
1432
  if (status === "invalid-response") return translate("account.status.invalidResponse");
1042
1433
  if (status === "blocked") return translate("account.status.blocked");
1043
1434
  if (status === "unsupported") return translate("account.status.unsupported");
1435
+ if (status === "unknown") return translate("account.status.unknown");
1044
1436
  return translate("subscription.status.unavailable");
1045
1437
  }
1046
1438
 
@@ -1054,12 +1446,69 @@ window.__ModuleLoader__.load({
1054
1446
  return kind;
1055
1447
  }
1056
1448
 
1057
- 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) {
1058
1464
  if (typeof resetsAt !== "string") return "";
1059
1465
  const date = new Date(resetsAt);
1060
1466
  if (Number.isNaN(date.getTime())) return "";
1061
- return translate("subscription.resets", {
1062
- 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
+ ])
1063
1512
  });
1064
1513
  }
1065
1514
 
@@ -1113,7 +1562,8 @@ window.__ModuleLoader__.load({
1113
1562
  const mode = account?.mode ?? provider.accountMode ?? "balance";
1114
1563
  const subscriptionMode = mode === "subscription";
1115
1564
  const status = accountLoading && account === null ? "loading" : account?.status ?? "unavailable";
1116
- 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")
1117
1567
  : status === "blocked" ? translate("account.status.blocked")
1118
1568
  : status === "unsupported" ? translate("account.status.unsupported")
1119
1569
  : subscriptionStatusLabel(status, translate);
@@ -1129,7 +1579,9 @@ window.__ModuleLoader__.load({
1129
1579
  return react_jsx_runtime.jsxs("article", {
1130
1580
  className: S.accountCard,
1131
1581
  "data-provider": provider.id,
1582
+ "data-adapter": provider.adapter ?? provider.accountMode ?? "",
1132
1583
  "data-account-mode": mode,
1584
+ "data-account-stale": account?.stale === true,
1133
1585
  children: [
1134
1586
  react_jsx_runtime.jsxs("div", {
1135
1587
  className: S.accountHead,
@@ -1156,7 +1608,8 @@ window.__ModuleLoader__.load({
1156
1608
  ? react_jsx_runtime.jsx("p", { className: S.quotaEmpty, children: translate("subscription.loading") })
1157
1609
  : react_jsx_runtime.jsx(SubscriptionContent, { provider: account ?? { status: "unavailable", windows: [] }, translate })
1158
1610
  : react_jsx_runtime.jsx(BalanceContent, { balance: account?.balance ?? null, state: balanceState, message: balanceMessage, translate, onRetry }),
1159
- 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
1160
1613
  ]
1161
1614
  });
1162
1615
  }
@@ -1315,20 +1768,41 @@ window.__ModuleLoader__.load({
1315
1768
  const zh = {
1316
1769
  "panel.title": "用量与余额",
1317
1770
  "panel.badge": "用量/余额",
1771
+ "sessionPill.aria": "{provider} 账户:{value}",
1772
+ "sessionPill.status.unsupported": "不支持",
1318
1773
  "account.title": "供应商账户",
1319
1774
  "account.provider": "当前供应商",
1320
1775
  "account.balanceMode": "API 余额",
1321
1776
  "account.loading": "正在加载供应商…",
1322
1777
  "account.status.loading": "查询中",
1778
+ "account.status.stale": "数据已过期",
1323
1779
  "account.status.blocked": "已阻止",
1324
1780
  "account.status.unsupported": "不支持余额",
1325
1781
  "account.status.invalidResponse": "响应异常",
1782
+ "account.status.unknown": "未知",
1326
1783
  "account.invalidResponse": "供应商返回了无法识别的额度数据。",
1327
1784
  "account.reason.dnsResolutionFailed": "无法解析供应商域名。",
1328
1785
  "account.reason.allAddressesUnreachable": "已验证的网络地址均无法连接。",
1329
1786
  "account.reason.upstreamNotJson": "上游返回的不是 JSON",
1330
1787
  "account.reason.upstreamInvalidJson": "上游返回了无法解析的 JSON",
1331
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": "未知",
1332
1806
  "account.blocked": "账户查询被本地安全策略阻止,请检查 HTTPS、同源和私网访问设置。",
1333
1807
  "balance.title": "账户余额",
1334
1808
  "balance.provider": "供应商",
@@ -1359,7 +1833,11 @@ window.__ModuleLoader__.load({
1359
1833
  "subscription.window.quota": "总额度",
1360
1834
  "subscription.window.mcp": "MCP 月度额度",
1361
1835
  "subscription.used": "已用 {value}%",
1362
- "subscription.resets": "{time} 重置",
1836
+ "subscription.resets": "{time}后重置",
1837
+ "subscription.resetDue": "已到重置时间",
1838
+ "duration.minutes": "{minutes} 分钟",
1839
+ "duration.hoursMinutes": "{hours} 小时 {minutes} 分钟",
1840
+ "duration.daysHours": "{days} 天 {hours} 小时",
1363
1841
  "subscription.notConfigured": "配置 {refs} 后显示真实订阅比例。",
1364
1842
  "subscription.unauthorized": "凭据已失效,请更新后重试。",
1365
1843
  "subscription.rateLimited": "供应商暂时限制查询,请稍后重试。",
@@ -1383,6 +1861,17 @@ window.__ModuleLoader__.load({
1383
1861
  "usage.cacheRead": "缓存读",
1384
1862
  "usage.unknownModel": "未知模型",
1385
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}",
1386
1875
  "month.year": "{year}年{month}",
1387
1876
  "action.refresh": "刷新",
1388
1877
  "action.retry": "重试",
@@ -1403,20 +1892,41 @@ window.__ModuleLoader__.load({
1403
1892
  const en = {
1404
1893
  "panel.title": "Usage & Balance",
1405
1894
  "panel.badge": "Usage/Balance",
1895
+ "sessionPill.aria": "{provider} account: {value}",
1896
+ "sessionPill.status.unsupported": "Unsupported",
1406
1897
  "account.title": "Provider account",
1407
1898
  "account.provider": "Current provider",
1408
1899
  "account.balanceMode": "API balance",
1409
1900
  "account.loading": "Loading providers…",
1410
1901
  "account.status.loading": "Loading",
1902
+ "account.status.stale": "Stale data",
1411
1903
  "account.status.blocked": "Blocked",
1412
1904
  "account.status.unsupported": "Balance unsupported",
1413
1905
  "account.status.invalidResponse": "Invalid response",
1906
+ "account.status.unknown": "Unknown",
1414
1907
  "account.invalidResponse": "The provider returned unrecognized quota data.",
1415
1908
  "account.reason.dnsResolutionFailed": "The provider hostname could not be resolved.",
1416
1909
  "account.reason.allAddressesUnreachable": "None of the validated network addresses could be reached.",
1417
1910
  "account.reason.upstreamNotJson": "The upstream response was not JSON",
1418
1911
  "account.reason.upstreamInvalidJson": "The upstream returned unparsable JSON",
1419
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",
1420
1930
  "account.blocked": "The account query was blocked by the local security policy. Check HTTPS, same-origin, and private-network settings.",
1421
1931
  "balance.title": "Account balance",
1422
1932
  "balance.provider": "Provider",
@@ -1447,7 +1957,11 @@ window.__ModuleLoader__.load({
1447
1957
  "subscription.window.quota": "Total quota",
1448
1958
  "subscription.window.mcp": "Monthly MCP quota",
1449
1959
  "subscription.used": "{value}% used",
1450
- "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",
1451
1965
  "subscription.notConfigured": "Configure {refs} to show live subscription usage.",
1452
1966
  "subscription.unauthorized": "The credential has expired; update it and retry.",
1453
1967
  "subscription.rateLimited": "The provider is rate limiting checks; retry later.",
@@ -1471,6 +1985,17 @@ window.__ModuleLoader__.load({
1471
1985
  "usage.cacheRead": "Cache read",
1472
1986
  "usage.unknownModel": "Unknown model",
1473
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}",
1474
1999
  "month.year": "{month} {year}",
1475
2000
  "action.refresh": "Refresh",
1476
2001
  "action.retry": "Retry",
@@ -1495,7 +2020,8 @@ window.__ModuleLoader__.load({
1495
2020
  const inject = ["slots", "locale"];
1496
2021
 
1497
2022
  /**
1498
- * 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.
1499
2025
  * @param ctx - client root context.
1500
2026
  */
1501
2027
  function apply(ctx) {
@@ -1506,12 +2032,26 @@ window.__ModuleLoader__.load({
1506
2032
  locale: NS,
1507
2033
  order: 10
1508
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
+ });
1509
2047
  }
1510
2048
  //#endregion
1511
2049
 
1512
2050
  exports.apply = apply;
1513
2051
  exports.inject = inject;
1514
2052
  exports.UsageStatsPanel = UsageStatsPanel;
2053
+ exports.CurrentSessionPill = CurrentSessionPill;
2054
+ exports.CurrentSessionPillView = CurrentSessionPillView;
1515
2055
  exports.DayDetail = DayDetail;
1516
2056
  exports.ProviderAccountCard = ProviderAccountCard;
1517
2057
  exports.MonthHeatmap = MonthHeatmap;
@@ -1522,10 +2062,26 @@ window.__ModuleLoader__.load({
1522
2062
  exports.modelLabelOf = modelLabelOf;
1523
2063
  exports.fmt = fmt;
1524
2064
  exports.fmtCurrency = fmtCurrency;
2065
+ exports.budgetWindowText = budgetWindowText;
1525
2066
  exports.badgeAccountValue = badgeAccountValue;
1526
2067
  exports.badgeWarnOf = badgeWarnOf;
1527
2068
  exports.shouldDismissPanel = shouldDismissPanel;
1528
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;
1529
2085
  return module.exports;
1530
2086
  }
1531
2087
  });