@ychris12138/dsh-usage-stats 0.3.0 → 0.3.2

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,15 +31,6 @@ 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}",
43
34
  ".usg_layer.usg_rail{width:36px;height:36px;margin:0}",
44
35
  ".usg_layer.usg_rail .usg_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}",
45
36
  ".usg_layer.usg_rail .usg_footerButtons{flex-direction:column;gap:2px}",
@@ -77,6 +68,7 @@ window.__ModuleLoader__.load({
77
68
  ".usg_accountCard[data-provider=ollama],.usg_accountCard[data-adapter=ollama]{--usg-providerAccent:#d97706}",
78
69
  ".usg_accountCard[data-provider=zai],.usg_accountCard[data-provider=zai-coding-cn]{--usg-providerAccent:#7656e8}",
79
70
  ".usg_accountCard[data-provider=openrouter]{--usg-providerAccent:#6366f1}",
71
+ ".usg_accountCard[data-provider=orcarouter]{--usg-providerAccent:#0891b2}",
80
72
  ".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}",
81
73
  ".usg_accountHead{align-items:center;gap:8px;display:flex}",
82
74
  ".usg_accountMark{width:24px;height:24px;color:#fff;background:var(--usg-providerAccent);border-radius:7px;justify-content:center;align-items:center;font-size:10px;font-weight:700;display:flex;box-shadow:0 4px 12px color-mix(in srgb,var(--usg-providerAccent) 25%,transparent)}",
@@ -131,8 +123,8 @@ window.__ModuleLoader__.load({
131
123
  ".usg_day{width:100%;min-height:30px;align-items:center;gap:8px;border:0;background:0 0;border-bottom:1px solid var(--dsw-alias-border-l1);padding:5px 0;font:inherit;text-align:left;cursor:pointer;display:flex}",
132
124
  ".usg_day:last-child{border-bottom:0}",
133
125
  ".usg_day:hover{background:var(--dsw-alias-interactive-bg-hover)}",
134
- ".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:none;width:104px;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
135
- ".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
126
+ ".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:0 1 104px;min-width:0;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums;overflow:hidden}",
127
+ ".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;min-width:84px;text-align:right;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
136
128
  ".usg_dayHit{color:var(--dsw-alias-label-tertiary);flex:none;width:52px;font-size:11px;line-height:18px;font-variant-numeric:tabular-nums;text-align:right}",
137
129
  ".usg_dayBar{background:var(--usg-blue);border-radius:2px;height:6px;flex:1;min-width:4px;opacity:.65}",
138
130
  ".usg_detailHeader{align-items:center;gap:8px;display:flex}",
@@ -170,10 +162,6 @@ window.__ModuleLoader__.load({
170
162
  badgeOk: "usg_badgeOk",
171
163
  badgeBad: "usg_badgeBad",
172
164
  badgeCount: "usg_badgeCount",
173
- sessionPill: "usg_sessionPill",
174
- sessionPillProvider: "usg_sessionPillProvider",
175
- sessionPillSeparator: "usg_sessionPillSeparator",
176
- sessionPillValue: "usg_sessionPillValue",
177
165
  panel: "usg_panel",
178
166
  header: "usg_header",
179
167
  headerLeft: "usg_headerLeft",
@@ -454,201 +442,66 @@ window.__ModuleLoader__.load({
454
442
  })) : [];
455
443
  }
456
444
 
445
+ const ORCAROUTER_ADD_SENTINEL = "__add_orcarouter__";
446
+ const ORCAROUTER_INTEGRATION_PATH = "/api/usage-stats/integrations/orcarouter";
447
+
448
+ /** Append the explicit setup action without changing the real provider list. */
449
+ function buildProviderPickerChoices(providers, integration) {
450
+ if (!Array.isArray(providers)) return [];
451
+ if (providers.some((provider) => provider?.id === "orcarouter")) return providers;
452
+ if (integration?.available !== true || integration?.installed !== false) return providers;
453
+ return [...providers, {
454
+ id: ORCAROUTER_ADD_SENTINEL,
455
+ displayName: "OrcaRouter",
456
+ accountMode: "balance",
457
+ synthetic: true
458
+ }];
459
+ }
460
+
461
+ /** Localized selector label; sponsorship is a small disclosure, not a card. */
462
+ function providerChoiceLabel(provider, translate) {
463
+ const label = typeof provider?.displayName === "string" && provider.displayName.trim() !== ""
464
+ ? provider.displayName
465
+ : String(provider?.id ?? "");
466
+ if (provider?.id !== "orcarouter" && provider?.id !== ORCAROUTER_ADD_SENTINEL) return label;
467
+ const suffix = typeof translate === "function" ? translate("orcarouter.choiceSuffix") : "";
468
+ return typeof suffix === "string" && suffix !== "" ? `${label}${suffix}` : label;
469
+ }
470
+
457
471
  /** Locale-safe template interpolation: `t("key", {a})` replaces `{a}`. */
458
472
  function interpolate(template, params) {
459
473
  if (params === void 0) return template;
460
474
  return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(params, key) ? String(params[key]) : match));
461
475
  }
462
476
 
463
- async function fetchJson(path) {
464
- const response = await fetch(path, { headers: { accept: "application/json" } });
477
+ async function fetchJson(path, options = {}) {
478
+ const response = await fetch(path, {
479
+ ...options,
480
+ headers: { accept: "application/json", ...options.headers }
481
+ });
465
482
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
466
483
  const payload = await response.json();
467
484
  if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
468
485
  return payload;
469
486
  }
470
487
 
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
- ]);
488
+ async function loadOrcaRouterIntegration(request = fetchJson) {
489
+ const payload = await request(ORCAROUTER_INTEGRATION_PATH);
490
+ if (payload?.ok !== true || payload.integration === null || typeof payload.integration !== "object") throw new Error("integration status unavailable");
491
+ return payload.integration;
499
492
  }
500
493
 
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);
494
+ async function addOrcaRouterIntegration(request = fetchJson) {
495
+ const payload = await request(ORCAROUTER_INTEGRATION_PATH, {
496
+ method: "POST",
497
+ headers: {
498
+ "content-type": "application/json",
499
+ "x-dsh-usage-stats-action": "add-orcarouter"
500
+ },
501
+ body: "{}"
502
+ });
503
+ if (payload?.ok !== true || payload.integration?.installed !== true) throw new Error("provider preset was not added");
504
+ return payload.integration;
652
505
  }
653
506
 
654
507
  function enableFooterActionWrapping(host) {
@@ -663,69 +516,6 @@ window.__ModuleLoader__.load({
663
516
  };
664
517
  }
665
518
 
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
-
729
519
  /**
730
520
  * Build one month's calendar heatmap: weeks as rows (Mon-first), only
731
521
  * the month's own days, padded with null placeholders. Cell tokens come
@@ -798,7 +588,11 @@ window.__ModuleLoader__.load({
798
588
  }
799
589
 
800
590
  function validStoredProvider(value) {
801
- return typeof value === "string" && value.length > 0 && value.length <= 256 && !value.includes("\0") ? value : null;
591
+ return typeof value === "string"
592
+ && value !== ORCAROUTER_ADD_SENTINEL
593
+ && value.length > 0
594
+ && value.length <= 256
595
+ && !value.includes("\0") ? value : null;
802
596
  }
803
597
 
804
598
  function readSelectedProvider(storage = browserStorage()) {
@@ -814,6 +608,10 @@ window.__ModuleLoader__.load({
814
608
 
815
609
  function writeSelectedProvider(providerId, storage = browserStorage()) {
816
610
  try {
611
+ // The synthetic dropdown entry is an action, never provider state. A
612
+ // defensive no-op also preserves the previous real selection if this
613
+ // helper is called directly by a future UI refactor.
614
+ if (providerId === ORCAROUTER_ADD_SENTINEL) return;
817
615
  const normalized = validStoredProvider(providerId);
818
616
  if (normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
819
617
  else storage?.setItem?.(SELECTED_PROVIDER_STORAGE_KEY, normalized);
@@ -824,6 +622,9 @@ window.__ModuleLoader__.load({
824
622
  }
825
623
 
826
624
  function reconcileSelectedProvider(current, providers, storage = browserStorage()) {
625
+ // An empty response can be a transient provider-service refresh. Keep the
626
+ // current choice until a non-empty authoritative list can prove removal.
627
+ if (!Array.isArray(providers) || providers.length === 0) return current;
827
628
  if (Array.isArray(providers) && providers.some((provider) => provider?.id === current)) return current;
828
629
  if (current !== null) writeSelectedProvider(null, storage);
829
630
  if (!Array.isArray(providers) || providers.length === 0) return null;
@@ -856,8 +657,12 @@ window.__ModuleLoader__.load({
856
657
  const [account, setAccount] = react.useState(null);
857
658
  const [accountLoading, setAccountLoading] = react.useState(false);
858
659
  const [accountError, setAccountError] = react.useState(null);
660
+ const [orcaRouterIntegration, setOrcaRouterIntegration] = react.useState(null);
661
+ const [orcaRouterPending, setOrcaRouterPending] = react.useState(false);
662
+ const [orcaRouterError, setOrcaRouterError] = react.useState(false);
859
663
  const [refreshedAt, setRefreshedAt] = react.useState(null);
860
664
  const mountedRef = react.useRef(true);
665
+ const orcaRouterMutationRef = react.useRef(false);
861
666
  const usageLoaderRef = react.useRef(null);
862
667
  const accountLoaderRef = react.useRef(null);
863
668
  const layerRef = react.useRef(null);
@@ -865,22 +670,24 @@ window.__ModuleLoader__.load({
865
670
  if (usageLoaderRef.current === null) usageLoaderRef.current = createLoader();
866
671
  if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
867
672
  const providerChoices = react.useMemo(() => buildProviderChoices(providers), [providers]);
673
+ const providerPickerChoices = react.useMemo(
674
+ () => buildProviderPickerChoices(providerChoices, orcaRouterIntegration),
675
+ [providerChoices, orcaRouterIntegration]
676
+ );
868
677
  const selectedProviderInfo = providerChoices.find((provider) => provider.id === selectedProvider) ?? null;
869
678
  const selectProvider = react.useCallback((providerId) => {
679
+ if (providerId === ORCAROUTER_ADD_SENTINEL) return;
870
680
  const normalized = validStoredProvider(providerId);
871
681
  setSelectedProvider(normalized);
872
682
  writeSelectedProvider(normalized, storageRef.current);
873
683
  }, []);
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]);
883
-
684
+ const closePanel = react.useCallback(() => {
685
+ // Re-assert the last visible choice at the explicit close boundary. This
686
+ // keeps the X action stable even when storage was temporarily unavailable
687
+ // during an earlier provider-selection event.
688
+ if (selectedProvider !== null) writeSelectedProvider(selectedProvider, storageRef.current);
689
+ setOpen(false);
690
+ }, [selectedProvider]);
884
691
  const loadUsage = react.useCallback(() => {
885
692
  const seq = usageLoaderRef.current.start();
886
693
  setUsageError(null);
@@ -898,16 +705,59 @@ window.__ModuleLoader__.load({
898
705
  });
899
706
  }, []);
900
707
 
901
- const loadProviders = react.useCallback(() => {
902
- fetchJson("/api/usage-stats/providers").then((payload) => {
903
- if (!mountedRef.current) return;
708
+ const loadProviders = react.useCallback(async () => {
709
+ try {
710
+ const payload = await fetchJson("/api/usage-stats/providers");
711
+ if (!mountedRef.current) return null;
904
712
  const list = authoritativeProviderList(payload);
905
- if (list === null) return;
713
+ if (list === null) return null;
906
714
  setProviders(list);
907
715
  setProvidersAuthoritative(true);
908
- }).catch(() => {});
716
+ return list;
717
+ } catch {
718
+ return null;
719
+ }
720
+ }, []);
721
+
722
+ const loadOrcaRouterStatus = react.useCallback(async () => {
723
+ if (mountedRef.current) setOrcaRouterError(false);
724
+ try {
725
+ const integration = await loadOrcaRouterIntegration();
726
+ if (mountedRef.current) setOrcaRouterIntegration(integration);
727
+ return integration;
728
+ } catch {
729
+ if (mountedRef.current) setOrcaRouterIntegration(null);
730
+ return null;
731
+ }
909
732
  }, []);
910
733
 
734
+ const selectProviderChoice = react.useCallback(async (providerId) => {
735
+ if (providerId !== ORCAROUTER_ADD_SENTINEL) {
736
+ setOrcaRouterError(false);
737
+ selectProvider(providerId);
738
+ return;
739
+ }
740
+ if (orcaRouterMutationRef.current) return;
741
+ orcaRouterMutationRef.current = true;
742
+ setOrcaRouterPending(true);
743
+ setOrcaRouterError(false);
744
+ try {
745
+ const integration = await addOrcaRouterIntegration();
746
+ const refreshedProviders = await loadProviders();
747
+ if (!mountedRef.current) return;
748
+ if (!Array.isArray(refreshedProviders) || !refreshedProviders.some((provider) => provider?.id === "orcarouter")) {
749
+ throw new Error("installed provider was not returned");
750
+ }
751
+ setOrcaRouterIntegration(integration);
752
+ selectProvider("orcarouter");
753
+ } catch {
754
+ if (mountedRef.current) setOrcaRouterError(true);
755
+ } finally {
756
+ orcaRouterMutationRef.current = false;
757
+ if (mountedRef.current) setOrcaRouterPending(false);
758
+ }
759
+ }, [loadProviders, selectProvider]);
760
+
911
761
  const loadAccount = react.useCallback((providerId, force = false, activity = null) => {
912
762
  const seq = accountLoaderRef.current.start();
913
763
  setAccountLoading(true);
@@ -990,13 +840,14 @@ window.__ModuleLoader__.load({
990
840
  if (!open) return;
991
841
  loadUsage();
992
842
  loadProviders();
843
+ loadOrcaRouterStatus();
993
844
  const usageTimer = window.setInterval(loadUsage, 60000);
994
845
  const providerTimer = window.setInterval(loadProviders, 300000);
995
846
  return () => {
996
847
  window.clearInterval(usageTimer);
997
848
  window.clearInterval(providerTimer);
998
849
  };
999
- }, [open, loadUsage, loadProviders]);
850
+ }, [open, loadUsage, loadProviders, loadOrcaRouterStatus]);
1000
851
 
1001
852
  // Fetch the selected account for BOTH the panel and the collapsed
1002
853
  // sidebar badge. The server owns the adaptive upstream schedule; this
@@ -1146,7 +997,7 @@ window.__ModuleLoader__.load({
1146
997
  type: "button",
1147
998
  className: S.iconButton,
1148
999
  "aria-label": translate("action.close"),
1149
- onClick: () => setOpen(false),
1000
+ onClick: closePanel,
1150
1001
  children: react_jsx_runtime.jsx(primitives.IconCloseOutline16, { size: 14 })
1151
1002
  })
1152
1003
  })
@@ -1168,20 +1019,27 @@ window.__ModuleLoader__.load({
1168
1019
  children: react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("account.title") })
1169
1020
  }),
1170
1021
  react_jsx_runtime.jsx(ProviderPicker, {
1171
- providers: providerChoices,
1172
- selectedProvider,
1173
- onSelect: selectProvider,
1022
+ providers: providerPickerChoices,
1023
+ selectedProvider,
1024
+ onSelect: selectProviderChoice,
1025
+ disabled: orcaRouterPending,
1174
1026
  translate
1175
1027
  }),
1028
+ orcaRouterError ? react_jsx_runtime.jsx("p", {
1029
+ className: S.note,
1030
+ role: "status",
1031
+ "data-orcarouter-action-error": true,
1032
+ children: translate("orcarouter.addError")
1033
+ }) : null,
1176
1034
  selectedProviderInfo === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("account.loading") }) : react_jsx_runtime.jsx("div", {
1177
1035
  className: S.accountGrid,
1178
- children: react_jsx_runtime.jsx(ProviderAccountCard, {
1179
- provider: selectedProviderInfo,
1180
- account: account?.id === selectedProvider ? account : null,
1181
- accountLoading,
1182
- accountError,
1183
- translate,
1184
- onRetry: () => loadAccount(selectedProvider, true, "detail")
1036
+ children: react_jsx_runtime.jsx(ProviderAccountCard, {
1037
+ provider: selectedProviderInfo,
1038
+ account: account?.id === selectedProvider ? account : null,
1039
+ accountLoading,
1040
+ accountError,
1041
+ translate,
1042
+ onRetry: () => loadAccount(selectedProvider, true, "detail")
1185
1043
  }, selectedProviderInfo.id)
1186
1044
  }),
1187
1045
  react_jsx_runtime.jsx("section", {
@@ -1352,6 +1210,7 @@ window.__ModuleLoader__.load({
1352
1210
  "opencode-go": "GO",
1353
1211
  ollama: "OL",
1354
1212
  openrouter: "OR",
1213
+ orcarouter: "OC",
1355
1214
  moonshotai: "K",
1356
1215
  "moonshotai-cn": "K",
1357
1216
  kimi: "K",
@@ -1404,7 +1263,7 @@ window.__ModuleLoader__.load({
1404
1263
  }
1405
1264
 
1406
1265
  /** Provider selector shared by monetary and subscription account modes. */
1407
- function ProviderPicker({ providers, selectedProvider, onSelect, translate }) {
1266
+ function ProviderPicker({ providers, selectedProvider, onSelect, disabled = false, translate }) {
1408
1267
  if (providers.length === 0) return null;
1409
1268
  return react_jsx_runtime.jsxs("label", {
1410
1269
  className: S.providerPicker,
@@ -1413,11 +1272,12 @@ window.__ModuleLoader__.load({
1413
1272
  react_jsx_runtime.jsx("select", {
1414
1273
  className: S.providerSelect,
1415
1274
  value: selectedProvider ?? "",
1275
+ disabled,
1416
1276
  "aria-label": translate("account.provider"),
1417
1277
  onChange: (event) => onSelect(event.target.value),
1418
1278
  children: providers.map((provider) => react_jsx_runtime.jsx("option", {
1419
1279
  value: provider.id,
1420
- children: provider.displayName
1280
+ children: providerChoiceLabel(provider, translate)
1421
1281
  }, provider.id))
1422
1282
  })
1423
1283
  ]
@@ -1768,8 +1628,8 @@ window.__ModuleLoader__.load({
1768
1628
  const zh = {
1769
1629
  "panel.title": "用量与余额",
1770
1630
  "panel.badge": "用量/余额",
1771
- "sessionPill.aria": "{provider} 账户:{value}",
1772
- "sessionPill.status.unsupported": "不支持",
1631
+ "orcarouter.choiceSuffix": "(赞助集成)",
1632
+ "orcarouter.addError": "未能添加 OrcaRouter,请稍后重试。",
1773
1633
  "account.title": "供应商账户",
1774
1634
  "account.provider": "当前供应商",
1775
1635
  "account.balanceMode": "API 余额",
@@ -1892,8 +1752,8 @@ window.__ModuleLoader__.load({
1892
1752
  const en = {
1893
1753
  "panel.title": "Usage & Balance",
1894
1754
  "panel.badge": "Usage/Balance",
1895
- "sessionPill.aria": "{provider} account: {value}",
1896
- "sessionPill.status.unsupported": "Unsupported",
1755
+ "orcarouter.choiceSuffix": " (Sponsored integration)",
1756
+ "orcarouter.addError": "Could not add OrcaRouter. Please try again.",
1897
1757
  "account.title": "Provider account",
1898
1758
  "account.provider": "Current provider",
1899
1759
  "account.balanceMode": "API balance",
@@ -2020,8 +1880,7 @@ window.__ModuleLoader__.load({
2020
1880
  const inject = ["slots", "locale"];
2021
1881
 
2022
1882
  /**
2023
- * Client plugin body: register dictionaries, the sidebar account panel, and
2024
- * the current-session pill beside the host's model selector.
1883
+ * Client plugin body: register dictionaries and the sidebar account panel.
2025
1884
  * @param ctx - client root context.
2026
1885
  */
2027
1886
  function apply(ctx) {
@@ -2032,26 +1891,12 @@ window.__ModuleLoader__.load({
2032
1891
  locale: NS,
2033
1892
  order: 10
2034
1893
  }, 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
- });
2047
1894
  }
2048
1895
  //#endregion
2049
1896
 
2050
1897
  exports.apply = apply;
2051
1898
  exports.inject = inject;
2052
1899
  exports.UsageStatsPanel = UsageStatsPanel;
2053
- exports.CurrentSessionPill = CurrentSessionPill;
2054
- exports.CurrentSessionPillView = CurrentSessionPillView;
2055
1900
  exports.DayDetail = DayDetail;
2056
1901
  exports.ProviderAccountCard = ProviderAccountCard;
2057
1902
  exports.MonthHeatmap = MonthHeatmap;
@@ -2059,6 +1904,9 @@ window.__ModuleLoader__.load({
2059
1904
  exports.cellColor = cellColor;
2060
1905
  exports.createLoader = createLoader;
2061
1906
  exports.buildProviderChoices = buildProviderChoices;
1907
+ exports.buildProviderPickerChoices = buildProviderPickerChoices;
1908
+ exports.loadOrcaRouterIntegration = loadOrcaRouterIntegration;
1909
+ exports.addOrcaRouterIntegration = addOrcaRouterIntegration;
2062
1910
  exports.modelLabelOf = modelLabelOf;
2063
1911
  exports.fmt = fmt;
2064
1912
  exports.fmtCurrency = fmtCurrency;
@@ -2068,15 +1916,9 @@ window.__ModuleLoader__.load({
2068
1916
  exports.shouldDismissPanel = shouldDismissPanel;
2069
1917
  exports.safeDiagnosticReason = safeDiagnosticReason;
2070
1918
  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
1919
  exports.enableFooterActionWrapping = enableFooterActionWrapping;
1920
+ exports.providerChoiceLabel = providerChoiceLabel;
1921
+ exports.ORCAROUTER_ADD_SENTINEL = ORCAROUTER_ADD_SENTINEL;
2080
1922
  exports.SELECTED_PROVIDER_STORAGE_KEY = SELECTED_PROVIDER_STORAGE_KEY;
2081
1923
  exports.authoritativeProviderList = authoritativeProviderList;
2082
1924
  exports.readSelectedProvider = readSelectedProvider;