@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/README.md +118 -20
- package/SECURITY.md +3 -1
- package/docs/release-checklist.md +106 -0
- package/docs/release-notes-v0.3.0.md +25 -0
- package/lib/accounts.js +422 -164
- package/lib/balance.js +3 -5
- package/lib/billing.js +319 -0
- package/lib/client.js +626 -70
- package/lib/export.js +227 -0
- package/lib/index.js +435 -63
- package/lib/network.js +65 -0
- package/lib/pricing.js +391 -0
- package/lib/provider-identity.js +126 -0
- package/lib/subscriptions.js +59 -1
- package/lib/usage.js +190 -13
- package/package.json +15 -5
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
|
-
|
|
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
|
|
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 [
|
|
488
|
-
const
|
|
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
|
-
|
|
524
|
-
|
|
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
|
-
|
|
530
|
-
}).catch(() => {
|
|
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
|
|
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
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
572
|
-
//
|
|
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
|
-
|
|
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 (!
|
|
614
|
-
setSelectedProvider((current) =>
|
|
615
|
-
|
|
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
|
|
637
|
-
//
|
|
638
|
-
// minutes (no refresh=1
|
|
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
|
-
|
|
643
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1062
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
});
|