dsh-xdpool 2.4.6 → 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
@@ -162,22 +162,6 @@ window.__ModuleLoader__.load({
162
162
  */
163
163
  const POOL_RELAY_PATH = "/plugins/dsh-xdpool/relay";
164
164
  /**
165
- * What the locally running billion-context proxy is doing, read-only.
166
- *
167
- * Its own route because the answer comes from ANOTHER plugin: billion-context
168
- * runs its own loopback listener and publishes its own JSON, so this route only
169
- * finds that listener and relays a filtered subset. Nothing here is folded into
170
- * {@link POOL_STATUS_PATH}: the pool card polls that every 30 seconds, and a
171
- * third-party proxy's session table is not something every pool poll should
172
- * carry.
173
- *
174
- * The word "read-only" is load-bearing on both ends. This plugin sends GETs
175
- * only — it never configures, restarts or writes to the proxy — and the payload
176
- * it relays deliberately omits every field that could carry conversation
177
- * material.
178
- */
179
- const POOL_BILLION_PATH = "/plugins/dsh-xdpool/billion";
180
- /**
181
165
  * The pack's own management route, which the card posts to directly.
182
166
  *
183
167
  * Not a re-export of anything: the vendored bundle registers this path itself
@@ -1111,6 +1095,31 @@ window.__ModuleLoader__.load({
1111
1095
  .dsm-xdpool-star svg{fill:currentColor;transition:transform .5s cubic-bezier(.34,1.56,.64,1)}
1112
1096
  .dsm-xdpool-star:hover svg{transform:rotate(72deg) scale(1.12)}
1113
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}
1114
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%)}
1115
1124
  /* A raised surface, not a tinted wash: the glider has to read as "this one is
1116
1125
  sitting on top of the strip" from across the panel, and a wash alone is
@@ -1158,29 +1167,6 @@ window.__ModuleLoader__.load({
1158
1167
  /* Long model ids and account emails get clipped rather than wrapped: a wrapped
1159
1168
  number column breaks the alignment that makes it scannable. */
1160
1169
 
1161
- `.trim();
1162
- const BILLION_CARD_CSS = `
1163
-
1164
- .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)}
1165
- .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}
1166
- /* A session with requests still in flight. The dot is the only motion on this
1167
- page; nothing animates while idle, so a still dot means still idle. */
1168
- .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)}
1169
- /* Section header that carries its own controls: title on the left, actions on
1170
- the right, both on one baseline. */
1171
- .dsm-xdpool-sechead{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
1172
- .dsm-xdpool-sechead .dsm-xdpool-sectitle{margin:0}
1173
- .dsm-xdpool-secbtns{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
1174
- /* A control that reads as a link-shaped button: it only navigates or reveals,
1175
- it never commits state, so it stays quiet until hovered. */
1176
- .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)}
1177
- .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)}
1178
- .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)}
1179
- /* The session id itself. A deep link into another plugin's UI, so it has to
1180
- read as a link without pretending to be the pool's own navigation. */
1181
- .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}
1182
- .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)}
1183
-
1184
1170
  `.trim();
1185
1171
  const ROUTING_CARD_CSS = `
1186
1172
 
@@ -1369,9 +1355,8 @@ window.__ModuleLoader__.load({
1369
1355
  .dsm-btn-primary:hover:not(:disabled){opacity:.9}
1370
1356
  /* The borderless third of the button family: links out to a vendor's console
1371
1357
  and the destructive "forget this key" action. It had no rule at all, so the
1372
- aggregate row's three links and the billion page's "open its dashboard"
1373
- rendered as bare browser buttons — which is how a secondary action ends up
1374
- 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. */
1375
1360
  .dsm-btn-ghost{border-color:transparent;background:transparent;color:var(--dsw-alias-label-tertiary,#8a90a0)}
1376
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}
1377
1362
  .dsm-xdpool-panel-empty{color:var(--dsw-alias-label-tertiary,#999);font-size:14px;line-height:20px}
@@ -1704,6 +1689,10 @@ window.__ModuleLoader__.load({
1704
1689
  */
1705
1690
  const POOL_CARD_AVATAR = "data:image/jpeg;base64,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";
1706
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
1707
1696
  //#region src/client/shell.tsx
1708
1697
  /**
1709
1698
  * The card shell: brand bar, page tabs, per-page hero.
@@ -1730,7 +1719,8 @@ window.__ModuleLoader__.load({
1730
1719
  * highlight on the correct button at the correct x, in the wrong row.
1731
1720
  *
1732
1721
  * 3. **Nothing here is a gate.** The star button opens the repository in a new
1733
- * 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
1734
1724
  * that, on a machine without outbound network, looks exactly like a broken
1735
1725
  * plugin.
1736
1726
  *
@@ -1824,6 +1814,26 @@ window.__ModuleLoader__.load({
1824
1814
  function PoolShell({ t, version, avatar, tabs, active, onSelect, hero, children }) {
1825
1815
  /** Measured box of the active tab's button, in tab-strip coordinates. */
1826
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]);
1827
1837
  const tabRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
1828
1838
  const tabsRef = (0, react.useRef)(null);
1829
1839
  const measure = (0, react.useCallback)(() => {
@@ -1903,6 +1913,28 @@ window.__ModuleLoader__.load({
1903
1913
  })
1904
1914
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("shell.star") ?? "Star on GitHub" })]
1905
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
+ }),
1906
1938
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1907
1939
  className: "dsm-xdpool-navtabs",
1908
1940
  role: "tablist",
@@ -1937,6 +1969,46 @@ window.__ModuleLoader__.load({
1937
1969
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1938
1970
  className: "dsm-xdpool-body",
1939
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
+ })
1940
2012
  })
1941
2013
  ]
1942
2014
  });
@@ -1986,7 +2058,7 @@ window.__ModuleLoader__.load({
1986
2058
  */
1987
2059
  /** How many recent calls the detail table lists. */
1988
2060
  const DETAIL_ROWS = 12;
1989
- function fmtInt$1(value) {
2061
+ function fmtInt(value) {
1990
2062
  return value.toLocaleString("en-US");
1991
2063
  }
1992
2064
  /** Milliseconds as `1.2s` / `340ms`, because `1200ms` is not a unit anyone reads. */
@@ -1995,7 +2067,7 @@ window.__ModuleLoader__.load({
1995
2067
  }
1996
2068
  /** Tokens per second, kept integer: 12.4 tok/s reads as false precision. */
1997
2069
  function fmtTps(value) {
1998
- return value === null ? "—" : `${fmtInt$1(value)}`;
2070
+ return value === null ? "—" : `${fmtInt(value)}`;
1999
2071
  }
2000
2072
  function fmtClock(at) {
2001
2073
  const d = new Date(at);
@@ -2035,9 +2107,9 @@ window.__ModuleLoader__.load({
2035
2107
  "—"
2036
2108
  ];
2037
2109
  return [
2038
- fmtInt$1(call.usage.inputTokens),
2039
- fmtInt$1(call.usage.outputTokens),
2040
- fmtInt$1(call.usage.cachedTokens)
2110
+ fmtInt(call.usage.inputTokens),
2111
+ fmtInt(call.usage.outputTokens),
2112
+ fmtInt(call.usage.cachedTokens)
2041
2113
  ];
2042
2114
  }
2043
2115
  function LedgerPage({ t, usage: shared }) {
@@ -2159,20 +2231,20 @@ window.__ModuleLoader__.load({
2159
2231
  children: [
2160
2232
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2161
2233
  label: t?.("ledger.tokens") ?? "Tokens",
2162
- 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",
2163
2235
  sub: summary.usageReported ? t?.("ledger.tokenSplit", {
2164
- input: fmtInt$1(totals?.input ?? 0),
2165
- output: fmtInt$1(totals?.output ?? 0)
2166
- }) ?? `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"
2167
2239
  }),
2168
2240
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2169
2241
  label: t?.("ledger.turns") ?? "Turns",
2170
- value: fmtInt$1(summary.turns),
2242
+ value: fmtInt(summary.turns),
2171
2243
  sub: t?.("ledger.callsSub", { calls: summary.calls }) ?? `${summary.calls} upstream calls`
2172
2244
  }),
2173
2245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2174
2246
  label: t?.("ledger.speed") ?? "Speed",
2175
- 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`,
2176
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)}`
2177
2249
  }),
2178
2250
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
@@ -2181,11 +2253,11 @@ window.__ModuleLoader__.load({
2181
2253
  const rate = cacheHitRate(summary);
2182
2254
  return rate === null || !summary.usageReported ? "—" : `${Math.round(rate * 100)}%`;
2183
2255
  })(),
2184
- 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)) }) ?? "" : ""
2185
2257
  }),
2186
2258
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
2187
2259
  label: t?.("ledger.requests") ?? "Requests",
2188
- value: fmtInt$1(summary.calls),
2260
+ value: fmtInt(summary.calls),
2189
2261
  sub: summary.failedTurns === 0 ? t?.("ledger.allOk") ?? "no turn failed outright" : t?.("ledger.failedSub", {
2190
2262
  failed: summary.failedTurns,
2191
2263
  recovered: summary.recoveredTurns
@@ -2292,19 +2364,19 @@ window.__ModuleLoader__.load({
2292
2364
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: regionLabel(row.region, t) }),
2293
2365
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2294
2366
  className: "dsm-xdpool-num",
2295
- children: fmtInt$1(row.turns)
2367
+ children: fmtInt(row.turns)
2296
2368
  }),
2297
2369
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2298
2370
  className: "dsm-xdpool-num",
2299
- children: fmtInt$1(row.calls)
2371
+ children: fmtInt(row.calls)
2300
2372
  }),
2301
2373
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2302
2374
  className: "dsm-xdpool-num",
2303
- children: fmtInt$1(row.inputTokens)
2375
+ children: fmtInt(row.inputTokens)
2304
2376
  }),
2305
2377
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2306
2378
  className: "dsm-xdpool-num",
2307
- children: fmtInt$1(row.outputTokens)
2379
+ children: fmtInt(row.outputTokens)
2308
2380
  }),
2309
2381
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2310
2382
  className: "dsm-xdpool-ellipsis",
@@ -2354,7 +2426,7 @@ window.__ModuleLoader__.load({
2354
2426
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: regionLabel(row.region, t) }),
2355
2427
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2356
2428
  className: "dsm-xdpool-num",
2357
- children: fmtInt$1(row.calls)
2429
+ children: fmtInt(row.calls)
2358
2430
  }),
2359
2431
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2360
2432
  className: "dsm-xdpool-num",
@@ -9251,29 +9323,6 @@ window.__ModuleLoader__.load({
9251
9323
  }
9252
9324
  return rows;
9253
9325
  }
9254
- function creditStatusesFrom(envelope) {
9255
- const value = envelope.value;
9256
- if (value === null || typeof value !== "object") return [];
9257
- const accounts = value.accounts;
9258
- if (!Array.isArray(accounts)) return [];
9259
- const rows = [];
9260
- for (const entry of accounts) {
9261
- if (entry === null || typeof entry !== "object") continue;
9262
- const accountId = entry.accountId;
9263
- if (typeof accountId !== "string" || accountId === "") continue;
9264
- const nickname = entry.nickname;
9265
- const status = entry.status;
9266
- const statusRow = status !== null && typeof status === "object" ? status : void 0;
9267
- const todayCredit = statusRow?.["todayCredit"];
9268
- rows.push({
9269
- accountId,
9270
- ...typeof nickname === "string" && nickname !== "" ? { nickname } : {},
9271
- ...statusRow?.["todayCheckedIn"] === true ? { todayCheckedIn: true } : {},
9272
- ...typeof todayCredit === "number" && Number.isFinite(todayCredit) ? { todayCredit } : {}
9273
- });
9274
- }
9275
- return rows;
9276
- }
9277
9326
  function autoCheckinFrom(envelope) {
9278
9327
  const value = envelope.value;
9279
9328
  if (value === null || typeof value !== "object") return void 0;
@@ -9886,429 +9935,6 @@ window.__ModuleLoader__.load({
9886
9935
  });
9887
9936
  }
9888
9937
  //#endregion
9889
- //#region src/client/BillionPage.tsx
9890
- /**
9891
- * The billion-context page: what the compressor on this machine is doing.
9892
- *
9893
- * This page observes; it does not compress. billion-context is a separate
9894
- * plugin that very likely already runs here — on a stock desktop profile it is
9895
- * mounted by the profile's bundle layer and listens on its own loopback port.
9896
- * Everything below is read from that proxy's own JSON, relayed through
9897
- * `src/billion.ts`, and nothing on this page can change what the proxy does.
9898
- *
9899
- * Three states, three different things to say, and the difference matters more
9900
- * than it looks:
9901
- *
9902
- * - **missing** — no proxy recorded. Normal on a machine that never installed
9903
- * it. The page shows the install command instead of an empty table, because
9904
- * an empty table here would be indistinguishable from "compression ran and
9905
- * saved nothing".
9906
- * - **unreachable** — a proxy IS recorded but did not answer. Most often a
9907
- * restart in progress: the origin file is rewritten on every respawn. This
9908
- * must not read as an empty dashboard.
9909
- * - **ready** — the numbers.
9910
- *
9911
- * Every figure is the proxy's own. Nothing is re-derived, and in particular the
9912
- * cache hit rate below is weighted by the sessions actually shown rather than
9913
- * averaged across them: a page that averaged a 200-request session with a
9914
- * 1-request one would report a number that describes neither.
9915
- *
9916
- * WHAT THIS PAGE DOES NOT REDRAW
9917
- *
9918
- * billion-context ships its own dashboard, including a per-session detail view.
9919
- * The rows below therefore link out to it rather than re-implementing those
9920
- * screens, and the table itself starts collapsed: a fleet number is what this
9921
- * page is for, and 50 rows of session ids is a wall the user did not ask for.
9922
- * Both are deliberate — one answer, and the summary stays the summary.
9923
- *
9924
- * @module dsh-xdpool/client/BillionPage
9925
- */
9926
- /** Poll cadence, matching the other pages that watch a live process. */
9927
- const POLL_MS$1 = 15e3;
9928
- function fmtInt(value) {
9929
- return value.toLocaleString("en-US");
9930
- }
9931
- /** A token count in millions or billions, because 486,201,338 stops being a number. */
9932
- function compact$1(value) {
9933
- if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
9934
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
9935
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
9936
- return String(value);
9937
- }
9938
- /** `288.6K tokens`, or an em dash when the proxy has no measurement. */
9939
- function tokens(value) {
9940
- return value === 0 ? "—" : `${compact$1(value)} tokens`;
9941
- }
9942
- /** Local clock, or an em dash when the proxy never saw a request. */
9943
- function fmtLastSeen(iso) {
9944
- if (iso === "") return "—";
9945
- const at = Date.parse(iso);
9946
- if (Number.isNaN(at)) return "—";
9947
- const d = new Date(at);
9948
- return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
9949
- }
9950
- /**
9951
- * Fleet cache hit rate, or null when nothing was measured.
9952
- *
9953
- * Weighted by input tokens rather than averaged per session, and null — never
9954
- * zero — when no input was counted at all. The proxy reports `cacheHitPct: -1`
9955
- * for exactly that case, and rendering it as `0%` would claim the cache never
9956
- * helped, which is the opposite of what "not measured" means.
9957
- */
9958
- function fleetHitRate(sessions) {
9959
- let input = 0;
9960
- let cached = 0;
9961
- for (const row of sessions) {
9962
- if (row.cacheHitPct < 0) continue;
9963
- input += row.inputTokens + row.cachedTokens;
9964
- cached += row.cachedTokens;
9965
- }
9966
- return input === 0 ? null : cached / input;
9967
- }
9968
- /**
9969
- * A link into the proxy's own dashboard, or undefined when there is nothing to
9970
- * link to.
9971
- *
9972
- * Two things are checked here rather than trusted. The origin has to be an
9973
- * absolute http(s) URL, because a relative one would resolve against the HOST
9974
- * and silently open the wrong page; and the session id is percent-encoded,
9975
- * because these are proxy-chosen identifiers that may contain characters a
9976
- * hash route cannot carry raw.
9977
- *
9978
- * The hash is the proxy's own route shape (`#/sessions/<id>`), read from its
9979
- * shipped router — a deep link into someone else's UI, which is the whole
9980
- * reason the table links instead of redrawing.
9981
- */
9982
- function proxyLink(origin, hash) {
9983
- if (typeof origin !== "string" || origin === "") return void 0;
9984
- let base;
9985
- try {
9986
- base = new URL(origin);
9987
- } catch {
9988
- return;
9989
- }
9990
- if (base.protocol !== "http:" && base.protocol !== "https:") return void 0;
9991
- const url = new URL("/__bili/", base);
9992
- url.hash = hash;
9993
- return url.toString();
9994
- }
9995
- /**
9996
- * Open the proxy's dashboard in a new tab.
9997
- *
9998
- * `_blank` with `noopener` for the same reason the channels page does it: the
9999
- * proxy owns that tab afterwards, and this page must not be able to reach back
10000
- * into it. The settings panel itself is never navigated away from.
10001
- */
10002
- function openProxyDashboard(url) {
10003
- window.open(url, "_blank", "noopener");
10004
- }
10005
- function BillionPage({ t }) {
10006
- const [doc, setDoc] = (0, react.useState)(void 0);
10007
- const [failed, setFailed] = (0, react.useState)(false);
10008
- const mounted = (0, react.useRef)(true);
10009
- (0, react.useEffect)(() => () => {
10010
- mounted.current = false;
10011
- }, []);
10012
- const load = (0, react.useCallback)(async (signal) => {
10013
- try {
10014
- const response = await fetch(POOL_BILLION_PATH, {
10015
- headers: { accept: "application/json" },
10016
- credentials: "same-origin",
10017
- ...signal === void 0 ? {} : { signal }
10018
- });
10019
- if (!response.ok) {
10020
- if (mounted.current) setFailed(true);
10021
- return;
10022
- }
10023
- const value = await response.json().catch(() => void 0);
10024
- if (mounted.current && signal?.aborted !== true) {
10025
- setDoc(value);
10026
- setFailed(false);
10027
- }
10028
- } catch {
10029
- if (mounted.current && signal?.aborted !== true) setFailed(true);
10030
- }
10031
- }, []);
10032
- (0, react.useEffect)(() => {
10033
- const controller = new AbortController();
10034
- load(controller.signal);
10035
- const timer = window.setInterval(() => {
10036
- load(controller.signal);
10037
- }, POLL_MS$1);
10038
- return () => {
10039
- window.clearInterval(timer);
10040
- controller.abort();
10041
- };
10042
- }, [load]);
10043
- /**
10044
- * Fleet totals over the rows on screen.
10045
- *
10046
- * Summing the rows this page will render is what keeps the KPI row from
10047
- * disagreeing with the table under it — the same rule the pool dashboard
10048
- * follows, and for the same reason: a dashboard that gets trusted must not be
10049
- * able to contradict itself.
10050
- */
10051
- const totals = (0, react.useMemo)(() => {
10052
- const sessions = doc?.sessions ?? [];
10053
- let requests = 0;
10054
- let input = 0;
10055
- let cached = 0;
10056
- let output = 0;
10057
- let saved = 0;
10058
- let live = 0;
10059
- for (const row of sessions) {
10060
- requests += row.requests;
10061
- input += row.inputTokens;
10062
- cached += row.cachedTokens;
10063
- output += row.outputTokens;
10064
- saved += row.tokensSaved;
10065
- if (row.live) live += 1;
10066
- }
10067
- return {
10068
- sessions,
10069
- requests,
10070
- input,
10071
- cached,
10072
- output,
10073
- saved,
10074
- live
10075
- };
10076
- }, [doc]);
10077
- const hitRate = (0, react.useMemo)(() => fleetHitRate(totals.sessions), [totals.sessions]);
10078
- const ready = doc?.state === "ready";
10079
- /**
10080
- * The rows start hidden, and the user opens them on purpose.
10081
- *
10082
- * The summary above is what this page is for; the fleet table is the
10083
- * detail behind it. Collapsed is also the honest default on a machine with 50
10084
- * sessions, where an expanded table pushes every other control off screen.
10085
- */
10086
- const [rowsOpen, setRowsOpen] = (0, react.useState)(false);
10087
- const dashboard = proxyLink(doc?.origin, "#/overview");
10088
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
10089
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: BILLION_CARD_CSS }),
10090
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PageHero, {
10091
- title: t?.("billion.title") ?? "billion-context",
10092
- sub: t?.("billion.sub") ?? "Read-only view of the compressor running on this machine. This plugin never configures it.",
10093
- actions: dashboard === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10094
- type: "button",
10095
- className: "dsm-btn dsm-btn-ghost",
10096
- onClick: () => {
10097
- openProxyDashboard(dashboard);
10098
- },
10099
- children: t?.("billion.openDashboard") ?? "Open its dashboard"
10100
- }),
10101
- pills: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10102
- className: "dsm-xdpool-pill",
10103
- "data-tone": ready ? "on" : "off",
10104
- children: ready ? t?.("billion.pillRunning", { version: doc?.version ?? "?" }) ?? `running · v${doc?.version ?? "?"}` : t?.("billion.pillNotRunning") ?? "not running"
10105
- }), ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10106
- className: "dsm-xdpool-pill",
10107
- "data-tone": "off",
10108
- children: t?.("billion.pillReadOnly") ?? "read-only"
10109
- }) : null] })
10110
- }),
10111
- failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10112
- className: "dsm-xdpool-note",
10113
- role: "alert",
10114
- children: t?.("billion.failed") ?? "Could not reach the pool relay. The pool itself is unaffected."
10115
- }) : null,
10116
- doc === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10117
- className: "dsm-xdpool-note",
10118
- children: t?.("billion.loading") ?? "Looking for a billion-context proxy on this machine…"
10119
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [doc.state !== "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
10120
- className: "dsm-xdpool-sec",
10121
- children: [
10122
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
10123
- className: "dsm-xdpool-sectitle",
10124
- children: doc.state === "missing" ? t?.("billion.notInstalled") ?? "billion-context is not installed here" : t?.("billion.notAnswering") ?? "billion-context did not answer"
10125
- }),
10126
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10127
- className: "dsm-xdpool-note",
10128
- children: doc.reason ?? ""
10129
- }),
10130
- doc.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
10131
- className: "dsm-xdpool-steps",
10132
- children: [
10133
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", { children: [
10134
- "dsh plugin --profile ",
10135
- dshProfileHint(t),
10136
- " add billion-context"
10137
- ] }) }),
10138
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("billion.stepRestart") ?? "Restart DSH so the profile bundle layer mounts it." }),
10139
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("billion.stepBack") ?? "Come back to this page; it fills in on its own." })
10140
- ]
10141
- }) : null
10142
- ]
10143
- }) : null, ready ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10144
- className: "dsm-xdpool-kpis",
10145
- children: [
10146
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10147
- label: t?.("billion.sessions") ?? "Sessions",
10148
- value: fmtInt(doc.totalSessions),
10149
- sub: totals.live === 0 ? t?.("billion.noLive") ?? "none active right now" : t?.("billion.liveSub", { count: totals.live }) ?? `${totals.live} active right now`
10150
- }),
10151
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10152
- label: t?.("billion.requests") ?? "Requests",
10153
- value: fmtInt(totals.requests),
10154
- sub: t?.("billion.requestsSub") ?? "through the proxy, all lanes"
10155
- }),
10156
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10157
- label: t?.("billion.saved") ?? "Tokens saved",
10158
- value: compact$1(totals.saved),
10159
- sub: t?.("billion.savedSub") ?? "by compression, net of what it cost"
10160
- }),
10161
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10162
- label: t?.("billion.tokens") ?? "Tokens",
10163
- value: compact$1(totals.input),
10164
- sub: t?.("billion.tokensSub", {
10165
- cached: compact$1(totals.cached),
10166
- out: compact$1(totals.output)
10167
- }) ?? `${compact$1(totals.cached)} cached · ${compact$1(totals.output)} out`
10168
- }),
10169
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KpiCard, {
10170
- label: t?.("billion.cache") ?? "Cache hit",
10171
- value: hitRate === null ? "—" : `${Math.round(hitRate * 100)}%`,
10172
- sub: hitRate === null ? t?.("billion.cacheNone") ?? "nothing measured yet" : t?.("billion.cacheSub") ?? "of everything sent"
10173
- })
10174
- ]
10175
- }), totals.sessions.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10176
- className: "dsm-xdpool-note",
10177
- children: t?.("billion.empty") ?? "The proxy is up but has seen no requests yet. Chat and this fills in."
10178
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
10179
- className: "dsm-xdpool-sec",
10180
- children: [
10181
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10182
- className: "dsm-xdpool-sechead",
10183
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
10184
- className: "dsm-xdpool-sectitle",
10185
- children: t?.("billion.perSession") ?? "Per session"
10186
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
10187
- className: "dsm-xdpool-secbtns",
10188
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
10189
- type: "button",
10190
- className: "dsm-xdpool-secghost",
10191
- "aria-expanded": rowsOpen,
10192
- onClick: () => {
10193
- setRowsOpen((open) => !open);
10194
- },
10195
- children: rowsOpen ? t?.("billion.hideRows") ?? "Hide rows" : t?.("billion.showRows", { count: totals.sessions.length }) ?? `Show ${totals.sessions.length} rows`
10196
- }), dashboard === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
10197
- className: "dsm-xdpool-secghost",
10198
- href: dashboard,
10199
- target: "_blank",
10200
- rel: "noopener noreferrer",
10201
- children: t?.("billion.openDashboard") ?? "Open the proxy's own dashboard"
10202
- })]
10203
- })]
10204
- }),
10205
- rowsOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
10206
- className: "dsm-xdpool-tablewrap",
10207
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
10208
- className: "dsm-xdpool-table",
10209
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
10210
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colSession") ?? "Session" }),
10211
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colLast") ?? "Last seen" }),
10212
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t?.("billion.colModel") ?? "Model" }),
10213
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10214
- className: "dsm-xdpool-num",
10215
- children: t?.("billion.colRequests") ?? "Requests"
10216
- }),
10217
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10218
- className: "dsm-xdpool-num",
10219
- children: t?.("billion.colContext") ?? "Context"
10220
- }),
10221
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10222
- className: "dsm-xdpool-num",
10223
- children: t?.("billion.colBlocks") ?? "Blocks"
10224
- }),
10225
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10226
- className: "dsm-xdpool-num",
10227
- children: t?.("billion.colSaved") ?? "Saved"
10228
- }),
10229
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
10230
- className: "dsm-xdpool-num",
10231
- children: t?.("billion.colCache") ?? "Cache"
10232
- })
10233
- ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: totals.sessions.map((row) => {
10234
- const detail = proxyLink(doc?.origin, `#/sessions/${encodeURIComponent(row.id)}`);
10235
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
10236
- "data-live": row.live,
10237
- children: [
10238
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
10239
- className: "dsm-xdpool-ellipsis",
10240
- title: row.id,
10241
- children: [detail === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: row.id }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
10242
- className: "dsm-xdpool-sesslink",
10243
- href: detail,
10244
- target: "_blank",
10245
- rel: "noopener noreferrer",
10246
- title: row.id,
10247
- children: row.id
10248
- }), row.live ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10249
- className: "dsm-xdpool-live",
10250
- "aria-label": "live"
10251
- }) : null]
10252
- }),
10253
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: fmtLastSeen(row.lastSeen) }),
10254
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10255
- className: "dsm-xdpool-ellipsis",
10256
- title: row.lastModel,
10257
- children: row.lastModel === "" ? "—" : row.lastModel
10258
- }),
10259
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10260
- className: "dsm-xdpool-num",
10261
- children: fmtInt(row.requests)
10262
- }),
10263
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10264
- className: "dsm-xdpool-num",
10265
- title: t?.("billion.contextTitle", {
10266
- source: row.contextTokensSource,
10267
- window: row.contextWindow
10268
- }) ?? `source ${row.contextTokensSource} · window ${row.contextWindow}`,
10269
- children: tokens(row.contextTokens)
10270
- }),
10271
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10272
- className: "dsm-xdpool-num",
10273
- children: fmtInt(row.blocks)
10274
- }),
10275
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10276
- className: "dsm-xdpool-num",
10277
- children: compact$1(row.tokensSaved)
10278
- }),
10279
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
10280
- className: "dsm-xdpool-num",
10281
- children: row.cacheHitPct < 0 ? "—" : `${row.cacheHitPct}%`
10282
- })
10283
- ]
10284
- }, row.id);
10285
- }) })]
10286
- })
10287
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10288
- className: "dsm-xdpool-note",
10289
- 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."
10290
- }),
10291
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10292
- className: "dsm-xdpool-note",
10293
- children: t?.("billion.note") ?? "These numbers are the proxy’s own. Nothing here is an estimate, and this page never changes a setting."
10294
- })
10295
- ]
10296
- })] }) : null] })
10297
- ] });
10298
- }
10299
- /**
10300
- * The profile name for the install command.
10301
- *
10302
- * The host publishes it as an env var; when that is absent the command still
10303
- * works with `--profile` omitted on a single-profile install, so the hint says
10304
- * so rather than printing an empty flag.
10305
- */
10306
- function dshProfileHint(t) {
10307
- const profile = typeof process === "undefined" ? void 0 : process.env.DSH_PROFILE;
10308
- if (typeof profile === "string" && profile !== "") return profile;
10309
- return t?.("billion.defaultProfile") ?? "<profile>";
10310
- }
10311
- //#endregion
10312
9938
  //#region src/client/RoutingPage.tsx
10313
9939
  /**
10314
9940
  * The routing page: chains that are themselves models.
@@ -10395,8 +10021,10 @@ window.__ModuleLoader__.load({
10395
10021
  const next = value;
10396
10022
  setStatus(next);
10397
10023
  setError(void 0);
10398
- setDrafts((prev) => seeded.current ? prev : (next.combos ?? []).map(draftOf));
10399
- seeded.current = true;
10024
+ if (!seeded.current) {
10025
+ seeded.current = true;
10026
+ setDrafts((next.combos ?? []).map(draftOf));
10027
+ }
10400
10028
  }
10401
10029
  } catch (cause) {
10402
10030
  if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
@@ -11568,6 +11196,18 @@ window.__ModuleLoader__.load({
11568
11196
  *
11569
11197
  * @module dsh-xdpool/client/ChannelPoolSection
11570
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
+ }
11571
11211
  /** How many automatic retries a cold-start mount gets before giving up. */
11572
11212
  const LOAD_RETRIES = 5;
11573
11213
  /** Gap between those retries, in milliseconds. */
@@ -11660,8 +11300,18 @@ window.__ModuleLoader__.load({
11660
11300
  return Number.isInteger(value) ? String(value) : value.toFixed(2);
11661
11301
  }
11662
11302
  function ChannelPoolSection({ t }) {
11663
- const [entries, setEntries] = (0, react.useState)([]);
11664
- 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);
11665
11315
  const [notice, setNotice] = (0, react.useState)(void 0);
11666
11316
  /** provider → the open list, or `undefined` for a card with both folded. */
11667
11317
  const [folds, setFolds] = (0, react.useState)(/* @__PURE__ */ new Map());
@@ -11674,6 +11324,15 @@ window.__ModuleLoader__.load({
11674
11324
  const entriesRef = (0, react.useRef)(entries);
11675
11325
  entriesRef.current = entries;
11676
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);
11677
11336
  (0, react.useEffect)(() => () => {
11678
11337
  mountedRef.current = false;
11679
11338
  }, []);
@@ -11686,6 +11345,7 @@ window.__ModuleLoader__.load({
11686
11345
  });
11687
11346
  };
11688
11347
  const load = async () => {
11348
+ loadSeq.current += 1;
11689
11349
  try {
11690
11350
  const channels = (await fetchChannels()).filter((channel) => !REGION_OWNED_CHANNELS.has(channel.id));
11691
11351
  const statusAnswer = await callPack("provider.status", { providers: channels.map((channel) => channel.id) });
@@ -11695,25 +11355,17 @@ window.__ModuleLoader__.load({
11695
11355
  const wantBalances = channels.find((channel) => channel.id === provider)?.credits !== void 0;
11696
11356
  const accountsPromise = callPack("account.list", { provider });
11697
11357
  const modelsPromise = callPack("model.list", { provider });
11698
- const balancesPromise = wantBalances ? callPack("credits.balances", { provider }) : Promise.resolve(void 0);
11699
- const statusesPromise = wantBalances && channels.find((c) => c.id === provider)?.credits?.status === true ? callPack("credits.status", { provider }) : Promise.resolve(void 0);
11700
- const [accounts, models, balances, creditStatuses] = await Promise.all([
11701
- accountsPromise.catch(() => refused("account.list failed")),
11702
- modelsPromise.catch(() => refused("model.list failed")),
11703
- balancesPromise.catch(() => refused("credits.balances failed")),
11704
- statusesPromise.catch(() => refused("credits.status failed"))
11705
- ]);
11358
+ const [accounts, models] = await Promise.all([accountsPromise.catch(() => refused("account.list failed")), modelsPromise.catch(() => refused("model.list failed"))]);
11706
11359
  const modelRows = modelsFrom(models.value);
11707
11360
  const modelEmptyBecauseRefused = modelRows.length === 0 && models.ok !== true;
11708
- const balanceRows = balances === void 0 ? [] : balancesFrom(balances);
11709
11361
  return [provider, {
11710
11362
  accounts: dedupeAccounts(accountsFrom(accounts)),
11711
11363
  models: sortChannelModels(modelRows),
11712
11364
  facts: /* @__PURE__ */ new Map(),
11713
- balances: balanceRows,
11714
- statuses: creditStatuses === void 0 ? [] : creditStatusesFrom(creditStatuses),
11715
- balancePending: false,
11716
- 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,
11717
11369
  modelError: modelEmptyBecauseRefused ? models.error?.message ?? "model.list refused" : void 0
11718
11370
  }];
11719
11371
  }));
@@ -11724,25 +11376,48 @@ window.__ModuleLoader__.load({
11724
11376
  facts: /* @__PURE__ */ new Map(),
11725
11377
  status: statuses.get(channel.id)
11726
11378
  }));
11379
+ poolSnapshot = {
11380
+ entries: next,
11381
+ error: void 0
11382
+ };
11727
11383
  if (!mountedRef.current) return;
11384
+ const seq = loadSeq.current;
11728
11385
  setEntries(next);
11729
11386
  setError(void 0);
11387
+ setLoading(false);
11730
11388
  if (next.length === 0) {
11731
- 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
+ }
11732
11397
  return;
11733
11398
  }
11399
+ for (const entry of next) {
11400
+ if (entry.channel.credits === void 0) continue;
11401
+ refreshBalances(entry.channel.id, false, true, seq);
11402
+ }
11734
11403
  const idsByProvider = {};
11735
11404
  for (const [provider, entry] of perProvider) if (entry.models.length > 0) idsByProvider[provider] = entry.models.map((model) => model.id);
11736
11405
  const facts = await fetchChannelModels(next.map((entry) => entry.channel.id), idsByProvider).catch(() => /* @__PURE__ */ new Map());
11737
- if (!mountedRef.current) return;
11406
+ if (!mountedRef.current || loadSeq.current !== seq) return;
11738
11407
  if (facts.size === 0) return;
11739
11408
  setEntries((previous) => previous.map((entry) => ({
11740
11409
  ...entry,
11741
11410
  facts: new Map((facts.get(entry.channel.id) ?? []).map((model) => [model.id, model]))
11742
11411
  })));
11743
11412
  } catch (cause) {
11413
+ const message = cause instanceof Error ? cause.message : String(cause);
11414
+ poolSnapshot = {
11415
+ entries: entriesRef.current,
11416
+ error: message
11417
+ };
11744
11418
  if (!mountedRef.current) return;
11745
- setError(cause instanceof Error ? cause.message : String(cause));
11419
+ setError(message);
11420
+ setLoading(false);
11746
11421
  }
11747
11422
  };
11748
11423
  /**
@@ -11896,8 +11571,20 @@ window.__ModuleLoader__.load({
11896
11571
  await load();
11897
11572
  }
11898
11573
  };
11899
- const refreshBalances = async (provider, force) => {
11900
- 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);
11901
11588
  setEntries((previous) => previous.map((entry) => entry.channel.id === provider ? {
11902
11589
  ...entry,
11903
11590
  balancePending: true
@@ -11907,7 +11594,21 @@ window.__ModuleLoader__.load({
11907
11594
  provider,
11908
11595
  force: true
11909
11596
  } : { provider });
11910
- 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
+ }
11911
11612
  if (!envelope.ok) {
11912
11613
  setEntries((previous) => previous.map((entry) => entry.channel.id === provider ? {
11913
11614
  ...entry,
@@ -11924,7 +11625,7 @@ window.__ModuleLoader__.load({
11924
11625
  balances: rows
11925
11626
  } : entry));
11926
11627
  } finally {
11927
- if (mountedRef.current) markBusy(provider, false);
11628
+ if (mountedRef.current && !quiet) markBusy(provider, false);
11928
11629
  }
11929
11630
  };
11930
11631
  const claim = async (provider) => {
@@ -11968,12 +11669,24 @@ window.__ModuleLoader__.load({
11968
11669
  return;
11969
11670
  }
11970
11671
  if (action === "test") setNotice(t?.("channelpool.testOk") ?? "Reachable.");
11672
+ invalidatePoolSnapshot();
11971
11673
  await load();
11972
11674
  } finally {
11973
11675
  if (mountedRef.current) markBusy(provider, false);
11974
11676
  }
11975
11677
  };
11976
- 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
+ }
11977
11690
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
11978
11691
  className: "dsm-xdpool-cpool",
11979
11692
  "aria-label": t?.("channelpool.title") ?? "Channels",
@@ -12368,7 +12081,7 @@ window.__ModuleLoader__.load({
12368
12081
  *
12369
12082
  * @module dsh-xdpool/version
12370
12083
  */
12371
- const POOL_VERSION = "2.4.6";
12084
+ const POOL_VERSION = "2.5.1";
12372
12085
  //#endregion
12373
12086
  //#region src/client/PoolCard.tsx
12374
12087
  /**
@@ -12734,10 +12447,6 @@ window.__ModuleLoader__.load({
12734
12447
  id: "ledger",
12735
12448
  label: t?.("shell.tabLedger") ?? "Dashboard"
12736
12449
  },
12737
- {
12738
- id: "billion",
12739
- label: t?.("shell.tabBillion") ?? "billion-context"
12740
- },
12741
12450
  {
12742
12451
  id: "routing",
12743
12452
  label: t?.("shell.tabRouting") ?? "Routing"
@@ -13582,7 +13291,7 @@ window.__ModuleLoader__.load({
13582
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, {
13583
13292
  t,
13584
13293
  usage
13585
- }) : 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: [
13586
13295
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: POOL_CARD_CSS }),
13587
13296
  usage === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
13588
13297
  className: "dsm-xdpool-card dsm-xdpool-usage",
@@ -14583,6 +14292,20 @@ window.__ModuleLoader__.load({
14583
14292
  global: "xdpool-global"
14584
14293
  };
14585
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)();
14586
14309
  const now = /* @__PURE__ */ new Date();
14587
14310
  const tag = tagFor(model, now, region);
14588
14311
  const promo = promoStatusFor(model, now, region);
@@ -14651,7 +14374,7 @@ window.__ModuleLoader__.load({
14651
14374
  "aria-label": t?.("row.modelContextBudget") ?? "Context",
14652
14375
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14653
14376
  type: "radio",
14654
- name: `budget-${model.id}`,
14377
+ name: `budget-${radioScope}`,
14655
14378
  checked: currentBudget === DEFAULT_CONTEXT_BUDGET,
14656
14379
  disabled: !editable,
14657
14380
  onChange: () => {
@@ -14659,7 +14382,7 @@ window.__ModuleLoader__.load({
14659
14382
  }
14660
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", {
14661
14384
  type: "radio",
14662
- name: `budget-${model.id}`,
14385
+ name: `budget-${radioScope}`,
14663
14386
  checked: currentBudget === native,
14664
14387
  disabled: !editable,
14665
14388
  onChange: () => {
@@ -15174,6 +14897,12 @@ window.__ModuleLoader__.load({
15174
14897
  "shell.brand": "XD Pool",
15175
14898
  "shell.star": "Star",
15176
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.",
15177
14906
  "shell.tabPool": "Account pool",
15178
14907
  "shell.tabChannels": "Login channels",
15179
14908
  "shell.tabAggregate": "Providers",
@@ -15182,8 +14911,6 @@ window.__ModuleLoader__.load({
15182
14911
  "shell.subAggregate": "Bring your own API keys: any OpenAI-compatible provider, reachable as extra models.",
15183
14912
  "shell.tabLedger": "Dashboard",
15184
14913
  "shell.subLedger": "What this machine sent and got back, since the pool last started.",
15185
- "shell.tabBillion": "billion-context",
15186
- "shell.subBillion": "A read-only window onto the compressor running beside this pool.",
15187
14914
  "shell.tabRouting": "Routing",
15188
14915
  "shell.subRouting": "Chains that try models in order, so one busy or rate-limited model falls through to the next.",
15189
14916
  "shell.tabGateway": "Gateway",
@@ -15266,48 +14993,9 @@ window.__ModuleLoader__.load({
15266
14993
  "ledger.unattributed": "(unattributed)",
15267
14994
  "ledger.cn": "CN",
15268
14995
  "ledger.global": "Global",
15269
- "billion.title": "billion-context",
15270
- "billion.sub": "What the compressor on this machine is doing. This page reads it; it cannot change it.",
15271
- "billion.pillRunning": "running · v{version}",
15272
- "billion.pillNotRunning": "not running",
15273
- "billion.pillReadOnly": "read-only",
15274
- "billion.failed": "Could not reach the pool relay. The pool itself is unaffected.",
15275
- "billion.loading": "Looking for a billion-context proxy on this machine…",
15276
- "billion.notInstalled": "billion-context is not installed on this machine",
15277
- "billion.notAnswering": "A billion-context proxy is registered but did not answer",
15278
- "billion.stepRestart": "Restart DSH so the profile bundle layer mounts it.",
15279
- "billion.stepBack": "Come back to this page; it fills in on its own.",
15280
- "billion.defaultProfile": "<profile>",
15281
- "billion.sessions": "Sessions",
15282
- "billion.noLive": "none active right now",
15283
- "billion.liveSub": "{count} still active right now",
15284
- "billion.requests": "Requests",
15285
- "billion.requestsSub": "through the proxy, every lane",
15286
- "billion.saved": "Tokens saved",
15287
- "billion.savedSub": "by compression, net of what it cost",
15288
- "billion.tokens": "Tokens",
15289
- "billion.tokensSub": "{cached} cached · {out} out",
15290
- "billion.cache": "Cache hit",
15291
- "billion.cacheNone": "nothing measured yet",
15292
- "billion.cacheSub": "of everything sent",
15293
- "billion.empty": "The proxy is up but has seen no requests yet. Chat and this fills in.",
15294
- "billion.perSession": "Per session",
15295
- "billion.showRows": "Show {count} rows",
15296
- "billion.hideRows": "Hide rows",
15297
- "billion.openDashboard": "Open its dashboard",
15298
- "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.",
15299
- "billion.note": "These are the proxy’s own numbers, read over loopback. Nothing here is an estimate.",
15300
- "billion.colSession": "Session",
15301
- "billion.colLast": "Last seen",
15302
- "billion.colModel": "Model",
15303
- "billion.colRequests": "Requests",
15304
- "billion.colContext": "Context",
15305
- "billion.colBlocks": "Blocks",
15306
- "billion.colSaved": "Saved",
15307
- "billion.colCache": "Cache",
15308
- "billion.contextTitle": "measured {source} · model window {window}",
15309
14996
  "channelpool.title": "Scan channels",
15310
14997
  "channelpool.accountCount": "{count} account(s)",
14998
+ "channelpool.loading": "Reading scan channels…",
15311
14999
  "channelpool.disabled": "disabled",
15312
15000
  "channelpool.noModels": "No models published yet.",
15313
15001
  "channelpool.modelError": "Could not list models: {message}",
@@ -15685,6 +15373,12 @@ window.__ModuleLoader__.load({
15685
15373
  "shell.brand": "XD Pool",
15686
15374
  "shell.star": "点Star",
15687
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 说一声,会更新。",
15688
15382
  "shell.tabPool": "账号池",
15689
15383
  "shell.tabChannels": "扫码登录",
15690
15384
  "shell.tabAggregate": "聚合渠道",
@@ -15693,8 +15387,6 @@ window.__ModuleLoader__.load({
15693
15387
  "shell.subAggregate": "带上你自己的 API key:任何兼容 OpenAI 的服务都能变成额外模型。",
15694
15388
  "shell.tabLedger": "数据看板",
15695
15389
  "shell.subLedger": "这台机器发出去什么、收回来什么,从池子上次启动算起。",
15696
- "shell.tabBillion": "billion-context",
15697
- "shell.subBillion": "一个只读的窗口,看着旁边跑着的那台压缩器。",
15698
15390
  "shell.tabRouting": "路由",
15699
15391
  "shell.subRouting": "把几个模型串成一条链,按顺序试;一个忙了或被限流,就自动落到下一个。",
15700
15392
  "shell.tabGateway": "网关",
@@ -15777,48 +15469,9 @@ window.__ModuleLoader__.load({
15777
15469
  "ledger.unattributed": "(没归属到账号)",
15778
15470
  "ledger.cn": "国内版",
15779
15471
  "ledger.global": "国际版",
15780
- "billion.title": "billion-context",
15781
- "billion.sub": "这台机器上的压缩器正在做什么。这一页只看不改,它动不了那个代理。",
15782
- "billion.pillRunning": "运行中 · v{version}",
15783
- "billion.pillNotRunning": "没在运行",
15784
- "billion.pillReadOnly": "只读",
15785
- "billion.failed": "连不上池子的中继接口,池子本身不受影响。",
15786
- "billion.loading": "正在这台机器上找 billion-context 代理…",
15787
- "billion.notInstalled": "这台机器上没装 billion-context",
15788
- "billion.notAnswering": "登记过一个 billion-context 代理,但它没应答",
15789
- "billion.stepRestart": "重启 DSH,让 profile 的 bundle 层把它挂上。",
15790
- "billion.stepBack": "回到这一页,数字会自己填上。",
15791
- "billion.defaultProfile": "<profile>",
15792
- "billion.sessions": "会话数",
15793
- "billion.noLive": "当前没有活跃会话",
15794
- "billion.liveSub": "当前还有 {count} 个在跑",
15795
- "billion.requests": "请求数",
15796
- "billion.requestsSub": "经过代理的,全部线路",
15797
- "billion.saved": "省下的 Token",
15798
- "billion.savedSub": "压缩净省的,已经扣掉它自己花掉的",
15799
- "billion.tokens": "Token 总量",
15800
- "billion.tokensSub": "{cached} 来自缓存 · {out} 输出",
15801
- "billion.cache": "缓存命中",
15802
- "billion.cacheNone": "还没有测量数据",
15803
- "billion.cacheSub": "在送出去的所有 token 里",
15804
- "billion.empty": "代理起来了,但还没收到过请求,聊两句这里就填上了。",
15805
- "billion.perSession": "按会话看",
15806
- "billion.showRows": "展开 {count} 行",
15807
- "billion.hideRows": "收起",
15808
- "billion.openDashboard": "打开它自己的仪表盘",
15809
- "billion.rowsHint": "上面的全局数字就是全部情况。要看这一个代理的明细就展开表格,要看单个会话就去它自己的仪表盘。",
15810
- "billion.note": "这些是代理自己报的数字,走的是本机回环。这里没有一个是估算值。",
15811
- "billion.colSession": "会话",
15812
- "billion.colLast": "最后出现",
15813
- "billion.colModel": "模型",
15814
- "billion.colRequests": "请求",
15815
- "billion.colContext": "上下文",
15816
- "billion.colBlocks": "压缩块",
15817
- "billion.colSaved": "省下",
15818
- "billion.colCache": "缓存",
15819
- "billion.contextTitle": "实测 {source} · 模型窗口 {window}",
15820
15472
  "channelpool.title": "扫码渠道",
15821
15473
  "channelpool.accountCount": "{count} 个账号",
15474
+ "channelpool.loading": "正在读取扫码渠道…",
15822
15475
  "channelpool.disabled": "已停用",
15823
15476
  "channelpool.noModels": "该渠道还没有发布模型。",
15824
15477
  "channelpool.modelError": "读取模型列表失败:{message}",