dsh-provider-usage 0.3.9 → 0.3.11
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 +50 -11
- package/README_ZH.md +49 -10
- package/docs/panel-codex-en.png +0 -0
- package/docs/panel-codex-zh.png +0 -0
- package/docs/panel-ds-en.png +0 -0
- package/docs/panel-ds-zh.png +0 -0
- package/docs/provider-quota-endpoints.json +184 -0
- package/docs/provider-quota-endpoints.md +274 -0
- package/lib/client.js +275 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +71 -27
- package/package.json +10 -9
package/lib/client.js
CHANGED
|
@@ -13,6 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
"action.label": "用量",
|
|
14
14
|
"action.aria": "Provider 用量",
|
|
15
15
|
"panel.title": "Provider 用量",
|
|
16
|
+
"panel.resize": "拖拽调整面板高度",
|
|
16
17
|
"panel.refresh": "立即刷新",
|
|
17
18
|
"panel.refreshing": "刷新中…",
|
|
18
19
|
"lang.switch": "Switch to English",
|
|
@@ -56,6 +57,7 @@ window.__ModuleLoader__.load({
|
|
|
56
57
|
"action.label": "Usage",
|
|
57
58
|
"action.aria": "Provider usage",
|
|
58
59
|
"panel.title": "Provider Usage",
|
|
60
|
+
"panel.resize": "Drag to resize panel",
|
|
59
61
|
"panel.refresh": "Refresh now",
|
|
60
62
|
"panel.refreshing": "Refreshing…",
|
|
61
63
|
"lang.switch": "切换到中文",
|
|
@@ -117,11 +119,52 @@ window.__ModuleLoader__.load({
|
|
|
117
119
|
const POS_KEY = "dsh.provider-usage.floatPos";
|
|
118
120
|
const THRESHOLD_RED_KEY = "dsh.provider-usage.balanceRedThreshold";
|
|
119
121
|
const THRESHOLD_YELLOW_KEY = "dsh.provider-usage.balanceYellowThreshold";
|
|
122
|
+
const PANEL_HEIGHT_KEY = "dsh.provider-usage.panelHeight";
|
|
120
123
|
const DEFAULT_INTERVAL = 60;
|
|
121
124
|
/** Balance thresholds fall back to these hardcoded defaults when neither the
|
|
122
125
|
host config nor a stored override provides them. */
|
|
123
126
|
const DEFAULT_BALANCE_RED = 10;
|
|
124
127
|
const DEFAULT_BALANCE_YELLOW = 30;
|
|
128
|
+
/** Smallest panel height the top-edge drag allows (px). */
|
|
129
|
+
const PANEL_MIN_H = 120;
|
|
130
|
+
/** Largest panel height the top-edge drag allows: the viewport minus a
|
|
131
|
+
breathing margin, so the panel can never outgrow the screen. */
|
|
132
|
+
function panelMaxHeight() {
|
|
133
|
+
return Math.max(PANEL_MIN_H, window.innerHeight - 24);
|
|
134
|
+
}
|
|
135
|
+
/** Bottom padding of the open panel (px), read live so the layout math stays
|
|
136
|
+
correct if the stylesheet ever changes it. */
|
|
137
|
+
function panelPaddingBottom(panel) {
|
|
138
|
+
const pad = Number.parseFloat(getComputedStyle(panel).paddingBottom);
|
|
139
|
+
return Number.isFinite(pad) ? pad : 0;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Natural content height of the open panel — children plus padding — even
|
|
143
|
+
* while the panel itself is height-constrained or scrolled (children rects
|
|
144
|
+
* are compensated by scrollTop). This is the grow limit: at this height the
|
|
145
|
+
* panel needs no scrolling and leaves no blank strip at the bottom.
|
|
146
|
+
*/
|
|
147
|
+
function panelContentHeight(panel) {
|
|
148
|
+
const panelTop = panel.getBoundingClientRect().top;
|
|
149
|
+
let bottom = 0;
|
|
150
|
+
for (const child of Array.from(panel.children)) {
|
|
151
|
+
const rect = child.getBoundingClientRect();
|
|
152
|
+
bottom = Math.max(bottom, rect.bottom - panelTop + panel.scrollTop);
|
|
153
|
+
}
|
|
154
|
+
return Math.ceil(bottom) + panelPaddingBottom(panel);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Shrink limit: the height that keeps the panel head plus the FIRST provider
|
|
158
|
+
* card fully visible (bottom padding included). Falls back to PANEL_MIN_H
|
|
159
|
+
* when no provider card is present (e.g. empty list).
|
|
160
|
+
*/
|
|
161
|
+
function firstCardMinHeight(panel) {
|
|
162
|
+
const card = panel.querySelector(".dsh-usage-card");
|
|
163
|
+
if (card === null) return PANEL_MIN_H;
|
|
164
|
+
const panelTop = panel.getBoundingClientRect().top;
|
|
165
|
+
const cardRect = card.getBoundingClientRect();
|
|
166
|
+
return Math.ceil(cardRect.bottom - panelTop + panel.scrollTop) + panelPaddingBottom(panel);
|
|
167
|
+
}
|
|
125
168
|
/** Floating ball diameter in px; drag math derives from it. */
|
|
126
169
|
const BALL_SIZE = 32;
|
|
127
170
|
/** Panel-level override stored in localStorage; null means follow the harness language. */
|
|
@@ -183,6 +226,21 @@ window.__ModuleLoader__.load({
|
|
|
183
226
|
localStorage.setItem(key, value);
|
|
184
227
|
} catch {}
|
|
185
228
|
}
|
|
229
|
+
function readStoredPanelHeight() {
|
|
230
|
+
try {
|
|
231
|
+
const raw = localStorage.getItem(PANEL_HEIGHT_KEY);
|
|
232
|
+
if (raw === null) return null;
|
|
233
|
+
const value = Number(raw);
|
|
234
|
+
return Number.isFinite(value) && value >= PANEL_MIN_H ? value : null;
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function storePanelHeight(value) {
|
|
240
|
+
try {
|
|
241
|
+
localStorage.setItem(PANEL_HEIGHT_KEY, String(value));
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
186
244
|
/** Equal margin from the chat area's left edge and the window's bottom edge. */
|
|
187
245
|
const DOCK_MARGIN = 24;
|
|
188
246
|
/**
|
|
@@ -271,29 +329,35 @@ window.__ModuleLoader__.load({
|
|
|
271
329
|
if (value < yellow) return "warn";
|
|
272
330
|
return "ok";
|
|
273
331
|
}
|
|
332
|
+
const TONE_SEVERITY = {
|
|
333
|
+
ok: 0,
|
|
334
|
+
warn: 1,
|
|
335
|
+
danger: 2
|
|
336
|
+
};
|
|
337
|
+
/** More severe of two tones; used WITHIN one category. */
|
|
338
|
+
function worseTone(a, b) {
|
|
339
|
+
if (a === null) return b;
|
|
340
|
+
if (b === null) return a;
|
|
341
|
+
return TONE_SEVERITY[a] >= TONE_SEVERITY[b] ? a : b;
|
|
342
|
+
}
|
|
274
343
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
344
|
+
* Aggregate tone for one provider. The subscription/usage windows ("plan")
|
|
345
|
+
* and the prepaid balance/credits are two INDEPENDENT categories joined with
|
|
346
|
+
* OR semantics: the provider stays green while either category has enough
|
|
347
|
+
* left (the plan is normally consumed before credits, so the surviving
|
|
348
|
+
* resource sets the tone), and when both are running low the lighter warning
|
|
349
|
+
* wins. Within one category the worst row still governs — an exhausted 5h
|
|
350
|
+
* window is not "enough plan" just because the weekly window is healthy.
|
|
278
351
|
*/
|
|
279
|
-
function usageRowTone(row, red, yellow) {
|
|
280
|
-
if (row.label === "credits") return balanceTone(row.remaining, red, yellow);
|
|
281
|
-
return barTone(row.percent);
|
|
282
|
-
}
|
|
283
|
-
/** Worst balance/usage tone across one provider's balances and usage rows. */
|
|
284
352
|
function providerTone(provider, red, yellow) {
|
|
285
|
-
let
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (rowTone === "danger") return "danger";
|
|
294
|
-
if (rowTone === "warn") tone = "warn";
|
|
295
|
-
}
|
|
296
|
-
return tone;
|
|
353
|
+
let planTone = null;
|
|
354
|
+
let creditTone = null;
|
|
355
|
+
if (provider.kind === "balance") for (const row of provider.balances ?? []) creditTone = worseTone(creditTone, balanceTone(toAmount(row.total), red, yellow));
|
|
356
|
+
for (const row of provider.usages ?? []) if (row.label === "credits") creditTone = worseTone(creditTone, balanceTone(row.remaining, red, yellow));
|
|
357
|
+
else planTone = worseTone(planTone, barTone(row.percent));
|
|
358
|
+
if (planTone === null) return creditTone ?? "ok";
|
|
359
|
+
if (creditTone === null) return planTone;
|
|
360
|
+
return TONE_SEVERITY[planTone] <= TONE_SEVERITY[creditTone] ? planTone : creditTone;
|
|
297
361
|
}
|
|
298
362
|
/** Worst health across the fetch + the provider IN USE: drives the trigger's
|
|
299
363
|
status dot. Only the active provider (the composer's current selection)
|
|
@@ -317,6 +381,30 @@ window.__ModuleLoader__.load({
|
|
|
317
381
|
if (value === null) return "—";
|
|
318
382
|
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
319
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Refine the host's `active` flags with the provider of the FOCUSED session
|
|
386
|
+
* (the composer's own state), so a session switch re-highlights instantly
|
|
387
|
+
* even though the host only tracks the last explicit selection. Returns the
|
|
388
|
+
* same object when nothing changes; `undefined` provider leaves the host's
|
|
389
|
+
* flags untouched.
|
|
390
|
+
*/
|
|
391
|
+
function applyActiveProvider(data, provider) {
|
|
392
|
+
if (provider === void 0 || !Array.isArray(data.providers)) return data;
|
|
393
|
+
let changed = false;
|
|
394
|
+
const providers = data.providers.map((entry) => {
|
|
395
|
+
const active = entry.id === provider;
|
|
396
|
+
if (active === entry.active) return entry;
|
|
397
|
+
changed = true;
|
|
398
|
+
return {
|
|
399
|
+
...entry,
|
|
400
|
+
active
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
return changed ? {
|
|
404
|
+
...data,
|
|
405
|
+
providers
|
|
406
|
+
} : data;
|
|
407
|
+
}
|
|
320
408
|
function UsageRows({ provider, now, red, yellow, t }) {
|
|
321
409
|
const rows = provider.usages ?? [];
|
|
322
410
|
if (rows.length === 0) return null;
|
|
@@ -497,7 +585,7 @@ window.__ModuleLoader__.load({
|
|
|
497
585
|
]
|
|
498
586
|
});
|
|
499
587
|
}
|
|
500
|
-
function UsageAction({ t, fetchUsage }) {
|
|
588
|
+
function UsageAction({ t, fetchUsage, getActiveProvider, subscribeActiveChange }) {
|
|
501
589
|
const [open, setOpen] = (0, react.useState)(false);
|
|
502
590
|
const [data, setData] = (0, react.useState)(null);
|
|
503
591
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -519,6 +607,9 @@ window.__ModuleLoader__.load({
|
|
|
519
607
|
});
|
|
520
608
|
/** Transient ball position while dragging (cursor-centered); null when docked. */
|
|
521
609
|
const [dragPos, setDragPos] = (0, react.useState)(null);
|
|
610
|
+
/** User-resized panel height in px; null = auto-size to content (max-height
|
|
611
|
+
+ scroll). Persisted, like every other panel preference. */
|
|
612
|
+
const [panelHeight, setPanelHeight] = (0, react.useState)(() => readStoredPanelHeight());
|
|
522
613
|
const harnessLang = t("lang.switch") === zh["lang.switch"] ? "zh" : t("lang.switch") === en["lang.switch"] ? "en" : null;
|
|
523
614
|
const lang = langOverride ?? harnessLang ?? "en";
|
|
524
615
|
const tt = (0, react.useMemo)(() => makeT(lang === "zh" ? zh : en), [lang]);
|
|
@@ -543,13 +634,18 @@ window.__ModuleLoader__.load({
|
|
|
543
634
|
const dragRef = (0, react.useRef)(null);
|
|
544
635
|
/** Set when a drag ends so the trailing click does not toggle the panel. */
|
|
545
636
|
const suppressClickRef = (0, react.useRef)(false);
|
|
637
|
+
/** Active panel-height drag; cleared on pointerup/pointercancel. */
|
|
638
|
+
const resizeRef = (0, react.useRef)(null);
|
|
639
|
+
/** Resize bounds snapshotted at drag start from the live layout: min = first
|
|
640
|
+
provider card fully visible, max = content height (no scroll, no blank). */
|
|
641
|
+
const resizeLimitsRef = (0, react.useRef)(null);
|
|
546
642
|
const refresh = (0, react.useCallback)(async () => {
|
|
547
643
|
if (inFlight.current) return;
|
|
548
644
|
inFlight.current = true;
|
|
549
645
|
setLoading(true);
|
|
550
646
|
try {
|
|
551
647
|
const result = await fetchUsage();
|
|
552
|
-
setData(result);
|
|
648
|
+
setData(applyActiveProvider(result, getActiveProvider?.()));
|
|
553
649
|
setError(null);
|
|
554
650
|
} catch (reason) {
|
|
555
651
|
setError(reason instanceof Error ? reason.message : String(reason));
|
|
@@ -557,7 +653,7 @@ window.__ModuleLoader__.load({
|
|
|
557
653
|
inFlight.current = false;
|
|
558
654
|
setLoading(false);
|
|
559
655
|
}
|
|
560
|
-
}, [fetchUsage]);
|
|
656
|
+
}, [fetchUsage, getActiveProvider]);
|
|
561
657
|
(0, react.useEffect)(() => {
|
|
562
658
|
refresh();
|
|
563
659
|
}, [refresh]);
|
|
@@ -575,6 +671,11 @@ window.__ModuleLoader__.load({
|
|
|
575
671
|
if (readStoredThreshold(THRESHOLD_RED_KEY) === null && typeof data.balanceRedThreshold === "number") setRedThreshold(String(data.balanceRedThreshold));
|
|
576
672
|
if (readStoredThreshold(THRESHOLD_YELLOW_KEY) === null && typeof data.balanceYellowThreshold === "number") setYellowThreshold(String(data.balanceYellowThreshold));
|
|
577
673
|
}, [data]);
|
|
674
|
+
(0, react.useEffect)(() => {
|
|
675
|
+
return subscribeActiveChange?.(() => {
|
|
676
|
+
setData((prev) => prev === null ? prev : applyActiveProvider(prev, getActiveProvider?.()));
|
|
677
|
+
});
|
|
678
|
+
}, []);
|
|
578
679
|
(0, react.useEffect)(() => {
|
|
579
680
|
if (!open) return;
|
|
580
681
|
setNow(Date.now());
|
|
@@ -615,6 +716,16 @@ window.__ModuleLoader__.load({
|
|
|
615
716
|
pinnedPos,
|
|
616
717
|
viewport
|
|
617
718
|
]);
|
|
719
|
+
(0, react.useLayoutEffect)(() => {
|
|
720
|
+
const panel = panelRef.current;
|
|
721
|
+
if (panel === null || panelHeight === null) return;
|
|
722
|
+
const clamped = Math.min(panelHeight, panelContentHeight(panel), panelMaxHeight());
|
|
723
|
+
if (clamped !== panelHeight) setPanelHeight(clamped);
|
|
724
|
+
}, [
|
|
725
|
+
panelHeight,
|
|
726
|
+
data,
|
|
727
|
+
open
|
|
728
|
+
]);
|
|
618
729
|
(0, react.useEffect)(() => {
|
|
619
730
|
const onResize = () => setViewport({
|
|
620
731
|
w: window.innerWidth,
|
|
@@ -675,6 +786,45 @@ window.__ModuleLoader__.load({
|
|
|
675
786
|
dragRef.current = null;
|
|
676
787
|
setDragPos(null);
|
|
677
788
|
};
|
|
789
|
+
const onResizePointerDown = (event) => {
|
|
790
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
791
|
+
const panel = panelRef.current;
|
|
792
|
+
const rect = panel?.getBoundingClientRect();
|
|
793
|
+
const startHeight = panelHeight ?? Math.round(rect?.height ?? 0);
|
|
794
|
+
if (panel !== null) {
|
|
795
|
+
const viewportMax = panelMaxHeight();
|
|
796
|
+
const max = Math.min(panelContentHeight(panel), viewportMax);
|
|
797
|
+
const min = Math.min(Math.max(firstCardMinHeight(panel), PANEL_MIN_H), max);
|
|
798
|
+
resizeLimitsRef.current = {
|
|
799
|
+
min,
|
|
800
|
+
max
|
|
801
|
+
};
|
|
802
|
+
} else resizeLimitsRef.current = {
|
|
803
|
+
min: PANEL_MIN_H,
|
|
804
|
+
max: panelMaxHeight()
|
|
805
|
+
};
|
|
806
|
+
resizeRef.current = {
|
|
807
|
+
pointerId: event.pointerId,
|
|
808
|
+
startY: event.clientY,
|
|
809
|
+
startHeight,
|
|
810
|
+
current: startHeight
|
|
811
|
+
};
|
|
812
|
+
};
|
|
813
|
+
const onResizePointerMove = (event) => {
|
|
814
|
+
const resize = resizeRef.current;
|
|
815
|
+
if (resize === null || resize.pointerId !== event.pointerId) return;
|
|
816
|
+
const limits = resizeLimitsRef.current;
|
|
817
|
+
const next = limits === null ? resize.startHeight + (resize.startY - event.clientY) : Math.min(Math.max(resize.startHeight + (resize.startY - event.clientY), limits.min), limits.max);
|
|
818
|
+
resize.current = next;
|
|
819
|
+
setPanelHeight(next);
|
|
820
|
+
};
|
|
821
|
+
const endResize = (event) => {
|
|
822
|
+
const resize = resizeRef.current;
|
|
823
|
+
if (resize === null || resize.pointerId !== event.pointerId) return;
|
|
824
|
+
resizeRef.current = null;
|
|
825
|
+
resizeLimitsRef.current = null;
|
|
826
|
+
storePanelHeight(resize.current);
|
|
827
|
+
};
|
|
678
828
|
const onBallClick = () => {
|
|
679
829
|
if (suppressClickRef.current) {
|
|
680
830
|
suppressClickRef.current = false;
|
|
@@ -717,10 +867,23 @@ window.__ModuleLoader__.load({
|
|
|
717
867
|
style: {
|
|
718
868
|
left: panelPos.left,
|
|
719
869
|
top: panelPos.top,
|
|
720
|
-
bottom: panelPos.bottom
|
|
870
|
+
bottom: panelPos.bottom,
|
|
871
|
+
...panelHeight !== null ? {
|
|
872
|
+
height: Math.min(panelHeight, panelMaxHeight()),
|
|
873
|
+
maxHeight: "none"
|
|
874
|
+
} : {}
|
|
721
875
|
},
|
|
722
876
|
onKeyDown,
|
|
723
877
|
children: [
|
|
878
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
879
|
+
className: "dsh-usage-resize",
|
|
880
|
+
title: tt("panel.resize"),
|
|
881
|
+
"aria-label": tt("panel.resize"),
|
|
882
|
+
onPointerDown: onResizePointerDown,
|
|
883
|
+
onPointerMove: onResizePointerMove,
|
|
884
|
+
onPointerUp: endResize,
|
|
885
|
+
onPointerCancel: endResize
|
|
886
|
+
}),
|
|
724
887
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
725
888
|
className: "dsh-usage-head",
|
|
726
889
|
children: [
|
|
@@ -921,6 +1084,27 @@ window.__ModuleLoader__.load({
|
|
|
921
1084
|
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
922
1085
|
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
923
1086
|
}
|
|
1087
|
+
/* Top-edge resize handle: sticks to the panel top even while the body
|
|
1088
|
+
scrolls, so it is always grabbable; drag to make the panel taller/shorter
|
|
1089
|
+
(height persisted in localStorage). */
|
|
1090
|
+
.dsh-usage-resize {
|
|
1091
|
+
position: sticky; top: 0; z-index: 2; flex: none;
|
|
1092
|
+
height: 6px; margin: -4px 0 0;
|
|
1093
|
+
cursor: ns-resize; touch-action: none;
|
|
1094
|
+
border-radius: 3px;
|
|
1095
|
+
}
|
|
1096
|
+
.dsh-usage-resize::after {
|
|
1097
|
+
content: ''; position: absolute; left: 50%; top: 50%;
|
|
1098
|
+
transform: translate(-50%, -50%);
|
|
1099
|
+
width: 32px; height: 3px; border-radius: 2px;
|
|
1100
|
+
background: var(--dsw-alias-border-l2);
|
|
1101
|
+
}
|
|
1102
|
+
.dsh-usage-resize:hover, .dsh-usage-resize:active {
|
|
1103
|
+
background: color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 25%, transparent);
|
|
1104
|
+
}
|
|
1105
|
+
.dsh-usage-resize:hover::after, .dsh-usage-resize:active::after {
|
|
1106
|
+
background: var(--dsw-alias-accent, #4f8cff);
|
|
1107
|
+
}
|
|
924
1108
|
.dsh-usage-head {
|
|
925
1109
|
display: flex; align-items: center; gap: 8px; padding: 2px 4px 6px;
|
|
926
1110
|
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
|
@@ -1055,12 +1239,27 @@ window.__ModuleLoader__.load({
|
|
|
1055
1239
|
* sidebar footer's additive action slot. Data comes from the host `usage`
|
|
1056
1240
|
* Typert Remote (SRC mode — no generated descriptors) through the raw RPC
|
|
1057
1241
|
* caller on the connection service.
|
|
1242
|
+
*
|
|
1243
|
+
* The "provider in use" flag is refined CLIENT-side: the host marks the route
|
|
1244
|
+
* from the GLOBAL default model selection, which only moves on an explicit
|
|
1245
|
+
* composer pick — switching between sessions with different per-session
|
|
1246
|
+
* selections would leave it stale. The focused session's own effective
|
|
1247
|
+
* selection (pending → last used → host default, mirrored by the composer's
|
|
1248
|
+
* model seat) is read live from the client session services and overrides the
|
|
1249
|
+
* host flag, so the panel follows session switches immediately.
|
|
1250
|
+
*/
|
|
1251
|
+
/**
|
|
1252
|
+
* Required client services: slot registry, locale seats, the connection RPC
|
|
1253
|
+
* caller, and the live session/model-selection state used to refine the
|
|
1254
|
+
* "in use" flag (`sessions` + `modelDirectories`, both mounted by the web
|
|
1255
|
+
* app — the latter is the composer model seat's own service).
|
|
1058
1256
|
*/
|
|
1059
|
-
/** Required client services: slot registry, locale seats, and the connection RPC caller. */
|
|
1060
1257
|
const inject = [
|
|
1061
1258
|
"slots",
|
|
1062
1259
|
"locale",
|
|
1063
|
-
"connection"
|
|
1260
|
+
"connection",
|
|
1261
|
+
"sessions",
|
|
1262
|
+
"modelDirectories"
|
|
1064
1263
|
];
|
|
1065
1264
|
function apply(ctx) {
|
|
1066
1265
|
ensureStyles();
|
|
@@ -1073,12 +1272,61 @@ window.__ModuleLoader__.load({
|
|
|
1073
1272
|
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
1074
1273
|
return result.value;
|
|
1075
1274
|
};
|
|
1275
|
+
/**
|
|
1276
|
+
* The provider the FOCUSED session's composer shows (and its agent would
|
|
1277
|
+
* use): the session's durable model selection, else the deployment default
|
|
1278
|
+
* — exactly what the host resolves per session. Undefined while the client
|
|
1279
|
+
* session state is not ready; the host's own flag then stands.
|
|
1280
|
+
*/
|
|
1281
|
+
const getActiveProvider = () => {
|
|
1282
|
+
const current = ctx.sessions?.list?.getSnapshot()?.current;
|
|
1283
|
+
if (typeof current !== "string" || current === "") return void 0;
|
|
1284
|
+
try {
|
|
1285
|
+
const provider = (ctx.modelDirectories?.directoryFor(current))?.store?.getSnapshot()?.current?.provider;
|
|
1286
|
+
return typeof provider === "string" && provider !== "" ? provider : void 0;
|
|
1287
|
+
} catch {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
/**
|
|
1292
|
+
* Re-derive the panel's "in use" flag the moment the focused session or its
|
|
1293
|
+
* model selection changes — no need to wait for the next poll tick. The
|
|
1294
|
+
* directory subscription follows the CURRENT focused session and is
|
|
1295
|
+
* re-pointed on every session switch, so a late projection load still lands.
|
|
1296
|
+
*/
|
|
1297
|
+
const subscribeActiveChange = (onChange) => {
|
|
1298
|
+
const sessions = ctx.sessions;
|
|
1299
|
+
if (typeof sessions?.list?.subscribe !== "function") return () => {};
|
|
1300
|
+
let directoryUnsub = null;
|
|
1301
|
+
const followDirectory = () => {
|
|
1302
|
+
directoryUnsub?.();
|
|
1303
|
+
directoryUnsub = null;
|
|
1304
|
+
const current = sessions.list.getSnapshot()?.current;
|
|
1305
|
+
if (typeof current === "string" && current !== "") try {
|
|
1306
|
+
const store = ctx.modelDirectories?.directoryFor(current)?.store;
|
|
1307
|
+
if (typeof store?.subscribe === "function") directoryUnsub = store.subscribe(onChange);
|
|
1308
|
+
} catch {}
|
|
1309
|
+
};
|
|
1310
|
+
const offList = sessions.list.subscribe(() => {
|
|
1311
|
+
followDirectory();
|
|
1312
|
+
onChange();
|
|
1313
|
+
});
|
|
1314
|
+
followDirectory();
|
|
1315
|
+
return () => {
|
|
1316
|
+
offList?.();
|
|
1317
|
+
directoryUnsub?.();
|
|
1318
|
+
};
|
|
1319
|
+
};
|
|
1076
1320
|
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
1077
1321
|
name: "sidebar.footer.action",
|
|
1078
1322
|
id: "provider-usage",
|
|
1079
1323
|
order: 10,
|
|
1080
1324
|
locale: "provider-usage",
|
|
1081
|
-
inject: () => ({
|
|
1325
|
+
inject: () => ({
|
|
1326
|
+
fetchUsage,
|
|
1327
|
+
getActiveProvider,
|
|
1328
|
+
subscribeActiveChange
|
|
1329
|
+
})
|
|
1082
1330
|
}, UsageAction));
|
|
1083
1331
|
}
|
|
1084
1332
|
//#endregion
|