dsh-xdpool 2.5.0 → 2.5.1

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
@@ -7,44 +7,6 @@ window.__ModuleLoader__.load({
7
7
  //#endregion
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
- //#region src/client/sidebar-row.ts
11
- /**
12
- * The `sidebarRow` switch, read and written from one place.
13
- *
14
- * Two independent owners need this flag: the page card draws the checkbox, and
15
- * `apply()` decides whether to register the sidebar row before that card has ever
16
- * mounted. Both therefore have to read the same source of truth — the settings
17
- * mirror the host exposes — instead of one asking the server and the other
18
- * guessing.
19
- *
20
- * The scope is typed structurally here rather than imported from `PoolCard.tsx`:
21
- * this module is pulled into both tsconfigs (the server one has no JSX setting),
22
- * and the only thing it needs from that component is `getSnapshot().value`.
23
- *
24
- * The default is ON, and the read is fail-open on purpose: "the mirror has no
25
- * opinion yet" and "the user turned it off" must not be the same answer, or a
26
- * host that has not populated the mirror would silently hide a feature nobody
27
- * switched off. That is also why only the literal `false` disables the row.
28
- */
29
- /** Settings field the switch writes (declared in the plugin Config schema). */
30
- const SIDEBAR_ROW_FIELD = "sidebarRow";
31
- /**
32
- * Whether the sidebar entry should exist.
33
- *
34
- * Never throws: a locked-down host makes `getSnapshot()` unusable, and the
35
- * documented degradation there is the settings page, not a missing entry.
36
- */
37
- function readSidebarRow(scope) {
38
- if (scope === void 0) return true;
39
- try {
40
- const value = scope.getSnapshot().value;
41
- if (value === null || typeof value !== "object") return true;
42
- return value[SIDEBAR_ROW_FIELD] !== false;
43
- } catch {
44
- return true;
45
- }
46
- }
47
- //#endregion
48
10
  //#region src/status-paths.ts
49
11
  /**
50
12
  * Node-free constants and types shared by the Host and browser halves of the
@@ -200,22 +162,6 @@ window.__ModuleLoader__.load({
200
162
  */
201
163
  const POOL_RELAY_PATH = "/plugins/dsh-xdpool/relay";
202
164
  /**
203
- * What the locally running billion-context proxy is doing, read-only.
204
- *
205
- * Its own route because the answer comes from ANOTHER plugin: billion-context
206
- * runs its own loopback listener and publishes its own JSON, so this route only
207
- * finds that listener and relays a filtered subset. Nothing here is folded into
208
- * {@link POOL_STATUS_PATH}: the pool card polls that every 30 seconds, and a
209
- * third-party proxy's session table is not something every pool poll should
210
- * carry.
211
- *
212
- * The word "read-only" is load-bearing on both ends. This plugin sends GETs
213
- * only — it never configures, restarts or writes to the proxy — and the payload
214
- * it relays deliberately omits every field that could carry conversation
215
- * material.
216
- */
217
- const POOL_BILLION_PATH = "/plugins/dsh-xdpool/billion";
218
- /**
219
165
  * The pack's own management route, which the card posts to directly.
220
166
  *
221
167
  * Not a re-export of anything: the vendored bundle registers this path itself
@@ -556,7 +502,6 @@ window.__ModuleLoader__.load({
556
502
  .dsm-xdpool-usage-model{font-size:12px;font-weight:600;word-break:break-all}
557
503
  .dsm-xdpool-usage-numbers{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:17px;word-break:break-word}
558
504
  .dsm-xdpool-usage-note{margin:0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:17px}
559
- .dsm-xdpool-sidebar-row-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:6px}
560
505
  .dsm-xdpool-models-actions{display:flex;align-items:center;gap:8px;flex:none}
561
506
  /* "Built-in list (offline)" chip: a warning tint, since the user is looking at
562
507
  a SHORTER roster than the gateway offers and may wonder where models went. */
@@ -1150,6 +1095,31 @@ window.__ModuleLoader__.load({
1150
1095
  .dsm-xdpool-star svg{fill:currentColor;transition:transform .5s cubic-bezier(.34,1.56,.64,1)}
1151
1096
  .dsm-xdpool-star:hover svg{transform:rotate(72deg) scale(1.12)}
1152
1097
  .dsm-xdpool-star:active svg{transform:rotate(72deg) scale(.94)}
1098
+ /* The group button, next to the star, sharing the brand row.
1099
+ The nav is a grid of 1fr auto; a second control in the same cell would
1100
+ STACK on top of the star rather than sit beside it (grid items in one cell
1101
+ overlap). So the track list grows to three: brand | star | group, and both
1102
+ buttons are pinned to row 1 on their own columns. Auto-placement alone puts
1103
+ them under the tab strip, because the strip comes first in the DOM. */
1104
+ .dsm-xdpool-nav{grid-template-columns:1fr auto auto}
1105
+ .dsm-xdpool-star{grid-column:2;grid-row:1}
1106
+ .dsm-xdpool-group{grid-column:3;grid-row:1;margin-left:6px;cursor:pointer;font:inherit;font-size:12.5px;line-height:17px;font-weight:650;color:color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 82%, var(--dsw-alias-label-secondary,#c6c9d0));border-color:color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 88%, transparent);background:linear-gradient(140deg, color-mix(in srgb, var(--dsw-alias-bg-layer-3,#23252b) 72%, transparent), transparent 70%)}
1107
+ .dsm-xdpool-group:hover{transform:translateY(-1px);color:var(--dsw-alias-state-business-primary,#5686fe);box-shadow:0 0 0 4px color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 9%, transparent), 0 8px 22px rgb(0 0 0 / 22%)}
1108
+ /* The pulse is on the icon only, and slow: this button is a door, not a call to
1109
+ action, and a card that nags is a card people mute. */
1110
+ .dsm-xdpool-group:hover svg{transform:scale(1.1)}
1111
+ .dsm-xdpool-group svg{fill:currentColor;transition:transform .28s cubic-bezier(.34,1.56,.64,1)}
1112
+ /* The invite dialog. Positioned against the shell (which is the containing
1113
+ block) so it covers the card and nothing of the host dialog around it. */
1114
+ .dsm-xdpool-groupmask{position:absolute;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;padding:16px;border-radius:16px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 72%, transparent)}
1115
+ .dsm-xdpool-groupcard{display:flex;flex-direction:column;align-items:center;gap:10px;max-width:340px;padding:16px 18px 18px;border-radius:16px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 86%, transparent);background:var(--dsw-alias-bg-layer-2,#1c1e22);box-shadow:0 18px 48px rgb(0 0 0 / 34%), inset 0 1px 0 rgb(255 255 255 / 7%)}
1116
+ .dsm-xdpool-groupcardhead{display:flex;align-items:center;gap:10px;width:100%;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e8ea)}
1117
+ .dsm-xdpool-groupclose{margin-left:auto;appearance:none;border:none;background:transparent;padding:0 4px;font:inherit;font-size:18px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8a90a0);cursor:pointer;border-radius:8px}
1118
+ .dsm-xdpool-groupclose:hover{color:var(--dsw-alias-label-primary,#e8e8ea)}
1119
+ /* A white plate under the code, always: a QR inverted onto a dark panel scans
1120
+ on some phones and not others, and there is no way to tell which from here. */
1121
+ .dsm-xdpool-groupqr{display:block;width:260px;height:260px;border-radius:12px;background:#fff;padding:6px;object-fit:contain}
1122
+ .dsm-xdpool-groupnote{width:100%;margin:0;font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-tertiary,#8a90a0);text-align:center}
1153
1123
  .dsm-xdpool-navtabs{grid-column:1/-1;position:relative;display:flex;flex-wrap:wrap;gap:2px;padding:3px;border-radius:13px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l1,#ffffff14) 80%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 62%, transparent);box-shadow:inset 0 1px 2px rgb(0 0 0 / 4%)}
1154
1124
  /* A raised surface, not a tinted wash: the glider has to read as "this one is
1155
1125
  sitting on top of the strip" from across the panel, and a wash alone is
@@ -1197,29 +1167,6 @@ window.__ModuleLoader__.load({
1197
1167
  /* Long model ids and account emails get clipped rather than wrapped: a wrapped
1198
1168
  number column breaks the alignment that makes it scannable. */
1199
1169
 
1200
- `.trim();
1201
- const BILLION_CARD_CSS = `
1202
-
1203
- .dsm-xdpool-steps{margin:0;padding-left:20px;display:flex;flex-direction:column;gap:6px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
1204
- .dsm-xdpool-steps code{font-family:var(--dsw-alias-font-mono,ui-monospace,Menlo,Consolas,monospace);font-size:11px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-3,#26282e) 80%, transparent);border-radius:6px;padding:2px 6px}
1205
- /* A session with requests still in flight. The dot is the only motion on this
1206
- page; nothing animates while idle, so a still dot means still idle. */
1207
- .dsm-xdpool-live{display:inline-block;width:6px;height:6px;border-radius:50%;margin-left:6px;vertical-align:middle;background:var(--dsw-alias-state-success,#22c55e)}
1208
- /* Section header that carries its own controls: title on the left, actions on
1209
- the right, both on one baseline. */
1210
- .dsm-xdpool-sechead{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
1211
- .dsm-xdpool-sechead .dsm-xdpool-sectitle{margin:0}
1212
- .dsm-xdpool-secbtns{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
1213
- /* A control that reads as a link-shaped button: it only navigates or reveals,
1214
- it never commits state, so it stays quiet until hovered. */
1215
- .dsm-xdpool-secghost{appearance:none;display:inline-flex;align-items:center;font:inherit;font-size:11.5px;font-weight:560;line-height:17px;padding:3px 10px;border-radius:8px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#36373b) 85%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-3,#2a2c33) 70%, transparent);color:var(--dsw-alias-label-secondary,#c6c9d0);text-decoration:none;cursor:pointer;white-space:nowrap;transition:border-color .16s ease,color .16s ease,background .16s ease,transform .18s var(--ease-instrument)}
1216
- .dsm-xdpool-secghost:hover{border-color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 66%, var(--dsw-alias-border-l2,#36373b));color:var(--dsw-alias-label-primary,#e8e8ea);transform:translateY(-1px)}
1217
- .dsm-xdpool-secghost:focus-visible{outline:2px solid color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 78%, transparent);outline-offset:2px;box-shadow:0 0 0 4px color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 13%, transparent)}
1218
- /* The session id itself. A deep link into another plugin's UI, so it has to
1219
- read as a link without pretending to be the pool's own navigation. */
1220
- .dsm-xdpool-sesslink{color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 88%, var(--dsw-alias-label-primary,#e8e8ea));text-decoration:none;border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 30%, transparent);transition:color .16s ease,border-color .16s ease}
1221
- .dsm-xdpool-sesslink:hover{color:var(--dsw-alias-brand-primary,#5686fe);border-bottom-color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 70%, transparent)}
1222
-
1223
1170
  `.trim();
1224
1171
  const ROUTING_CARD_CSS = `
1225
1172
 
@@ -1408,9 +1355,8 @@ window.__ModuleLoader__.load({
1408
1355
  .dsm-btn-primary:hover:not(:disabled){opacity:.9}
1409
1356
  /* The borderless third of the button family: links out to a vendor's console
1410
1357
  and the destructive "forget this key" action. It had no rule at all, so the
1411
- aggregate row's three links and the billion page's "open its dashboard"
1412
- rendered as bare browser buttons — which is how a secondary action ends up
1413
- looking like the primary one. */
1358
+ aggregate row's three links rendered as bare browser buttons — which is how a
1359
+ secondary action ends up looking like the primary one. */
1414
1360
  .dsm-btn-ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-tertiary,#8a90a0)}
1415
1361
  .dsm-btn-ghost:hover:not(:disabled){border-color:var(--dsw-alias-border-l2,#36373b);color:var(--dsw-alias-label-primary,#e8e8ea);background:rgba(255,255,255,.04);text-decoration:none}
1416
1362
  .dsm-xdpool-panel-empty{color:var(--dsw-alias-label-tertiary,#999);font-size:14px;line-height:20px}
@@ -1743,6 +1689,10 @@ window.__ModuleLoader__.load({
1743
1689
  */
1744
1690
  const POOL_CARD_AVATAR = "data:image/jpeg;base64,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";
1745
1691
  //#endregion
1692
+ //#region src/client/community-qr.ts
1693
+ /** The group's invite code, as a data URI. */
1694
+ const COMMUNITY_QR_DATA_URI = "data:image/png;base64,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";
1695
+ //#endregion
1746
1696
  //#region src/client/shell.tsx
1747
1697
  /**
1748
1698
  * The card shell: brand bar, page tabs, per-page hero.
@@ -1769,7 +1719,8 @@ window.__ModuleLoader__.load({
1769
1719
  * highlight on the correct button at the correct x, in the wrong row.
1770
1720
  *
1771
1721
  * 3. **Nothing here is a gate.** The star button opens the repository in a new
1772
- * tab and nothing else. A settings card that can refuse to render is a card
1722
+ * tab and nothing else, and the group button beside it opens a dialog with
1723
+ * the WeChat invite code. A settings card that can refuse to render is a card
1773
1724
  * that, on a machine without outbound network, looks exactly like a broken
1774
1725
  * plugin.
1775
1726
  *
@@ -1863,6 +1814,26 @@ window.__ModuleLoader__.load({
1863
1814
  function PoolShell({ t, version, avatar, tabs, active, onSelect, hero, children }) {
1864
1815
  /** Measured box of the active tab's button, in tab-strip coordinates. */
1865
1816
  const [glider, setGlider] = (0, react.useState)(void 0);
1817
+ /** Whether the group-invite dialog is open. Local: nothing outside needs it. */
1818
+ const [groupOpen, setGroupOpen] = (0, react.useState)(false);
1819
+ /**
1820
+ * Close the dialog on Escape.
1821
+ *
1822
+ * The overlay's own click handler covers the pointer, but a keyboard user who
1823
+ * opened this with Enter has no other way out — and a modal with no key exit
1824
+ * is a trap. Bound only while open, so the listener is not paid for on every
1825
+ * page of a card that is mostly not showing a dialog.
1826
+ */
1827
+ (0, react.useEffect)(() => {
1828
+ if (!groupOpen) return void 0;
1829
+ const onKey = (event) => {
1830
+ if (event.key === "Escape") setGroupOpen(false);
1831
+ };
1832
+ window.addEventListener("keydown", onKey);
1833
+ return () => {
1834
+ window.removeEventListener("keydown", onKey);
1835
+ };
1836
+ }, [groupOpen]);
1866
1837
  const tabRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
1867
1838
  const tabsRef = (0, react.useRef)(null);
1868
1839
  const measure = (0, react.useCallback)(() => {
@@ -1942,6 +1913,28 @@ window.__ModuleLoader__.load({
1942
1913
  })
1943
1914
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("shell.star") ?? "Star on GitHub" })]
1944
1915
  }),
1916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1917
+ type: "button",
1918
+ className: "dsm-xdpool-star dsm-xdpool-group",
1919
+ title: t?.("shell.groupHint") ?? "扫码加入 XD 交流群",
1920
+ onClick: () => {
1921
+ setGroupOpen(true);
1922
+ },
1923
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1924
+ width: "15",
1925
+ height: "15",
1926
+ viewBox: "0 0 24 24",
1927
+ "aria-hidden": "true",
1928
+ focusable: "false",
1929
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1930
+ fill: "currentColor",
1931
+ d: "M9.1 3.4c-3.6 0-6.5 2.4-6.5 5.4 0 1.7 1 3.2 2.6 4.2l-.7 2.1 2.4-1.2c.7.2 1.4.3 2.2.3h.4a5.6 5.6 0 0 1-.2-1.5c0-3 2.9-5.4 6.5-5.4h.5C15.7 5.1 12.7 3.4 9.1 3.4zm-2.3 2.9a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8zm4.6 0a.9.9 0 1 1 0 1.8.9.9 0 0 1 0-1.8z"
1932
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1933
+ fill: "currentColor",
1934
+ d: "M21.4 12.7c0-2.5-2.4-4.5-5.4-4.5s-5.4 2-5.4 4.5 2.4 4.5 5.4 4.5c.6 0 1.2-.1 1.8-.2l2 1-.6-1.7c1.4-.9 2.2-2.2 2.2-3.6zm-7.2-1.4a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6zm3.6 0a.8.8 0 1 1 0 1.6.8.8 0 0 1 0-1.6z"
1935
+ })]
1936
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("shell.group") ?? "交流群" })]
1937
+ }),
1945
1938
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1946
1939
  className: "dsm-xdpool-navtabs",
1947
1940
  role: "tablist",
@@ -1976,6 +1969,46 @@ window.__ModuleLoader__.load({
1976
1969
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1977
1970
  className: "dsm-xdpool-body",
1978
1971
  children
1972
+ }),
1973
+ !groupOpen ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1974
+ className: "dsm-xdpool-groupmask",
1975
+ role: "dialog",
1976
+ "aria-modal": "true",
1977
+ "aria-label": t?.("shell.groupTitle") ?? "XD 交流群",
1978
+ onClick: () => {
1979
+ setGroupOpen(false);
1980
+ },
1981
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1982
+ className: "dsm-xdpool-groupcard",
1983
+ onClick: (event) => {
1984
+ event.stopPropagation();
1985
+ },
1986
+ children: [
1987
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1988
+ className: "dsm-xdpool-groupcardhead",
1989
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: t?.("shell.groupTitle") ?? "XD 交流群" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1990
+ type: "button",
1991
+ className: "dsm-xdpool-groupclose",
1992
+ "aria-label": t?.("shell.groupClose") ?? "关闭",
1993
+ onClick: () => {
1994
+ setGroupOpen(false);
1995
+ },
1996
+ children: "×"
1997
+ })]
1998
+ }),
1999
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2000
+ className: "dsm-xdpool-groupqr",
2001
+ src: COMMUNITY_QR_DATA_URI,
2002
+ alt: t?.("shell.groupAlt") ?? "微信群二维码:用微信扫一扫加入 XD 交流群",
2003
+ width: 260,
2004
+ height: 260
2005
+ }),
2006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2007
+ className: "dsm-xdpool-groupnote",
2008
+ children: t?.("shell.groupNote") ?? "用微信「扫一扫」加入 XD 交流群。二维码 7 天内有效,过期了在 GitHub 提 issue 说一声,会更新。"
2009
+ })
2010
+ ]
2011
+ })
1979
2012
  })
1980
2013
  ]
1981
2014
  });
@@ -2025,7 +2058,7 @@ window.__ModuleLoader__.load({
2025
2058
  */
2026
2059
  /** How many recent calls the detail table lists. */
2027
2060
  const DETAIL_ROWS = 12;
2028
- function fmtInt$1(value) {
2061
+ function fmtInt(value) {
2029
2062
  return value.toLocaleString("en-US");
2030
2063
  }
2031
2064
  /** Milliseconds as `1.2s` / `340ms`, because `1200ms` is not a unit anyone reads. */
@@ -2034,7 +2067,7 @@ window.__ModuleLoader__.load({
2034
2067
  }
2035
2068
  /** Tokens per second, kept integer: 12.4 tok/s reads as false precision. */
2036
2069
  function fmtTps(value) {
2037
- return value === null ? "—" : `${fmtInt$1(value)}`;
2070
+ return value === null ? "—" : `${fmtInt(value)}`;
2038
2071
  }
2039
2072
  function fmtClock(at) {
2040
2073
  const d = new Date(at);
@@ -2074,9 +2107,9 @@ window.__ModuleLoader__.load({
2074
2107
  "—"
2075
2108
  ];
2076
2109
  return [
2077
- fmtInt$1(call.usage.inputTokens),
2078
- fmtInt$1(call.usage.outputTokens),
2079
- fmtInt$1(call.usage.cachedTokens)
2110
+ fmtInt(call.usage.inputTokens),
2111
+ fmtInt(call.usage.outputTokens),
2112
+ fmtInt(call.usage.cachedTokens)
2080
2113
  ];
2081
2114
  }
2082
2115
  function LedgerPage({ t, usage: shared }) {
@@ -2198,20 +2231,20 @@ window.__ModuleLoader__.load({
2198
2231
  children: [
2199
2232
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2200
2233
  label: t?.("ledger.tokens") ?? "Tokens",
2201
- value: summary.usageReported ? fmtInt$1(totals?.input ?? 0) : t?.("ledger.notReported") ?? "not reported",
2234
+ value: summary.usageReported ? fmtInt(totals?.input ?? 0) : t?.("ledger.notReported") ?? "not reported",
2202
2235
  sub: summary.usageReported ? t?.("ledger.tokenSplit", {
2203
- input: fmtInt$1(totals?.input ?? 0),
2204
- output: fmtInt$1(totals?.output ?? 0)
2205
- }) ?? `in ${fmtInt$1(totals?.input ?? 0)} · out ${fmtInt$1(totals?.output ?? 0)}` : t?.("ledger.notReportedHint") ?? "the gateway ignored stream_options.include_usage"
2236
+ input: fmtInt(totals?.input ?? 0),
2237
+ output: fmtInt(totals?.output ?? 0)
2238
+ }) ?? `in ${fmtInt(totals?.input ?? 0)} · out ${fmtInt(totals?.output ?? 0)}` : t?.("ledger.notReportedHint") ?? "the gateway ignored stream_options.include_usage"
2206
2239
  }),
2207
2240
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2208
2241
  label: t?.("ledger.turns") ?? "Turns",
2209
- value: fmtInt$1(summary.turns),
2242
+ value: fmtInt(summary.turns),
2210
2243
  sub: t?.("ledger.callsSub", { calls: summary.calls }) ?? `${summary.calls} upstream calls`
2211
2244
  }),
2212
2245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2213
2246
  label: t?.("ledger.speed") ?? "Speed",
2214
- value: totals?.avgTps === null || totals?.avgTps === void 0 ? "—" : `${fmtInt$1(totals.avgTps)} tok/s`,
2247
+ value: totals?.avgTps === null || totals?.avgTps === void 0 ? "—" : `${fmtInt(totals.avgTps)} tok/s`,
2215
2248
  sub: totals?.avgTtftMs === null || totals?.avgTtftMs === void 0 ? t?.("ledger.speedNone") ?? "no timing reported" : t?.("ledger.ttft", { ms: fmtMs(totals.avgTtftMs) }) ?? `ttft ${fmtMs(totals.avgTtftMs)}`
2216
2249
  }),
2217
2250
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
@@ -2220,11 +2253,11 @@ window.__ModuleLoader__.load({
2220
2253
  const rate = cacheHitRate(summary);
2221
2254
  return rate === null || !summary.usageReported ? "—" : `${Math.round(rate * 100)}%`;
2222
2255
  })(),
2223
- sub: summary.usageReported ? t?.("ledger.cacheSub", { cached: fmtInt$1(summary.rows.reduce((n, r) => n + r.cachedTokens, 0)) }) ?? "" : ""
2256
+ sub: summary.usageReported ? t?.("ledger.cacheSub", { cached: fmtInt(summary.rows.reduce((n, r) => n + r.cachedTokens, 0)) }) ?? "" : ""
2224
2257
  }),
2225
2258
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2226
2259
  label: t?.("ledger.requests") ?? "Requests",
2227
- value: fmtInt$1(summary.calls),
2260
+ value: fmtInt(summary.calls),
2228
2261
  sub: summary.failedTurns === 0 ? t?.("ledger.allOk") ?? "no turn failed outright" : t?.("ledger.failedSub", {
2229
2262
  failed: summary.failedTurns,
2230
2263
  recovered: summary.recoveredTurns
@@ -2331,19 +2364,19 @@ window.__ModuleLoader__.load({
2331
2364
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: regionLabel(row.region, t) }),
2332
2365
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2333
2366
  className: "dsm-xdpool-num",
2334
- children: fmtInt$1(row.turns)
2367
+ children: fmtInt(row.turns)
2335
2368
  }),
2336
2369
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2337
2370
  className: "dsm-xdpool-num",
2338
- children: fmtInt$1(row.calls)
2371
+ children: fmtInt(row.calls)
2339
2372
  }),
2340
2373
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2341
2374
  className: "dsm-xdpool-num",
2342
- children: fmtInt$1(row.inputTokens)
2375
+ children: fmtInt(row.inputTokens)
2343
2376
  }),
2344
2377
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2345
2378
  className: "dsm-xdpool-num",
2346
- children: fmtInt$1(row.outputTokens)
2379
+ children: fmtInt(row.outputTokens)
2347
2380
  }),
2348
2381
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2349
2382
  className: "dsm-xdpool-ellipsis",
@@ -2393,7 +2426,7 @@ window.__ModuleLoader__.load({
2393
2426
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: regionLabel(row.region, t) }),
2394
2427
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2395
2428
  className: "dsm-xdpool-num",
2396
- children: fmtInt$1(row.calls)
2429
+ children: fmtInt(row.calls)
2397
2430
  }),
2398
2431
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2399
2432
  className: "dsm-xdpool-num",
@@ -9047,7 +9080,6 @@ window.__ModuleLoader__.load({
9047
9080
  * message.
9048
9081
  */
9049
9082
  async function callPack(method, payload) {
9050
- if (packRpcBridge === void 0) packRpcBridge = resolvePackRpcBridge();
9051
9083
  if (packRpcBridge !== void 0) try {
9052
9084
  const result = await packRpcBridge.call(method, payload);
9053
9085
  if (result !== null && typeof result === "object") {
@@ -9291,29 +9323,6 @@ window.__ModuleLoader__.load({
9291
9323
  }
9292
9324
  return rows;
9293
9325
  }
9294
- function creditStatusesFrom(envelope) {
9295
- const value = envelope.value;
9296
- if (value === null || typeof value !== "object") return [];
9297
- const accounts = value.accounts;
9298
- if (!Array.isArray(accounts)) return [];
9299
- const rows = [];
9300
- for (const entry of accounts) {
9301
- if (entry === null || typeof entry !== "object") continue;
9302
- const accountId = entry.accountId;
9303
- if (typeof accountId !== "string" || accountId === "") continue;
9304
- const nickname = entry.nickname;
9305
- const status = entry.status;
9306
- const statusRow = status !== null && typeof status === "object" ? status : void 0;
9307
- const todayCredit = statusRow?.["todayCredit"];
9308
- rows.push({
9309
- accountId,
9310
- ...typeof nickname === "string" && nickname !== "" ? { nickname } : {},
9311
- ...statusRow?.["todayCheckedIn"] === true ? { todayCheckedIn: true } : {},
9312
- ...typeof todayCredit === "number" && Number.isFinite(todayCredit) ? { todayCredit } : {}
9313
- });
9314
- }
9315
- return rows;
9316
- }
9317
9326
  function autoCheckinFrom(envelope) {
9318
9327
  const value = envelope.value;
9319
9328
  if (value === null || typeof value !== "object") return void 0;
@@ -9926,429 +9935,6 @@ window.__ModuleLoader__.load({
9926
9935
  });
9927
9936
  }
9928
9937
  //#endregion
9929
- //#region src/client/BillionPage.tsx
9930
- /**
9931
- * The billion-context page: what the compressor on this machine is doing.
9932
- *
9933
- * This page observes; it does not compress. billion-context is a separate
9934
- * plugin that very likely already runs here — on a stock desktop profile it is
9935
- * mounted by the profile's bundle layer and listens on its own loopback port.
9936
- * Everything below is read from that proxy's own JSON, relayed through
9937
- * `src/billion.ts`, and nothing on this page can change what the proxy does.
9938
- *
9939
- * Three states, three different things to say, and the difference matters more
9940
- * than it looks:
9941
- *
9942
- * - **missing** — no proxy recorded. Normal on a machine that never installed
9943
- * it. The page shows the install command instead of an empty table, because
9944
- * an empty table here would be indistinguishable from "compression ran and
9945
- * saved nothing".
9946
- * - **unreachable** — a proxy IS recorded but did not answer. Most often a
9947
- * restart in progress: the origin file is rewritten on every respawn. This
9948
- * must not read as an empty dashboard.
9949
- * - **ready** — the numbers.
9950
- *
9951
- * Every figure is the proxy's own. Nothing is re-derived, and in particular the
9952
- * cache hit rate below is weighted by the sessions actually shown rather than
9953
- * averaged across them: a page that averaged a 200-request session with a
9954
- * 1-request one would report a number that describes neither.
9955
- *
9956
- * WHAT THIS PAGE DOES NOT REDRAW
9957
- *
9958
- * billion-context ships its own dashboard, including a per-session detail view.
9959
- * The rows below therefore link out to it rather than re-implementing those
9960
- * screens, and the table itself starts collapsed: a fleet number is what this
9961
- * page is for, and 50 rows of session ids is a wall the user did not ask for.
9962
- * Both are deliberate — one answer, and the summary stays the summary.
9963
- *
9964
- * @module dsh-xdpool/client/BillionPage
9965
- */
9966
- /** Poll cadence, matching the other pages that watch a live process. */
9967
- const POLL_MS$1 = 15e3;
9968
- function fmtInt(value) {
9969
- return value.toLocaleString("en-US");
9970
- }
9971
- /** A token count in millions or billions, because 486,201,338 stops being a number. */
9972
- function compact$1(value) {
9973
- if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
9974
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
9975
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
9976
- return String(value);
9977
- }
9978
- /** `288.6K tokens`, or an em dash when the proxy has no measurement. */
9979
- function tokens(value) {
9980
- return value === 0 ? "—" : `${compact$1(value)} tokens`;
9981
- }
9982
- /** Local clock, or an em dash when the proxy never saw a request. */
9983
- function fmtLastSeen(iso) {
9984
- if (iso === "") return "—";
9985
- const at = Date.parse(iso);
9986
- if (Number.isNaN(at)) return "—";
9987
- const d = new Date(at);
9988
- return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
9989
- }
9990
- /**
9991
- * Fleet cache hit rate, or null when nothing was measured.
9992
- *
9993
- * Weighted by input tokens rather than averaged per session, and null — never
9994
- * zero — when no input was counted at all. The proxy reports `cacheHitPct: -1`
9995
- * for exactly that case, and rendering it as `0%` would claim the cache never
9996
- * helped, which is the opposite of what "not measured" means.
9997
- */
9998
- function fleetHitRate(sessions) {
9999
- let input = 0;
10000
- let cached = 0;
10001
- for (const row of sessions) {
10002
- if (row.cacheHitPct < 0) continue;
10003
- input += row.inputTokens + row.cachedTokens;
10004
- cached += row.cachedTokens;
10005
- }
10006
- return input === 0 ? null : cached / input;
10007
- }
10008
- /**
10009
- * A link into the proxy's own dashboard, or undefined when there is nothing to
10010
- * link to.
10011
- *
10012
- * Two things are checked here rather than trusted. The origin has to be an
10013
- * absolute http(s) URL, because a relative one would resolve against the HOST
10014
- * and silently open the wrong page; and the session id is percent-encoded,
10015
- * because these are proxy-chosen identifiers that may contain characters a
10016
- * hash route cannot carry raw.
10017
- *
10018
- * The hash is the proxy's own route shape (`#/sessions/<id>`), read from its
10019
- * shipped router — a deep link into someone else's UI, which is the whole
10020
- * reason the table links instead of redrawing.
10021
- */
10022
- function proxyLink(origin, hash) {
10023
- if (typeof origin !== "string" || origin === "") return void 0;
10024
- let base;
10025
- try {
10026
- base = new URL(origin);
10027
- } catch {
10028
- return;
10029
- }
10030
- if (base.protocol !== "http:" && base.protocol !== "https:") return void 0;
10031
- const url = new URL("/__bili/", base);
10032
- url.hash = hash;
10033
- return url.toString();
10034
- }
10035
- /**
10036
- * Open the proxy's dashboard in a new tab.
10037
- *
10038
- * `_blank` with `noopener` for the same reason the channels page does it: the
10039
- * proxy owns that tab afterwards, and this page must not be able to reach back
10040
- * into it. The settings panel itself is never navigated away from.
10041
- */
10042
- function openProxyDashboard(url) {
10043
- window.open(url, "_blank", "noopener");
10044
- }
10045
- function BillionPage({ t }) {
10046
- const [doc, setDoc] = (0, react.useState)(void 0);
10047
- const [failed, setFailed] = (0, react.useState)(false);
10048
- const mounted = (0, react.useRef)(true);
10049
- (0, react.useEffect)(() => () => {
10050
- mounted.current = false;
10051
- }, []);
10052
- const load = (0, react.useCallback)(async (signal) => {
10053
- try {
10054
- const response = await fetch(POOL_BILLION_PATH, {
10055
- headers: { accept: "application/json" },
10056
- credentials: "same-origin",
10057
- ...signal === void 0 ? {} : { signal }
10058
- });
10059
- if (!response.ok) {
10060
- if (mounted.current) setFailed(true);
10061
- return;
10062
- }
10063
- const value = await response.json().catch(() => void 0);
10064
- if (mounted.current && signal?.aborted !== true) {
10065
- setDoc(value);
10066
- setFailed(false);
10067
- }
10068
- } catch {
10069
- if (mounted.current && signal?.aborted !== true) setFailed(true);
10070
- }
10071
- }, []);
10072
- (0, react.useEffect)(() => {
10073
- const controller = new AbortController();
10074
- load(controller.signal);
10075
- const timer = window.setInterval(() => {
10076
- load(controller.signal);
10077
- }, POLL_MS$1);
10078
- return () => {
10079
- window.clearInterval(timer);
10080
- controller.abort();
10081
- };
10082
- }, [load]);
10083
- /**
10084
- * Fleet totals over the rows on screen.
10085
- *
10086
- * Summing the rows this page will render is what keeps the KPI row from
10087
- * disagreeing with the table under it — the same rule the pool dashboard
10088
- * follows, and for the same reason: a dashboard that gets trusted must not be
10089
- * able to contradict itself.
10090
- */
10091
- const totals = (0, react.useMemo)(() => {
10092
- const sessions = doc?.sessions ?? [];
10093
- let requests = 0;
10094
- let input = 0;
10095
- let cached = 0;
10096
- let output = 0;
10097
- let saved = 0;
10098
- let live = 0;
10099
- for (const row of sessions) {
10100
- requests += row.requests;
10101
- input += row.inputTokens;
10102
- cached += row.cachedTokens;
10103
- output += row.outputTokens;
10104
- saved += row.tokensSaved;
10105
- if (row.live) live += 1;
10106
- }
10107
- return {
10108
- sessions,
10109
- requests,
10110
- input,
10111
- cached,
10112
- output,
10113
- saved,
10114
- live
10115
- };
10116
- }, [doc]);
10117
- const hitRate = (0, react.useMemo)(() => fleetHitRate(totals.sessions), [totals.sessions]);
10118
- const ready = doc?.state === "ready";
10119
- /**
10120
- * The rows start hidden, and the user opens them on purpose.
10121
- *
10122
- * The summary above is what this page is for; the fleet table is the
10123
- * detail behind it. Collapsed is also the honest default on a machine with 50
10124
- * sessions, where an expanded table pushes every other control off screen.
10125
- */
10126
- const [rowsOpen, setRowsOpen] = (0, react.useState)(false);
10127
- const dashboard = proxyLink(doc?.origin, "#/overview");
10128
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
10129
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BILLION_CARD_CSS }),
10130
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageHero, {
10131
- title: t?.("billion.title") ?? "billion-context",
10132
- sub: t?.("billion.sub") ?? "Read-only view of the compressor running on this machine. This plugin never configures it.",
10133
- actions: dashboard === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10134
- type: "button",
10135
- className: "dsm-btn dsm-btn-ghost",
10136
- onClick: () => {
10137
- openProxyDashboard(dashboard);
10138
- },
10139
- children: t?.("billion.openDashboard") ?? "Open its dashboard"
10140
- }),
10141
- pills: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10142
- className: "dsm-xdpool-pill",
10143
- "data-tone": ready ? "on" : "off",
10144
- children: ready ? t?.("billion.pillRunning", { version: doc?.version ?? "?" }) ?? `running · v${doc?.version ?? "?"}` : t?.("billion.pillNotRunning") ?? "not running"
10145
- }), ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10146
- className: "dsm-xdpool-pill",
10147
- "data-tone": "off",
10148
- children: t?.("billion.pillReadOnly") ?? "read-only"
10149
- }) : null] })
10150
- }),
10151
- failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10152
- className: "dsm-xdpool-note",
10153
- role: "alert",
10154
- children: t?.("billion.failed") ?? "Could not reach the pool relay. The pool itself is unaffected."
10155
- }) : null,
10156
- doc === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10157
- className: "dsm-xdpool-note",
10158
- children: t?.("billion.loading") ?? "Looking for a billion-context proxy on this machine…"
10159
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [doc.state !== "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
10160
- className: "dsm-xdpool-sec",
10161
- children: [
10162
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
10163
- className: "dsm-xdpool-sectitle",
10164
- children: doc.state === "missing" ? t?.("billion.notInstalled") ?? "billion-context is not installed here" : t?.("billion.notAnswering") ?? "billion-context did not answer"
10165
- }),
10166
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10167
- className: "dsm-xdpool-note",
10168
- children: doc.reason ?? ""
10169
- }),
10170
- doc.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
10171
- className: "dsm-xdpool-steps",
10172
- children: [
10173
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", { children: [
10174
- "dsh plugin --profile ",
10175
- dshProfileHint(t),
10176
- " add billion-context"
10177
- ] }) }),
10178
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("billion.stepRestart") ?? "Restart DSH so the profile bundle layer mounts it." }),
10179
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("billion.stepBack") ?? "Come back to this page; it fills in on its own." })
10180
- ]
10181
- }) : null
10182
- ]
10183
- }) : null, ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10184
- className: "dsm-xdpool-kpis",
10185
- children: [
10186
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10187
- label: t?.("billion.sessions") ?? "Sessions",
10188
- value: fmtInt(doc.totalSessions),
10189
- sub: totals.live === 0 ? t?.("billion.noLive") ?? "none active right now" : t?.("billion.liveSub", { count: totals.live }) ?? `${totals.live} active right now`
10190
- }),
10191
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10192
- label: t?.("billion.requests") ?? "Requests",
10193
- value: fmtInt(totals.requests),
10194
- sub: t?.("billion.requestsSub") ?? "through the proxy, all lanes"
10195
- }),
10196
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10197
- label: t?.("billion.saved") ?? "Tokens saved",
10198
- value: compact$1(totals.saved),
10199
- sub: t?.("billion.savedSub") ?? "by compression, net of what it cost"
10200
- }),
10201
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10202
- label: t?.("billion.tokens") ?? "Tokens",
10203
- value: compact$1(totals.input),
10204
- sub: t?.("billion.tokensSub", {
10205
- cached: compact$1(totals.cached),
10206
- out: compact$1(totals.output)
10207
- }) ?? `${compact$1(totals.cached)} cached · ${compact$1(totals.output)} out`
10208
- }),
10209
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10210
- label: t?.("billion.cache") ?? "Cache hit",
10211
- value: hitRate === null ? "—" : `${Math.round(hitRate * 100)}%`,
10212
- sub: hitRate === null ? t?.("billion.cacheNone") ?? "nothing measured yet" : t?.("billion.cacheSub") ?? "of everything sent"
10213
- })
10214
- ]
10215
- }), totals.sessions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10216
- className: "dsm-xdpool-note",
10217
- children: t?.("billion.empty") ?? "The proxy is up but has seen no requests yet. Chat and this fills in."
10218
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
10219
- className: "dsm-xdpool-sec",
10220
- children: [
10221
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10222
- className: "dsm-xdpool-sechead",
10223
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
10224
- className: "dsm-xdpool-sectitle",
10225
- children: t?.("billion.perSession") ?? "Per session"
10226
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10227
- className: "dsm-xdpool-secbtns",
10228
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10229
- type: "button",
10230
- className: "dsm-xdpool-secghost",
10231
- "aria-expanded": rowsOpen,
10232
- onClick: () => {
10233
- setRowsOpen((open) => !open);
10234
- },
10235
- children: rowsOpen ? t?.("billion.hideRows") ?? "Hide rows" : t?.("billion.showRows", { count: totals.sessions.length }) ?? `Show ${totals.sessions.length} rows`
10236
- }), dashboard === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
10237
- className: "dsm-xdpool-secghost",
10238
- href: dashboard,
10239
- target: "_blank",
10240
- rel: "noopener noreferrer",
10241
- children: t?.("billion.openDashboard") ?? "Open the proxy's own dashboard"
10242
- })]
10243
- })]
10244
- }),
10245
- rowsOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10246
- className: "dsm-xdpool-tablewrap",
10247
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
10248
- className: "dsm-xdpool-table",
10249
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
10250
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colSession") ?? "Session" }),
10251
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colLast") ?? "Last seen" }),
10252
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colModel") ?? "Model" }),
10253
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10254
- className: "dsm-xdpool-num",
10255
- children: t?.("billion.colRequests") ?? "Requests"
10256
- }),
10257
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10258
- className: "dsm-xdpool-num",
10259
- children: t?.("billion.colContext") ?? "Context"
10260
- }),
10261
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10262
- className: "dsm-xdpool-num",
10263
- children: t?.("billion.colBlocks") ?? "Blocks"
10264
- }),
10265
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10266
- className: "dsm-xdpool-num",
10267
- children: t?.("billion.colSaved") ?? "Saved"
10268
- }),
10269
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10270
- className: "dsm-xdpool-num",
10271
- children: t?.("billion.colCache") ?? "Cache"
10272
- })
10273
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: totals.sessions.map((row) => {
10274
- const detail = proxyLink(doc?.origin, `#/sessions/${encodeURIComponent(row.id)}`);
10275
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
10276
- "data-live": row.live,
10277
- children: [
10278
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
10279
- className: "dsm-xdpool-ellipsis",
10280
- title: row.id,
10281
- children: [detail === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: row.id }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
10282
- className: "dsm-xdpool-sesslink",
10283
- href: detail,
10284
- target: "_blank",
10285
- rel: "noopener noreferrer",
10286
- title: row.id,
10287
- children: row.id
10288
- }), row.live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10289
- className: "dsm-xdpool-live",
10290
- "aria-label": "live"
10291
- }) : null]
10292
- }),
10293
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtLastSeen(row.lastSeen) }),
10294
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10295
- className: "dsm-xdpool-ellipsis",
10296
- title: row.lastModel,
10297
- children: row.lastModel === "" ? "—" : row.lastModel
10298
- }),
10299
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10300
- className: "dsm-xdpool-num",
10301
- children: fmtInt(row.requests)
10302
- }),
10303
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10304
- className: "dsm-xdpool-num",
10305
- title: t?.("billion.contextTitle", {
10306
- source: row.contextTokensSource,
10307
- window: row.contextWindow
10308
- }) ?? `source ${row.contextTokensSource} · window ${row.contextWindow}`,
10309
- children: tokens(row.contextTokens)
10310
- }),
10311
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10312
- className: "dsm-xdpool-num",
10313
- children: fmtInt(row.blocks)
10314
- }),
10315
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10316
- className: "dsm-xdpool-num",
10317
- children: compact$1(row.tokensSaved)
10318
- }),
10319
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10320
- className: "dsm-xdpool-num",
10321
- children: row.cacheHitPct < 0 ? "—" : `${row.cacheHitPct}%`
10322
- })
10323
- ]
10324
- }, row.id);
10325
- }) })]
10326
- })
10327
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10328
- className: "dsm-xdpool-note",
10329
- children: t?.("billion.rowsHint") ?? "Hidden by default. The summary above is the whole fleet; open the rows or the proxy’s own dashboard when you need a specific session."
10330
- }),
10331
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10332
- className: "dsm-xdpool-note",
10333
- children: t?.("billion.note") ?? "These numbers are the proxy’s own. Nothing here is an estimate, and this page never changes a setting."
10334
- })
10335
- ]
10336
- })] }) : null] })
10337
- ] });
10338
- }
10339
- /**
10340
- * The profile name for the install command.
10341
- *
10342
- * The host publishes it as an env var; when that is absent the command still
10343
- * works with `--profile` omitted on a single-profile install, so the hint says
10344
- * so rather than printing an empty flag.
10345
- */
10346
- function dshProfileHint(t) {
10347
- const profile = typeof process === "undefined" ? void 0 : process.env.DSH_PROFILE;
10348
- if (typeof profile === "string" && profile !== "") return profile;
10349
- return t?.("billion.defaultProfile") ?? "<profile>";
10350
- }
10351
- //#endregion
10352
9938
  //#region src/client/RoutingPage.tsx
10353
9939
  /**
10354
9940
  * The routing page: chains that are themselves models.
@@ -10435,8 +10021,10 @@ window.__ModuleLoader__.load({
10435
10021
  const next = value;
10436
10022
  setStatus(next);
10437
10023
  setError(void 0);
10438
- setDrafts((prev) => seeded.current ? prev : (next.combos ?? []).map(draftOf));
10439
- seeded.current = true;
10024
+ if (!seeded.current) {
10025
+ seeded.current = true;
10026
+ setDrafts((next.combos ?? []).map(draftOf));
10027
+ }
10440
10028
  }
10441
10029
  } catch (cause) {
10442
10030
  if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
@@ -11608,6 +11196,18 @@ window.__ModuleLoader__.load({
11608
11196
  *
11609
11197
  * @module dsh-xdpool/client/ChannelPoolSection
11610
11198
  */
11199
+ let poolSnapshot;
11200
+ /**
11201
+ * Drop the snapshot after anything that changes the data behind it.
11202
+ *
11203
+ * Logins, deletions, toggles and manual balance refreshes all make the snapshot
11204
+ * stale in a way a silent revalidation cannot hide: showing yesterday's account
11205
+ * list for a card the user just added an account to is worse than showing the
11206
+ * "reading" line for a moment.
11207
+ */
11208
+ function invalidatePoolSnapshot() {
11209
+ poolSnapshot = void 0;
11210
+ }
11611
11211
  /** How many automatic retries a cold-start mount gets before giving up. */
11612
11212
  const LOAD_RETRIES = 5;
11613
11213
  /** Gap between those retries, in milliseconds. */
@@ -11700,8 +11300,18 @@ window.__ModuleLoader__.load({
11700
11300
  return Number.isInteger(value) ? String(value) : value.toFixed(2);
11701
11301
  }
11702
11302
  function ChannelPoolSection({ t }) {
11703
- const [entries, setEntries] = (0, react.useState)([]);
11704
- const [error, setError] = (0, react.useState)(void 0);
11303
+ const [entries, setEntries] = (0, react.useState)(() => poolSnapshot?.entries ?? []);
11304
+ const [error, setError] = (0, react.useState)(() => poolSnapshot?.error);
11305
+ /**
11306
+ * True until the first wave has answered once.
11307
+ *
11308
+ * The section used to render NOTHING while loading, so a cold start and a
11309
+ * broken RPC bridge looked identical: blank (#95). The bridge failing is a
11310
+ * real, common failure — `provider.status` answers `ok: false`, the map comes
11311
+ * back empty, `scanProviders` is empty and the old code returned `<></>` with
11312
+ * `error === undefined`. Silence was indistinguishable from "no accounts".
11313
+ */
11314
+ const [loading, setLoading] = (0, react.useState)(() => poolSnapshot === void 0);
11705
11315
  const [notice, setNotice] = (0, react.useState)(void 0);
11706
11316
  /** provider → the open list, or `undefined` for a card with both folded. */
11707
11317
  const [folds, setFolds] = (0, react.useState)(/* @__PURE__ */ new Map());
@@ -11714,6 +11324,15 @@ window.__ModuleLoader__.load({
11714
11324
  const entriesRef = (0, react.useRef)(entries);
11715
11325
  entriesRef.current = entries;
11716
11326
  const mountedRef = (0, react.useRef)(true);
11327
+ /**
11328
+ * Which load is current.
11329
+ *
11330
+ * Two things make a stale answer arrive late: the retry ladder re-runs the
11331
+ * whole chain, and a remount starts a new chain while the old request is
11332
+ * still in flight. Stamping each run is what keeps an older wave-2 result
11333
+ * from painting over a newer wave-1 one.
11334
+ */
11335
+ const loadSeq = (0, react.useRef)(0);
11717
11336
  (0, react.useEffect)(() => () => {
11718
11337
  mountedRef.current = false;
11719
11338
  }, []);
@@ -11726,6 +11345,7 @@ window.__ModuleLoader__.load({
11726
11345
  });
11727
11346
  };
11728
11347
  const load = async () => {
11348
+ loadSeq.current += 1;
11729
11349
  try {
11730
11350
  const channels = (await fetchChannels()).filter((channel) => !REGION_OWNED_CHANNELS.has(channel.id));
11731
11351
  const statusAnswer = await callPack("provider.status", { providers: channels.map((channel) => channel.id) });
@@ -11735,25 +11355,17 @@ window.__ModuleLoader__.load({
11735
11355
  const wantBalances = channels.find((channel) => channel.id === provider)?.credits !== void 0;
11736
11356
  const accountsPromise = callPack("account.list", { provider });
11737
11357
  const modelsPromise = callPack("model.list", { provider });
11738
- const balancesPromise = wantBalances ? callPack("credits.balances", { provider }) : Promise.resolve(void 0);
11739
- const statusesPromise = wantBalances && channels.find((c) => c.id === provider)?.credits?.status === true ? callPack("credits.status", { provider }) : Promise.resolve(void 0);
11740
- const [accounts, models, balances, creditStatuses] = await Promise.all([
11741
- accountsPromise.catch(() => refused("account.list failed")),
11742
- modelsPromise.catch(() => refused("model.list failed")),
11743
- balancesPromise.catch(() => refused("credits.balances failed")),
11744
- statusesPromise.catch(() => refused("credits.status failed"))
11745
- ]);
11358
+ const [accounts, models] = await Promise.all([accountsPromise.catch(() => refused("account.list failed")), modelsPromise.catch(() => refused("model.list failed"))]);
11746
11359
  const modelRows = modelsFrom(models.value);
11747
11360
  const modelEmptyBecauseRefused = modelRows.length === 0 && models.ok !== true;
11748
- const balanceRows = balances === void 0 ? [] : balancesFrom(balances);
11749
11361
  return [provider, {
11750
11362
  accounts: dedupeAccounts(accountsFrom(accounts)),
11751
11363
  models: sortChannelModels(modelRows),
11752
11364
  facts: /* @__PURE__ */ new Map(),
11753
- balances: balanceRows,
11754
- statuses: creditStatuses === void 0 ? [] : creditStatusesFrom(creditStatuses),
11755
- balancePending: false,
11756
- balanceError: balances !== void 0 && !balances.ok && balanceRows.length === 0 ? balances.error?.message ?? "credits.balances refused" : void 0,
11365
+ balances: [],
11366
+ statuses: [],
11367
+ balancePending: wantBalances,
11368
+ balanceError: void 0,
11757
11369
  modelError: modelEmptyBecauseRefused ? models.error?.message ?? "model.list refused" : void 0
11758
11370
  }];
11759
11371
  }));
@@ -11764,25 +11376,48 @@ window.__ModuleLoader__.load({
11764
11376
  facts: /* @__PURE__ */ new Map(),
11765
11377
  status: statuses.get(channel.id)
11766
11378
  }));
11379
+ poolSnapshot = {
11380
+ entries: next,
11381
+ error: void 0
11382
+ };
11767
11383
  if (!mountedRef.current) return;
11384
+ const seq = loadSeq.current;
11768
11385
  setEntries(next);
11769
11386
  setError(void 0);
11387
+ setLoading(false);
11770
11388
  if (next.length === 0) {
11771
- if (!statusAnswer.ok) setError(statusAnswer.error?.message ?? "provider.status refused");
11389
+ if (!statusAnswer.ok) {
11390
+ const message = statusAnswer.error?.message ?? "provider.status refused";
11391
+ poolSnapshot = {
11392
+ entries: next,
11393
+ error: message
11394
+ };
11395
+ setError(message);
11396
+ }
11772
11397
  return;
11773
11398
  }
11399
+ for (const entry of next) {
11400
+ if (entry.channel.credits === void 0) continue;
11401
+ refreshBalances(entry.channel.id, false, true, seq);
11402
+ }
11774
11403
  const idsByProvider = {};
11775
11404
  for (const [provider, entry] of perProvider) if (entry.models.length > 0) idsByProvider[provider] = entry.models.map((model) => model.id);
11776
11405
  const facts = await fetchChannelModels(next.map((entry) => entry.channel.id), idsByProvider).catch(() => /* @__PURE__ */ new Map());
11777
- if (!mountedRef.current) return;
11406
+ if (!mountedRef.current || loadSeq.current !== seq) return;
11778
11407
  if (facts.size === 0) return;
11779
11408
  setEntries((previous) => previous.map((entry) => ({
11780
11409
  ...entry,
11781
11410
  facts: new Map((facts.get(entry.channel.id) ?? []).map((model) => [model.id, model]))
11782
11411
  })));
11783
11412
  } catch (cause) {
11413
+ const message = cause instanceof Error ? cause.message : String(cause);
11414
+ poolSnapshot = {
11415
+ entries: entriesRef.current,
11416
+ error: message
11417
+ };
11784
11418
  if (!mountedRef.current) return;
11785
- setError(cause instanceof Error ? cause.message : String(cause));
11419
+ setError(message);
11420
+ setLoading(false);
11786
11421
  }
11787
11422
  };
11788
11423
  /**
@@ -11936,8 +11571,20 @@ window.__ModuleLoader__.load({
11936
11571
  await load();
11937
11572
  }
11938
11573
  };
11939
- const refreshBalances = async (provider, force) => {
11940
- markBusy(provider, true);
11574
+ /**
11575
+ * Read one channel's balances and merge them into its card.
11576
+ *
11577
+ * Wave 2 of the load calls this with `quiet` true: the card is already on
11578
+ * screen, so the provider must not be marked busy (that would disable the
11579
+ * claim button for a network read the reader did not ask for) and a failure
11580
+ * must land in `balanceError` rather than in the section-wide notice.
11581
+ *
11582
+ * `seq` is the load that asked for this read. A reply from an older load
11583
+ * cannot overwrite a newer card, which is what the previous version got wrong
11584
+ * when a remount overlapped an in-flight refresh.
11585
+ */
11586
+ const refreshBalances = async (provider, force, quiet = false, seq) => {
11587
+ if (!quiet) markBusy(provider, true);
11941
11588
  setEntries((previous) => previous.map((entry) => entry.channel.id === provider ? {
11942
11589
  ...entry,
11943
11590
  balancePending: true
@@ -11947,7 +11594,21 @@ window.__ModuleLoader__.load({
11947
11594
  provider,
11948
11595
  force: true
11949
11596
  } : { provider });
11950
- if (!mountedRef.current) return;
11597
+ if (seq !== void 0 && loadSeq.current !== seq) return;
11598
+ if (!mountedRef.current) {
11599
+ if (envelope.ok) {
11600
+ const rows = balancesFrom(envelope);
11601
+ poolSnapshot = {
11602
+ entries: (poolSnapshot?.entries ?? []).map((entry) => entry.channel.id === provider ? {
11603
+ ...entry,
11604
+ balancePending: false,
11605
+ balances: rows
11606
+ } : entry),
11607
+ error: poolSnapshot?.error
11608
+ };
11609
+ }
11610
+ return;
11611
+ }
11951
11612
  if (!envelope.ok) {
11952
11613
  setEntries((previous) => previous.map((entry) => entry.channel.id === provider ? {
11953
11614
  ...entry,
@@ -11964,7 +11625,7 @@ window.__ModuleLoader__.load({
11964
11625
  balances: rows
11965
11626
  } : entry));
11966
11627
  } finally {
11967
- if (mountedRef.current) markBusy(provider, false);
11628
+ if (mountedRef.current && !quiet) markBusy(provider, false);
11968
11629
  }
11969
11630
  };
11970
11631
  const claim = async (provider) => {
@@ -12008,12 +11669,24 @@ window.__ModuleLoader__.load({
12008
11669
  return;
12009
11670
  }
12010
11671
  if (action === "test") setNotice(t?.("channelpool.testOk") ?? "Reachable.");
11672
+ invalidatePoolSnapshot();
12011
11673
  await load();
12012
11674
  } finally {
12013
11675
  if (mountedRef.current) markBusy(provider, false);
12014
11676
  }
12015
11677
  };
12016
- if (entries.length === 0 && error === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
11678
+ if (entries.length === 0 && error === void 0) {
11679
+ if (!loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
11680
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
11681
+ className: "dsm-xdpool-cpool",
11682
+ "aria-label": t?.("channelpool.title") ?? "Channels",
11683
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
11684
+ className: "dsm-xdpool-note",
11685
+ role: "status",
11686
+ children: t?.("channelpool.loading") ?? "Reading scan channels…"
11687
+ })
11688
+ });
11689
+ }
12017
11690
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
12018
11691
  className: "dsm-xdpool-cpool",
12019
11692
  "aria-label": t?.("channelpool.title") ?? "Channels",
@@ -12408,7 +12081,7 @@ window.__ModuleLoader__.load({
12408
12081
  *
12409
12082
  * @module dsh-xdpool/version
12410
12083
  */
12411
- const POOL_VERSION = "2.5.0";
12084
+ const POOL_VERSION = "2.5.1";
12412
12085
  //#endregion
12413
12086
  //#region src/client/PoolCard.tsx
12414
12087
  /**
@@ -12704,23 +12377,6 @@ window.__ModuleLoader__.load({
12704
12377
  /** Render pool health, per-account credits/cooldown, and the model directory. */
12705
12378
  function PoolCard({ t, settingsScope }) {
12706
12379
  const settingsWritable = settingsScope?.getSnapshot().writable === true;
12707
- /**
12708
- * Whether the plugin owns a row in the left sidebar (issue #70).
12709
- *
12710
- * Seeded from, and kept in step with, the settings mirror rather than from the
12711
- * pool status: the row is registered by `apply()`, before any card exists, and
12712
- * both sides must answer the same question the same way.
12713
- */
12714
- const [sidebarRow, setSidebarRowState] = (0, react.useState)(() => readSidebarRow(settingsScope));
12715
- const [sidebarRowBusy, setSidebarRowBusy] = (0, react.useState)(false);
12716
- (0, react.useEffect)(() => {
12717
- const unsubscribe = settingsScope?.subscribe?.(() => {
12718
- setSidebarRowState(readSidebarRow(settingsScope));
12719
- });
12720
- return () => {
12721
- unsubscribe?.();
12722
- };
12723
- }, [settingsScope]);
12724
12380
  /** Which region tab is showing. A CN-only install never leaves this. */
12725
12381
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
12726
12382
  /**
@@ -12791,10 +12447,6 @@ window.__ModuleLoader__.load({
12791
12447
  id: "ledger",
12792
12448
  label: t?.("shell.tabLedger") ?? "Dashboard"
12793
12449
  },
12794
- {
12795
- id: "billion",
12796
- label: t?.("shell.tabBillion") ?? "billion-context"
12797
- },
12798
12450
  {
12799
12451
  id: "routing",
12800
12452
  label: t?.("shell.tabRouting") ?? "Routing"
@@ -13381,18 +13033,11 @@ window.__ModuleLoader__.load({
13381
13033
  const imageModelIds = Object.entries(local).filter(([, e]) => e.images).map(([id]) => id);
13382
13034
  const contextBudgets = {};
13383
13035
  for (const [id, entry] of Object.entries(local)) if (entry.budget !== void 0) contextBudgets[id] = entry.budget;
13384
- if (await write.call(settingsScope, MODEL_SELECTION_KEY[region], {
13036
+ await write.call(settingsScope, MODEL_SELECTION_KEY[region], {
13385
13037
  enabledModelIds,
13386
13038
  imageModelIds,
13387
13039
  contextBudgets
13388
- }) === false) {
13389
- if (mounted.current) setError(t?.("row.modelsConflict") ?? "The settings changed elsewhere; review and save again");
13390
- return;
13391
- }
13392
- if (await refresh(region) === void 0) {
13393
- if (mounted.current) setFlash(t?.("row.modelsSaved") ?? "Saved");
13394
- return;
13395
- }
13040
+ });
13396
13041
  setDraftFor(region, void 0);
13397
13042
  if (mounted.current) setFlash(t?.("row.modelsSaved") ?? "Saved");
13398
13043
  } catch (cause) {
@@ -13429,35 +13074,6 @@ window.__ModuleLoader__.load({
13429
13074
  }
13430
13075
  };
13431
13076
  /**
13432
- * Turn the sidebar row on or off (issue #70).
13433
- *
13434
- * Written through the same settings field `apply()` reads, so the two agree
13435
- * without a new endpoint. The row itself is owned by the bundle registration,
13436
- * so the mirror's change notification is what makes this take effect without a
13437
- * restart on hosts that publish one; everywhere else it lands on next launch,
13438
- * which the hint text says rather than implies.
13439
- */
13440
- const setSidebarRow = async (next) => {
13441
- const write = settingsScope?.set;
13442
- if (write === void 0) {
13443
- setError(t?.("row.modelsSaveError", { message: "settings scope is read-only" }) ?? "settings scope is read-only");
13444
- return;
13445
- }
13446
- setSidebarRowBusy(true);
13447
- setFlash(void 0);
13448
- try {
13449
- if (await write.call(settingsScope, "sidebarRow", next) === false) {
13450
- if (mounted.current) setError(t?.("row.modelsConflict") ?? "The settings changed elsewhere; review and save again");
13451
- return;
13452
- }
13453
- if (mounted.current) setSidebarRowState(next);
13454
- } catch (cause) {
13455
- if (mounted.current) setError(String(cause));
13456
- } finally {
13457
- if (mounted.current) setSidebarRowBusy(false);
13458
- }
13459
- };
13460
- /**
13461
13077
  * Switch the daily-points automation on or off.
13462
13078
  *
13463
13079
  * The whole `automation` object is written as one key, because that is how the
@@ -13675,7 +13291,7 @@ window.__ModuleLoader__.load({
13675
13291
  children: page === "aggregate" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AggregatePage, { t }) : page === "channels" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChannelsPage, { t }) : page === "ledger" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LedgerPage, {
13676
13292
  t,
13677
13293
  usage
13678
- }) : page === "billion" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BillionPage, { t }) : page === "routing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoutingPage, { t }) : page === "gateway" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GatewayPage, { t }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
13294
+ }) : page === "routing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoutingPage, { t }) : page === "gateway" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GatewayPage, { t }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
13679
13295
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: POOL_CARD_CSS }),
13680
13296
  usage === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
13681
13297
  className: "dsm-xdpool-card dsm-xdpool-usage",
@@ -13732,30 +13348,6 @@ window.__ModuleLoader__.load({
13732
13348
  })
13733
13349
  ]
13734
13350
  }),
13735
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
13736
- className: "dsm-xdpool-card dsm-xdpool-sidebar-row",
13737
- "aria-label": t?.("row.sidebarRowTitle") ?? "Sidebar entry",
13738
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13739
- className: "dsm-xdpool-sidebar-row-head",
13740
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
13741
- className: "dsm-xdpool-usage-title",
13742
- children: t?.("row.sidebarRowTitle") ?? "Sidebar entry"
13743
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
13744
- type: "button",
13745
- role: "switch",
13746
- "aria-checked": sidebarRow,
13747
- disabled: !settingsWritable || sidebarRowBusy,
13748
- className: `dsm-xdpool-auto-switch${sidebarRow ? " dsm-xdpool-auto-switch-on" : ""}`,
13749
- onClick: () => {
13750
- setSidebarRow(!sidebarRow);
13751
- },
13752
- children: sidebarRowBusy ? t?.("row.autoBusy") ?? "Saving…" : sidebarRow ? t?.("row.autoOn") ?? "On" : t?.("row.autoOff") ?? "Off"
13753
- })]
13754
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
13755
- className: "dsm-xdpool-usage-note",
13756
- children: sidebarRow ? t?.("row.sidebarRowOn") ?? "XD Pool has its own row in the left sidebar. Turn it off to open the pool from Settings instead." : t?.("row.sidebarRowOff") ?? "The sidebar row is hidden; the pool still opens from Settings → XD Pool, and models are unaffected."
13757
- })]
13758
- }),
13759
13351
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
13760
13352
  className: "dsm-xdpool-regions",
13761
13353
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -14700,6 +14292,20 @@ window.__ModuleLoader__.load({
14700
14292
  global: "xdpool-global"
14701
14293
  };
14702
14294
  function ModelRow({ model, region, paidTwin, t, draft, editable, onToggle, onToggleImage, onBudget, onTest, probe, probing }) {
14295
+ /**
14296
+ * A per-MOUNTED-ROW scope for the two budget radios.
14297
+ *
14298
+ * HTML groups radios by `name`, and React renders both region cards at once —
14299
+ * the collapsible panes stay in the DOM with `hidden`, they are not unmounted.
14300
+ * A `name` built from the model id alone is therefore the SAME name in the
14301
+ * cn card and the global card, so the two rows joined one radio group: the
14302
+ * browser keeps only the last checked member, and whichever card renders
14303
+ * later (global) silently un-checks the cn row's "1M". Adding the region
14304
+ * would not be enough either — the settings page and the sidebar pane can
14305
+ * both mount the same region — so the scope has to name the row instance,
14306
+ * which is what `useId` is for.
14307
+ */
14308
+ const radioScope = (0, react.useId)();
14703
14309
  const now = /* @__PURE__ */ new Date();
14704
14310
  const tag = tagFor(model, now, region);
14705
14311
  const promo = promoStatusFor(model, now, region);
@@ -14768,7 +14374,7 @@ window.__ModuleLoader__.load({
14768
14374
  "aria-label": t?.("row.modelContextBudget") ?? "Context",
14769
14375
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14770
14376
  type: "radio",
14771
- name: `budget-${model.id}`,
14377
+ name: `budget-${radioScope}`,
14772
14378
  checked: currentBudget === DEFAULT_CONTEXT_BUDGET,
14773
14379
  disabled: !editable,
14774
14380
  onChange: () => {
@@ -14776,7 +14382,7 @@ window.__ModuleLoader__.load({
14776
14382
  }
14777
14383
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(DEFAULT_CONTEXT_BUDGET) })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14778
14384
  type: "radio",
14779
- name: `budget-${model.id}`,
14385
+ name: `budget-${radioScope}`,
14780
14386
  checked: currentBudget === native,
14781
14387
  disabled: !editable,
14782
14388
  onChange: () => {
@@ -15076,10 +14682,6 @@ window.__ModuleLoader__.load({
15076
14682
  "row.modelsSaving": "Saving…",
15077
14683
  "row.modelsDiscard": "Discard",
15078
14684
  "row.modelsSaved": "Model selection saved",
15079
- "row.sidebarRowTitle": "Sidebar entry",
15080
- "row.sidebarRowOn": "XD Pool has its own row in the left sidebar. Turn it off to open the pool from Settings instead.",
15081
- "row.sidebarRowOff": "The sidebar row is hidden; the pool still opens from Settings → XD Pool, and models are unaffected.",
15082
- "row.modelsConflict": "The settings changed elsewhere; review and save again",
15083
14685
  "row.modelsSaveError": "Could not save: {message}",
15084
14686
  "row.modelsEmpty": "No model enabled — enable at least one before saving.",
15085
14687
  "row.catalogOffline": "built-in list (offline)",
@@ -15295,6 +14897,12 @@ window.__ModuleLoader__.load({
15295
14897
  "shell.brand": "XD Pool",
15296
14898
  "shell.star": "Star",
15297
14899
  "shell.starHint": "Open this plugin on GitHub",
14900
+ "shell.group": "Community",
14901
+ "shell.groupHint": "Scan the code to join the XD community group",
14902
+ "shell.groupTitle": "XD community group",
14903
+ "shell.groupAlt": "WeChat group code: scan it to join the XD community group",
14904
+ "shell.groupClose": "Close",
14905
+ "shell.groupNote": "Scan with WeChat to join the XD community group. The code is valid for 7 days; if it has expired, open an issue on GitHub and it will be refreshed.",
15298
14906
  "shell.tabPool": "Account pool",
15299
14907
  "shell.tabChannels": "Login channels",
15300
14908
  "shell.tabAggregate": "Providers",
@@ -15303,8 +14911,6 @@ window.__ModuleLoader__.load({
15303
14911
  "shell.subAggregate": "Bring your own API keys: any OpenAI-compatible provider, reachable as extra models.",
15304
14912
  "shell.tabLedger": "Dashboard",
15305
14913
  "shell.subLedger": "What this machine sent and got back, since the pool last started.",
15306
- "shell.tabBillion": "billion-context",
15307
- "shell.subBillion": "A read-only window onto the compressor running beside this pool.",
15308
14914
  "shell.tabRouting": "Routing",
15309
14915
  "shell.subRouting": "Chains that try models in order, so one busy or rate-limited model falls through to the next.",
15310
14916
  "shell.tabGateway": "Gateway",
@@ -15387,48 +14993,9 @@ window.__ModuleLoader__.load({
15387
14993
  "ledger.unattributed": "(unattributed)",
15388
14994
  "ledger.cn": "CN",
15389
14995
  "ledger.global": "Global",
15390
- "billion.title": "billion-context",
15391
- "billion.sub": "What the compressor on this machine is doing. This page reads it; it cannot change it.",
15392
- "billion.pillRunning": "running · v{version}",
15393
- "billion.pillNotRunning": "not running",
15394
- "billion.pillReadOnly": "read-only",
15395
- "billion.failed": "Could not reach the pool relay. The pool itself is unaffected.",
15396
- "billion.loading": "Looking for a billion-context proxy on this machine…",
15397
- "billion.notInstalled": "billion-context is not installed on this machine",
15398
- "billion.notAnswering": "A billion-context proxy is registered but did not answer",
15399
- "billion.stepRestart": "Restart DSH so the profile bundle layer mounts it.",
15400
- "billion.stepBack": "Come back to this page; it fills in on its own.",
15401
- "billion.defaultProfile": "<profile>",
15402
- "billion.sessions": "Sessions",
15403
- "billion.noLive": "none active right now",
15404
- "billion.liveSub": "{count} still active right now",
15405
- "billion.requests": "Requests",
15406
- "billion.requestsSub": "through the proxy, every lane",
15407
- "billion.saved": "Tokens saved",
15408
- "billion.savedSub": "by compression, net of what it cost",
15409
- "billion.tokens": "Tokens",
15410
- "billion.tokensSub": "{cached} cached · {out} out",
15411
- "billion.cache": "Cache hit",
15412
- "billion.cacheNone": "nothing measured yet",
15413
- "billion.cacheSub": "of everything sent",
15414
- "billion.empty": "The proxy is up but has seen no requests yet. Chat and this fills in.",
15415
- "billion.perSession": "Per session",
15416
- "billion.showRows": "Show {count} rows",
15417
- "billion.hideRows": "Hide rows",
15418
- "billion.openDashboard": "Open its dashboard",
15419
- "billion.rowsHint": "The fleet summary above is the whole picture. Open the rows for this proxy’s numbers, or the proxy’s own dashboard when you need a single session.",
15420
- "billion.note": "These are the proxy’s own numbers, read over loopback. Nothing here is an estimate.",
15421
- "billion.colSession": "Session",
15422
- "billion.colLast": "Last seen",
15423
- "billion.colModel": "Model",
15424
- "billion.colRequests": "Requests",
15425
- "billion.colContext": "Context",
15426
- "billion.colBlocks": "Blocks",
15427
- "billion.colSaved": "Saved",
15428
- "billion.colCache": "Cache",
15429
- "billion.contextTitle": "measured {source} · model window {window}",
15430
14996
  "channelpool.title": "Scan channels",
15431
14997
  "channelpool.accountCount": "{count} account(s)",
14998
+ "channelpool.loading": "Reading scan channels…",
15432
14999
  "channelpool.disabled": "disabled",
15433
15000
  "channelpool.noModels": "No models published yet.",
15434
15001
  "channelpool.modelError": "Could not list models: {message}",
@@ -15591,10 +15158,6 @@ window.__ModuleLoader__.load({
15591
15158
  "row.modelsSaving": "保存中…",
15592
15159
  "row.modelsDiscard": "放弃修改",
15593
15160
  "row.modelsSaved": "模型选择已保存",
15594
- "row.sidebarRowTitle": "侧栏入口",
15595
- "row.sidebarRowOn": "左侧栏会给 XD Pool 留一行入口。关掉后改为从「设置 → XD Pool」进入,模型池不受影响。",
15596
- "row.sidebarRowOff": "侧栏入口已隐藏;仍可从「设置 → XD Pool」进入,模型池不受影响。",
15597
- "row.modelsConflict": "设置已在别处被修改,请确认后重新保存",
15598
15161
  "row.modelsSaveError": "保存失败:{message}",
15599
15162
  "row.modelsEmpty": "至少要启用一个模型才能保存。",
15600
15163
  "row.catalogOffline": "内置列表(离线)",
@@ -15810,6 +15373,12 @@ window.__ModuleLoader__.load({
15810
15373
  "shell.brand": "XD Pool",
15811
15374
  "shell.star": "点Star",
15812
15375
  "shell.starHint": "在 GitHub 上给这个插件点个星",
15376
+ "shell.group": "交流群",
15377
+ "shell.groupHint": "扫码加入 XD 交流群",
15378
+ "shell.groupTitle": "XD 交流群",
15379
+ "shell.groupAlt": "微信群二维码:用微信扫一扫加入 XD 交流群",
15380
+ "shell.groupClose": "关闭",
15381
+ "shell.groupNote": "用微信「扫一扫」加入 XD 交流群。二维码 7 天内有效,过期了在 GitHub 提个 issue 说一声,会更新。",
15813
15382
  "shell.tabPool": "账号池",
15814
15383
  "shell.tabChannels": "扫码登录",
15815
15384
  "shell.tabAggregate": "聚合渠道",
@@ -15818,8 +15387,6 @@ window.__ModuleLoader__.load({
15818
15387
  "shell.subAggregate": "带上你自己的 API key:任何兼容 OpenAI 的服务都能变成额外模型。",
15819
15388
  "shell.tabLedger": "数据看板",
15820
15389
  "shell.subLedger": "这台机器发出去什么、收回来什么,从池子上次启动算起。",
15821
- "shell.tabBillion": "billion-context",
15822
- "shell.subBillion": "一个只读的窗口,看着旁边跑着的那台压缩器。",
15823
15390
  "shell.tabRouting": "路由",
15824
15391
  "shell.subRouting": "把几个模型串成一条链,按顺序试;一个忙了或被限流,就自动落到下一个。",
15825
15392
  "shell.tabGateway": "网关",
@@ -15902,48 +15469,9 @@ window.__ModuleLoader__.load({
15902
15469
  "ledger.unattributed": "(没归属到账号)",
15903
15470
  "ledger.cn": "国内版",
15904
15471
  "ledger.global": "国际版",
15905
- "billion.title": "billion-context",
15906
- "billion.sub": "这台机器上的压缩器正在做什么。这一页只看不改,它动不了那个代理。",
15907
- "billion.pillRunning": "运行中 · v{version}",
15908
- "billion.pillNotRunning": "没在运行",
15909
- "billion.pillReadOnly": "只读",
15910
- "billion.failed": "连不上池子的中继接口,池子本身不受影响。",
15911
- "billion.loading": "正在这台机器上找 billion-context 代理…",
15912
- "billion.notInstalled": "这台机器上没装 billion-context",
15913
- "billion.notAnswering": "登记过一个 billion-context 代理,但它没应答",
15914
- "billion.stepRestart": "重启 DSH,让 profile 的 bundle 层把它挂上。",
15915
- "billion.stepBack": "回到这一页,数字会自己填上。",
15916
- "billion.defaultProfile": "<profile>",
15917
- "billion.sessions": "会话数",
15918
- "billion.noLive": "当前没有活跃会话",
15919
- "billion.liveSub": "当前还有 {count} 个在跑",
15920
- "billion.requests": "请求数",
15921
- "billion.requestsSub": "经过代理的,全部线路",
15922
- "billion.saved": "省下的 Token",
15923
- "billion.savedSub": "压缩净省的,已经扣掉它自己花掉的",
15924
- "billion.tokens": "Token 总量",
15925
- "billion.tokensSub": "{cached} 来自缓存 · {out} 输出",
15926
- "billion.cache": "缓存命中",
15927
- "billion.cacheNone": "还没有测量数据",
15928
- "billion.cacheSub": "在送出去的所有 token 里",
15929
- "billion.empty": "代理起来了,但还没收到过请求,聊两句这里就填上了。",
15930
- "billion.perSession": "按会话看",
15931
- "billion.showRows": "展开 {count} 行",
15932
- "billion.hideRows": "收起",
15933
- "billion.openDashboard": "打开它自己的仪表盘",
15934
- "billion.rowsHint": "上面的全局数字就是全部情况。要看这一个代理的明细就展开表格,要看单个会话就去它自己的仪表盘。",
15935
- "billion.note": "这些是代理自己报的数字,走的是本机回环。这里没有一个是估算值。",
15936
- "billion.colSession": "会话",
15937
- "billion.colLast": "最后出现",
15938
- "billion.colModel": "模型",
15939
- "billion.colRequests": "请求",
15940
- "billion.colContext": "上下文",
15941
- "billion.colBlocks": "压缩块",
15942
- "billion.colSaved": "省下",
15943
- "billion.colCache": "缓存",
15944
- "billion.contextTitle": "实测 {source} · 模型窗口 {window}",
15945
15472
  "channelpool.title": "扫码渠道",
15946
15473
  "channelpool.accountCount": "{count} 个账号",
15474
+ "channelpool.loading": "正在读取扫码渠道…",
15947
15475
  "channelpool.disabled": "已停用",
15948
15476
  "channelpool.noModels": "该渠道还没有发布模型。",
15949
15477
  "channelpool.modelError": "读取模型列表失败:{message}",
@@ -16046,29 +15574,12 @@ window.__ModuleLoader__.load({
16046
15574
  }, PoolCard));
16047
15575
  installNavIcon(ctx, () => t("row.navLabel"));
16048
15576
  try {
16049
- let disposeRow;
16050
- const syncSidebarRow = () => {
16051
- const wanted = readSidebarRow(settingsScope);
16052
- if (wanted && disposeRow === void 0) disposeRow = ctx.slots.register({
16053
- name: "sidebar.panellist",
16054
- id: "xdpool",
16055
- order: 20,
16056
- label: () => t("row.navLabel")
16057
- }, PanelIcon);
16058
- else if (!wanted && disposeRow !== void 0) {
16059
- disposeRow();
16060
- disposeRow = void 0;
16061
- }
16062
- };
16063
- ctx.slots.inject("sidebar.panellist", () => {
16064
- syncSidebarRow();
16065
- const unsubscribe = settingsScope?.subscribe?.(syncSidebarRow);
16066
- return () => {
16067
- unsubscribe?.();
16068
- disposeRow?.();
16069
- disposeRow = void 0;
16070
- };
16071
- });
15577
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
15578
+ name: "sidebar.panellist",
15579
+ id: "xdpool",
15580
+ order: 20,
15581
+ label: () => t("row.navLabel")
15582
+ }, PanelIcon));
16072
15583
  ctx.slots.inject("main", () => ctx.slots.register({
16073
15584
  name: "main",
16074
15585
  key: "xdpool",