dsh-agy 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +21 -10
  2. package/lib/{accounts-BN4h0AU4.mjs → accounts-D605JE2i.mjs} +186 -19
  3. package/lib/accounts-D605JE2i.mjs.map +1 -0
  4. package/lib/cli/index.d.mts.map +1 -1
  5. package/lib/cli/index.mjs +9 -7
  6. package/lib/cli/index.mjs.map +1 -1
  7. package/lib/client.js +402 -51
  8. package/lib/{constants-BpyRirvk.mjs → constants-CmA9wXNe.mjs} +3 -3
  9. package/lib/{constants-BpyRirvk.mjs.map → constants-CmA9wXNe.mjs.map} +1 -1
  10. package/lib/{exchange-LzEyxve1.mjs → exchange-C-tndv7j.mjs} +3 -3
  11. package/lib/{exchange-LzEyxve1.mjs.map → exchange-C-tndv7j.mjs.map} +1 -1
  12. package/lib/{import-CotRHNDk.mjs → import-SxPDjH3u.mjs} +4 -4
  13. package/lib/{import-CotRHNDk.mjs.map → import-SxPDjH3u.mjs.map} +1 -1
  14. package/lib/index.mjs +2 -2
  15. package/lib/{models-Qn0-sh6X.mjs → models-B6bD7zPi.mjs} +3 -3
  16. package/lib/{models-Qn0-sh6X.mjs.map → models-B6bD7zPi.mjs.map} +1 -1
  17. package/lib/{plugin-common-N7p9Cm-3.mjs → plugin-common-kSBwIaFi.mjs} +303 -17
  18. package/lib/plugin-common-kSBwIaFi.mjs.map +1 -0
  19. package/lib/{proxy-DfaL73mL.mjs → proxy-BbuQX5U9.mjs} +28 -8
  20. package/lib/proxy-BbuQX5U9.mjs.map +1 -0
  21. package/lib/{quota-summary-adID82I2.mjs → quota-summary-DEA3P3Zh.mjs} +3 -3
  22. package/lib/{quota-summary-adID82I2.mjs.map → quota-summary-DEA3P3Zh.mjs.map} +1 -1
  23. package/lib/web/plugin.mjs +40 -18
  24. package/lib/web/plugin.mjs.map +1 -1
  25. package/package.json +1 -4
  26. package/lib/accounts-BN4h0AU4.mjs.map +0 -1
  27. package/lib/plugin-common-N7p9Cm-3.mjs.map +0 -1
  28. package/lib/proxy-DfaL73mL.mjs.map +0 -1
package/lib/client.js CHANGED
@@ -103,17 +103,28 @@ window.__ModuleLoader__.load({
103
103
  rounded rect inset 2px from the card edge and transparent at rest. A
104
104
  full-bleed rectangle reads as a slab and fights the card's own radius. */
105
105
  .agy-rows { display: flex; flex-direction: column; gap: 2px; }
106
+ /* The live line: one status strip above the rows, present ONLY while upstream
107
+ requests are in flight — an idle pool renders no strip, so quiet stays quiet.
108
+ The pulsing dot is the host StateDot primitive ('ongoing'), so the animation
109
+ is the platform's; this rule is layout and tone only. Sits OUTSIDE .agy-rows,
110
+ so the master list's scroll cap does not scroll the status away. */
111
+ .agy-live {
112
+ display: flex; align-items: center; gap: 7px;
113
+ margin: 2px 2px 6px; padding: 7px 8px; border-radius: 10px;
114
+ font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary, #61666b);
115
+ background: var(--dsw-alias-bg-layer-2, #f4f5f7);
116
+ }
106
117
  /* Master/detail: the account list beside the selected account's detail, so a
107
- * row and the panel it opens stay in view together.
108
- *
109
- * The collapse MUST be a CONTAINER query, not a viewport one. This section
110
- * renders inside the Settings panel, which is ~600px wide even on a large
111
- * display, so the former @media (max-width: 720px) never fired: the split
112
- * stayed two-column everywhere and the 300px master column squeezed the detail
113
- * to ~280px. That is the reported symptom (the panel "feels too narrow"): the
114
- * account email truncated to "a1…" and the latency value wrapped onto three
115
- * lines. The measured constraint is the PANEL's width, so the query must follow
116
- * it.
118
+ * row and the panel it opens stay in view together — but only when the
119
+ * container can host both columns comfortably. The Settings panel gives the
120
+ * wrap ~564px of inline space on desktop (800px modal - 188px nav - 48px
121
+ * padding), so at the panel the split STACKS into one full-width column:
122
+ * 564px cannot host two comfortable columns, and a forced 300px master
123
+ * truncated every email to "a1…" while squeezing the detail to ~280px (the
124
+ * original "panel feels too narrow" report). The breakpoint therefore sits
125
+ * ABOVE the panel width and must stay there; if the host's modal geometry
126
+ * changes, re-measure before moving it. The query is a CONTAINER one because
127
+ * the former viewport @media (max-width: 720px) never fired inside the panel.
117
128
  *
118
129
  * The containment lives on a dedicated wrapper, NOT on .agy-root:
119
130
  * container-type: inline-size applies layout containment, which makes the
@@ -314,6 +325,12 @@ window.__ModuleLoader__.load({
314
325
  color: var(--dsw-alias-label-primary, #1f2329); }
315
326
  .agy-limit-reset { text-align: right; font: var(--dsw-font-xxxs-11);
316
327
  color: var(--dsw-alias-label-tertiary, #8f959e); }
328
+ /* The burn projection: indented to align with the bar (58px label + 10px gap),
329
+ warn-tinted because "this window runs dry before it resets" is the one
330
+ projection that asks the reader to act. */
331
+ .agy-limit-burn { padding: 0 0 4px 68px;
332
+ font: var(--dsw-font-xxxs-11);
333
+ color: var(--dsw-alias-state-warn-primary, #f59e0b); }
317
334
 
318
335
  /* ── Dense breakdown tables (Usage tab only) ─────────────────────────────── */
319
336
  .agy-table-wrap { padding: 6px 0 2px; }
@@ -360,6 +377,19 @@ window.__ModuleLoader__.load({
360
377
  /* Danger has no primitive variant; keep the ghost skin and tint the label. */
361
378
  .agy-btn-danger { color: var(--dsw-alias-state-error-primary, #ec1313) !important; }
362
379
 
380
+ /* ── Recent activity ring ──────────────────────────────────────────────────
381
+ The "what just happened" list. Only the result cell carries color — ok
382
+ inherits the table's neutral, and a wall of tinted rows would read as an
383
+ alarm rather than a log. */
384
+ .agy-recent-state { font: var(--dsw-font-xxs-12); }
385
+ .agy-recent-state[data-kind="fail"] { color: var(--dsw-alias-state-error-primary, #ec1313); }
386
+ .agy-recent-state[data-kind="limited"] { color: var(--dsw-alias-state-warn-primary, #f59e0b); }
387
+ .agy-recent-state[data-kind="rotation"] { color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
388
+ /* The recent list is a standalone disclosure on the tab root, not one block
389
+ inside a card body — the separator border-top the disclosure idiom uses
390
+ between sibling blocks would draw a stray line across nothing here. */
391
+ .agy-recent.agy-disclosure { border-top: 0; }
392
+
363
393
  .agy-toolbar { display: flex; align-items: center; gap: 8px; }
364
394
  .agy-textarea { width: 100%; min-height: 88px; resize: vertical; outline: none;
365
395
  padding: 9px 10px; font: var(--dsw-font-xxs-12); font-family: var(--ds-font-family-code);
@@ -367,19 +397,24 @@ window.__ModuleLoader__.load({
367
397
  border: 0.5px solid var(--dsw-alias-border-l2, #eef0f3); border-radius: 8px; }
368
398
  .agy-textarea:focus { border-color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
369
399
  `;
370
- /** Install the stylesheet once (idempotent across plugin reloads). */
400
+ /** Install the stylesheet once (idempotent across plugin reloads): a second
401
+ * install never appends a duplicate, and one whose content has gone stale
402
+ * (a previous bundle's CSS) is refreshed in place. The disposer is
403
+ * deliberately a NO-OP — it runs on every Cordis effect re-evaluation, and
404
+ * removing the element there stripped every .agy-* style mid-session; the
405
+ * element is unique by id and inert once the section is gone. */
371
406
  function installAgyStyles() {
372
407
  if (typeof document === "undefined") return () => {};
373
- if (document.getElementById(STYLE_ID) !== null) return () => {
374
- document.getElementById(STYLE_ID)?.remove();
375
- };
408
+ const existing = document.getElementById(STYLE_ID);
409
+ if (existing !== null) {
410
+ if (existing.textContent !== CSS) existing.textContent = CSS;
411
+ return () => {};
412
+ }
376
413
  const style = document.createElement("style");
377
414
  style.id = STYLE_ID;
378
415
  style.textContent = CSS;
379
416
  document.head.appendChild(style);
380
- return () => {
381
- document.getElementById(STYLE_ID)?.remove();
382
- };
417
+ return () => {};
383
418
  }
384
419
  //#endregion
385
420
  //#region src/client/locales.ts
@@ -401,16 +436,34 @@ window.__ModuleLoader__.load({
401
436
  tabModels: "模型",
402
437
  tabUsage: "用量",
403
438
  tabCredentials: "凭据",
404
- stateActive: "使用中",
439
+ stateActive: "可用",
405
440
  stateCooling: "冷却中",
406
441
  stateVerificationRequired: "待验证",
407
442
  stateDisabled: "已停用",
408
443
  coolingUntil: "冷却至",
444
+ verificationRetry: "待验证 · {time} 重试",
409
445
  currentAccount: "当前账号",
446
+ fieldDisabled: "停用",
447
+ disabledCredentials: "凭据失效",
448
+ disabledSince: "停用于 {ago}",
449
+ disabledHint: "凭据已被上游拒绝。运行该账号所在行的「验证」尝试恢复;若仍失败,请重新登录导入。",
410
450
  noProject: "—",
411
451
  valueUnknown: "—",
412
452
  colAccount: "账号",
413
453
  colRequests: "请求",
454
+ colTime: "时间",
455
+ colResult: "结果",
456
+ colDuration: "耗时",
457
+ recentTitle: "最近请求",
458
+ recentEmpty: "暂无最近记录",
459
+ recentOk: "成功",
460
+ rowRequestsTotal: "累计请求 {n}",
461
+ lastActive: "{ago}活跃",
462
+ liveOne: "正在通过 {email} 生成 · {count} 个并发",
463
+ liveMany: "正在生成 · {count} 个并发 · {accounts} 个账号",
464
+ fieldThroughput: "吞吐",
465
+ throughputValue: "≈ {n} token/s",
466
+ throughputNote: "首 token 后 · 累计平均",
414
467
  colActions: "操作",
415
468
  emptyAccounts: "还没有账号。切到「凭据」标签导入,或点击上方「登录」。",
416
469
  detailTitle: "已选账号",
@@ -420,6 +473,7 @@ window.__ModuleLoader__.load({
420
473
  limitsRefreshOk: "已刷新 {measured} 个账号的限额",
421
474
  limitsRefreshFresh: "限额仍是新鲜的,无需刷新",
422
475
  limitsRefreshFailed: "{failed} 个账号的限额刷新失败",
476
+ limitBurnWarn: "按此速度{value}后耗尽",
423
477
  quotaWindow5h: "5 小时",
424
478
  quotaWindowWeekly: "每周",
425
479
  fieldProject: "项目",
@@ -447,13 +501,14 @@ window.__ModuleLoader__.load({
447
501
  quotaResetIn: "重置 {value}",
448
502
  relNow: "即将",
449
503
  relJustNow: "刚刚",
504
+ relSeconds: "{n} 秒",
450
505
  relAgo: "{value}前",
451
506
  relMinutes: "{n} 分钟",
452
507
  relHours: "{n} 小时",
453
508
  relDays: "{n} 天",
454
509
  relMonths: "{n} 个月",
455
510
  relYears: "{n} 年",
456
- actionActivate: "激活",
511
+ actionActivate: "设为当前",
457
512
  actionVerify: "验证",
458
513
  actionDelete: "删除",
459
514
  actionTest: "测试调用",
@@ -494,6 +549,8 @@ window.__ModuleLoader__.load({
494
549
  fieldRateLimitRotation: "限流 / 轮换",
495
550
  byModel: "按模型",
496
551
  byAccount: "按账号",
552
+ trendTitle: "近 7 天",
553
+ colDay: "日期",
497
554
  colModel: "模型",
498
555
  colTokenShare: "Token 占比",
499
556
  colOutput: "输出",
@@ -561,16 +618,34 @@ window.__ModuleLoader__.load({
561
618
  tabModels: "Models",
562
619
  tabUsage: "Usage",
563
620
  tabCredentials: "Credentials",
564
- stateActive: "Active",
621
+ stateActive: "Ready",
565
622
  stateCooling: "Cooling down",
566
623
  stateVerificationRequired: "Needs verification",
567
624
  stateDisabled: "Disabled",
568
625
  coolingUntil: "Cooling until",
569
- currentAccount: "current",
626
+ verificationRetry: "Needs verification · retries {time}",
627
+ currentAccount: "Current",
628
+ fieldDisabled: "Disabled",
629
+ disabledCredentials: "Credentials rejected",
630
+ disabledSince: "disabled {ago}",
631
+ disabledHint: "The credentials were rejected by upstream. Run Verify on this account's row to try restoring it; if that fails, sign in again to re-import.",
570
632
  noProject: "—",
571
633
  valueUnknown: "—",
572
634
  colAccount: "Account",
573
635
  colRequests: "Requests",
636
+ colTime: "Time",
637
+ colResult: "Result",
638
+ colDuration: "Duration",
639
+ recentTitle: "Recent requests",
640
+ recentEmpty: "No recent activity",
641
+ recentOk: "ok",
642
+ rowRequestsTotal: "{n} requests in total",
643
+ lastActive: "active {ago}",
644
+ liveOne: "Generating via {email} · {count} in flight",
645
+ liveMany: "Generating · {count} in flight across {accounts} account(s)",
646
+ fieldThroughput: "Throughput",
647
+ throughputValue: "≈ {n} token/s",
648
+ throughputNote: "after first token · cumulative",
574
649
  colActions: "Actions",
575
650
  emptyAccounts: "No accounts yet. Import one from the Credentials tab, or use Sign in above.",
576
651
  detailTitle: "Selected account",
@@ -580,6 +655,7 @@ window.__ModuleLoader__.load({
580
655
  limitsRefreshOk: "Refreshed limits for {measured} account(s)",
581
656
  limitsRefreshFresh: "Limits are already fresh — nothing to refresh",
582
657
  limitsRefreshFailed: "Limit refresh failed for {failed} account(s)",
658
+ limitBurnWarn: "at this rate, empty in {value}",
583
659
  quotaWindow5h: "5 hours",
584
660
  quotaWindowWeekly: "Weekly",
585
661
  fieldProject: "Project",
@@ -607,13 +683,14 @@ window.__ModuleLoader__.load({
607
683
  quotaResetIn: "resets {value}",
608
684
  relNow: "shortly",
609
685
  relJustNow: "just now",
686
+ relSeconds: "{n}s",
610
687
  relAgo: "{value} ago",
611
688
  relMinutes: "{n} min",
612
689
  relHours: "{n} h",
613
690
  relDays: "{n} d",
614
691
  relMonths: "{n} mo",
615
692
  relYears: "{n} y",
616
- actionActivate: "Activate",
693
+ actionActivate: "Set as current",
617
694
  actionVerify: "Verify",
618
695
  actionDelete: "Delete",
619
696
  actionTest: "Test call",
@@ -654,6 +731,8 @@ window.__ModuleLoader__.load({
654
731
  fieldRateLimitRotation: "Rate limits / rotations",
655
732
  byModel: "By model",
656
733
  byAccount: "By account",
734
+ trendTitle: "Last 7 days",
735
+ colDay: "Day",
657
736
  colModel: "Model",
658
737
  colTokenShare: "Token share",
659
738
  colOutput: "Output",
@@ -782,6 +861,14 @@ window.__ModuleLoader__.load({
782
861
  * it — see `actionError` in `AgySettings`.
783
862
  */
784
863
  const ACTION_MESSAGE_TTL_MS = 3500;
864
+ /**
865
+ * How often the section re-reads the pool's in-flight snapshot.
866
+ *
867
+ * The call is a pure in-memory read on the host (no token refresh, no quota
868
+ * probe), so a tight-ish interval is free; 3s makes the live line feel live
869
+ * without a visible request cost.
870
+ */
871
+ const POOL_POLL_INTERVAL_MS = 3e3;
785
872
  function h(tag, props, ...children) {
786
873
  return (0, react.createElement)(tag, props ?? null, ...children);
787
874
  }
@@ -854,6 +941,28 @@ window.__ModuleLoader__.load({
854
941
  return counters.input + counters.output + counters.cacheRead + counters.cacheWrite;
855
942
  }
856
943
  /**
944
+ * Cumulative average OUTPUT rate for one scope, in tokens per second.
945
+ *
946
+ * Pure and exported for a direct unit test. The denominator is the STREAMING
947
+ * window (average latency minus average time-to-first-token), NOT the wall
948
+ * clock: on this channel the pre-first-token wait (upstream queue, routing,
949
+ * prompt processing, silent thinking) is ~90% of the request — measured 7.5s
950
+ * of an 8.3s average — so dividing by wall time understated the decode rate
951
+ * about 8× (40 tok/s shown where the account really streamed ~300).
952
+ *
953
+ * Per-request AVERAGES, not raw sums: a failed request carries wall time but
954
+ * never a first token (`latencyN` > `ttftN`), so subtracting raw sums would
955
+ * hand its wait to the decode window. Both averages must exist — a scope with
956
+ * no timed request or no reported first token gets null, hiding the row rather
957
+ * than showing a fake rate.
958
+ */
959
+ function throughputTokenPerSecond(totals) {
960
+ if (totals.latencyN === 0 || totals.ttftN === 0 || totals.output <= 0) return null;
961
+ const decodeMs = totals.latencyMs / totals.latencyN - totals.ttftMs / totals.ttftN;
962
+ if (decodeMs <= 0) return null;
963
+ return Math.round(totals.output / totals.latencyN / (decodeMs / 1e3));
964
+ }
965
+ /**
857
966
  * The whole prompt side: everything the model read, cached or not.
858
967
  *
859
968
  * The LEDGER keeps `input` as the uncached portion alone, because that is DSH's
@@ -918,28 +1027,58 @@ window.__ModuleLoader__.load({
918
1027
  }
919
1028
  }
920
1029
  /**
1030
+ * Humanize a burn horizon (hours until a window runs dry at the sampled rate).
1031
+ *
1032
+ * Reuses the shared `rel*` magnitudes so the phrasing matches every other
1033
+ * duration on the panel; under an hour falls to minutes rather than rounding
1034
+ * to a false zero.
1035
+ */
1036
+ function burnHorizon(hours, t) {
1037
+ if (hours < 1) return t("relMinutes", { n: Math.max(1, Math.round(hours * 60)) });
1038
+ if (hours < 48) return t("relHours", { n: Math.round(hours) });
1039
+ return t("relDays", { n: Math.round(hours / 24) });
1040
+ }
1041
+ /**
921
1042
  * A wall-clock moment for a state label (a cooldown end).
922
1043
  *
923
1044
  * Time-of-day alone is enough while the wall is today; past midnight it must
924
1045
  * carry the date, or a 24h quota cooldown reads as though it ends in a few
925
1046
  * minutes. (`untilText` is the relative form, used where "how long from now" is
926
1047
  * the question rather than "when".)
1048
+ *
1049
+ * `lang` is the UI language ('zh' | 'en'), not the browser locale: the words on
1050
+ * this panel follow the host's language setting, so the dates must too — a zh
1051
+ * panel rendering `9/24/2026` (the browser's en-US ordering) was the symptom.
1052
+ * `undefined` degrades to the browser default, the pre-`lang` behaviour.
927
1053
  */
928
- function clockTime(iso) {
1054
+ function clockTime(iso, lang) {
929
1055
  if (iso === null) return "—";
930
1056
  const date = new Date(iso);
931
1057
  if (Number.isNaN(date.getTime())) return "—";
932
1058
  const now = /* @__PURE__ */ new Date();
933
- return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString([], {
1059
+ return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString(lang, {
934
1060
  hour: "2-digit",
935
1061
  minute: "2-digit"
936
- }) : date.toLocaleString([], {
1062
+ }) : date.toLocaleString(lang, {
937
1063
  month: "short",
938
1064
  day: "numeric",
939
1065
  hour: "2-digit",
940
1066
  minute: "2-digit"
941
1067
  });
942
1068
  }
1069
+ /**
1070
+ * A ledger day key ('YYYY-MM-DD', local by construction) as a short label.
1071
+ * Parsed as LOCAL date parts — a UTC parse would shift the label a day for
1072
+ * half the planet.
1073
+ */
1074
+ function dayLabel(day, lang) {
1075
+ const [y, m, d] = day.split("-").map(Number);
1076
+ if (y === void 0 || m === void 0 || d === void 0 || Number.isNaN(y)) return day;
1077
+ return new Date(y, m - 1, d).toLocaleDateString(lang, {
1078
+ month: "short",
1079
+ day: "numeric"
1080
+ });
1081
+ }
943
1082
  const MINUTE_MS = 6e4;
944
1083
  const HOUR_MS = 60 * MINUTE_MS;
945
1084
  const DAY_MS = 24 * HOUR_MS;
@@ -993,6 +1132,34 @@ window.__ModuleLoader__.load({
993
1132
  return t("relAgo", { value: diff < HOUR_MS ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < DAY_MS ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 30 * DAY_MS ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 365 * DAY_MS ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
994
1133
  }
995
1134
  /**
1135
+ * Ago label with seconds resolution, for the recent list. `agoText` collapses
1136
+ * the whole first minute into "just now", which is too coarse when the reader
1137
+ * is watching live activity — here the seconds carry the information.
1138
+ */
1139
+ function recentAgo(at, now, t) {
1140
+ const diff = now - at;
1141
+ if (diff < 1e4) return t("relJustNow");
1142
+ if (diff < MINUTE_MS) return t("relSeconds", { n: Math.floor(diff / 1e3) });
1143
+ return agoText(new Date(at).toISOString(), t, now);
1144
+ }
1145
+ /**
1146
+ * Middle-truncate an identity for the recent list.
1147
+ *
1148
+ * Both ends carry the signal — an email's domain, a model id's tier suffix —
1149
+ * so the cut is taken from the MIDDLE, and bounding the rendered length (not
1150
+ * relying on CSS clipping of a `table-layout: fixed` cell) is what keeps the
1151
+ * columns honest. The full value stays on the cell's title.
1152
+ * @param text - the full identity.
1153
+ * @param max - rendered character budget including the ellipsis.
1154
+ * @returns the truncated display string.
1155
+ */
1156
+ function truncateIdentity(text, max = 22) {
1157
+ if (text.length <= max) return text;
1158
+ const head = Math.ceil((max - 1) / 2);
1159
+ const tail = max - 1 - head;
1160
+ return `${text.slice(0, head)}…${text.slice(-tail)}`;
1161
+ }
1162
+ /**
996
1163
  * A host-styled button.
997
1164
  *
998
1165
  * Delegates to the host's `Button` so focus rings, disabled states, size tiers
@@ -1061,36 +1228,44 @@ window.__ModuleLoader__.load({
1061
1228
  const [proxyDraft, setProxyDraft] = (0, react.useState)("");
1062
1229
  const usage = account.usage;
1063
1230
  const now = Date.now();
1231
+ const throughput = usage === null ? null : throughputTokenPerSecond(usage.totals);
1064
1232
  const identityRows = [
1065
1233
  [t("fieldProject"), account.projectId ?? t("noProject")],
1066
1234
  [t("fieldProxy"), h("span", { className: "agy-mono" }, account.proxy ?? t("proxyDirect"))],
1067
1235
  [t("fieldFingerprint"), account.fingerprint === null ? t("fingerprintNone") : t("fingerprintRegenerated", {
1068
1236
  count: account.fingerprintHistory,
1069
- date: new Date(account.fingerprint.createdAt).toLocaleDateString()
1237
+ date: new Date(account.fingerprint.createdAt).toLocaleDateString(props.lang)
1070
1238
  })],
1071
- [t("fieldCooldownReason"), account.cooldownReason === null ? t("noProject") : `${cooldownReasonLabel(account.cooldownReason, t)} · ${agoText(account.cooldownSetAt, t, now)}`],
1239
+ ...account.cooldownReason === null ? [] : [[t("fieldCooldownReason"), `${cooldownReasonLabel(account.cooldownReason, t)} · ${agoText(account.cooldownSetAt, t, now)}`]],
1072
1240
  [t("fieldSources"), usage === null ? t("noProject") : t("sourcesSummary", {
1073
1241
  chat: usage.sources.chat,
1074
1242
  cli: usage.sources.cli,
1075
1243
  verify: usage.sources.verify,
1076
1244
  test: usage.sources.test
1077
1245
  })],
1078
- [t("fieldLatency"), usage === null ? t("noProject") : `${t("latencyAverage", { value: formatDuration(average(usage.totals.latencyMs, usage.totals.latencyN)) })} · ${t("latencyTtft", { value: formatDuration(average(usage.totals.ttftMs, usage.totals.ttftN)) })}`]
1246
+ [t("fieldLatency"), usage === null ? t("noProject") : `${t("latencyAverage", { value: formatDuration(average(usage.totals.latencyMs, usage.totals.latencyN)) })} · ${t("latencyTtft", { value: formatDuration(average(usage.totals.ttftMs, usage.totals.ttftN)) })}`],
1247
+ ...throughput === null ? [] : [[t("fieldThroughput"), `${t("throughputValue", { n: throughput })} · ${t("throughputNote")}`]]
1079
1248
  ];
1249
+ if (account.state === "disabled") identityRows.push([t("fieldDisabled"), account.disabledAt === null ? t("disabledCredentials") : `${t("disabledCredentials")} · ${t("disabledSince", { ago: agoText(account.disabledAt, t, now) })}`]);
1080
1250
  if (account.verificationRequired) identityRows.push([t("fieldVerification"), account.verificationUrl === null ? t("verificationNoUrl") : h("a", {
1081
1251
  className: "agy-link",
1082
1252
  href: account.verificationUrl,
1083
1253
  target: "_blank",
1084
1254
  rel: "noreferrer noopener"
1085
1255
  }, t("verificationOpen"))]);
1086
- const identity = card(t("detailTitle"), defs(identityRows), account.email ?? `#${account.index}`);
1256
+ const identity = card(t("detailTitle"), h("div", null, defs(identityRows), account.state === "disabled" ? hint(t("disabledHint")) : null), account.email ?? `#${account.index}`);
1087
1257
  const actions = card(t("colActions"), h("div", { className: "agy-actions" }, button(t("actionTest"), () => {
1088
1258
  handlers.onTest(account.index);
1089
1259
  }, { disabled: busy }), button(t("actionExport"), () => {
1090
1260
  handlers.onExport(account.index);
1091
1261
  }, { disabled: busy }), button(t("actionRegenerateFingerprint"), () => {
1092
1262
  handlers.onRegenerateFingerprint(account.index);
1093
- }, { disabled: busy })));
1263
+ }, { disabled: busy }), button(t("actionDelete"), () => {
1264
+ handlers.onDelete(account.index);
1265
+ }, {
1266
+ variant: "danger",
1267
+ disabled: busy
1268
+ })));
1094
1269
  /**
1095
1270
  * The 5-hour / weekly windows, placed ABOVE the cumulative usage card.
1096
1271
  *
@@ -1111,13 +1286,14 @@ window.__ModuleLoader__.load({
1111
1286
  key: group.name
1112
1287
  }, h("div", { className: "agy-limit-group-name" }, group.name), ...group.windows.map((window) => {
1113
1288
  const fraction = window.remainingFraction;
1114
- return h("div", {
1115
- className: "agy-limit-row",
1116
- key: window.bucketId
1117
- }, h("span", { className: "agy-limit-k" }, windowLabel(window.window, t)), h("span", { className: "agy-limit-track" }, fraction === null ? null : h("i", { style: {
1289
+ const burn = account.limitBurn?.[window.bucketId];
1290
+ const hoursLeft = fraction !== null && burn !== void 0 && burn > 0 ? fraction / burn : null;
1291
+ const resetHours = window.resetTime === null ? null : (new Date(window.resetTime).getTime() - now) / HOUR_MS;
1292
+ const exhaustsFirst = hoursLeft !== null && resetHours !== null && hoursLeft < resetHours;
1293
+ return h("div", { key: window.bucketId }, h("div", { className: "agy-limit-row" }, h("span", { className: "agy-limit-k" }, windowLabel(window.window, t)), h("span", { className: "agy-limit-track" }, fraction === null ? null : h("i", { style: {
1118
1294
  width: `${Math.round(fraction * 100)}%`,
1119
1295
  background: quotaColor(fraction)
1120
- } })), h("span", { className: "agy-limit-p" }, fraction === null ? t("valueUnknown") : `${Math.round(fraction * 100)}%`), h("span", { className: "agy-limit-reset" }, window.resetTime === null ? null : untilText(window.resetTime, t, now)));
1296
+ } })), h("span", { className: "agy-limit-p" }, fraction === null ? t("valueUnknown") : `${Math.round(fraction * 100)}%`), h("span", { className: "agy-limit-reset" }, window.resetTime === null ? null : untilText(window.resetTime, t, now))), exhaustsFirst ? h("div", { className: "agy-limit-burn" }, t("limitBurnWarn", { value: burnHorizon(hoursLeft, t) })) : null);
1121
1297
  })))));
1122
1298
  const usageBlock = usage === null ? null : card(t("usageCumulative"), metrics([
1123
1299
  metric(t("kpiInput"), promptTokens(usage.totals), t("kpiInputMissed", { tokens: tokenText(usage.totals.input) })),
@@ -1154,6 +1330,19 @@ window.__ModuleLoader__.load({
1154
1330
  })())));
1155
1331
  }
1156
1332
  /**
1333
+ * Whether the row should offer "set as current".
1334
+ *
1335
+ * A pure rule, exported for a direct unit test. Two cases hide the action:
1336
+ * the account is already the pool's preference, or it is DISABLED — `activate`
1337
+ * only writes the preference and cannot re-enable, so on a disabled row the
1338
+ * button was a no-op the user would read as broken. The repair path for a
1339
+ * button was a no-op the user would read as broken. The repair path for a
1340
+ * disabled account is `actionVerify`, which stays on the row.
1341
+ */
1342
+ function canActivateAccount(account) {
1343
+ return !account.active && account.state !== "disabled";
1344
+ }
1345
+ /**
1157
1346
  * Pick the selected account index: defaults to the active account (badge "current")
1158
1347
  * when present, otherwise falls back to index 0. Clamps to valid bounds.
1159
1348
  */
@@ -1171,6 +1360,29 @@ window.__ModuleLoader__.load({
1171
1360
  const { accounts, busy, handlers, t } = props;
1172
1361
  const [selected, setSelected] = (0, react.useState)(null);
1173
1362
  const selectedRef = (0, react.useRef)(null);
1363
+ const now = Date.now();
1364
+ /**
1365
+ * The live line: who is serving right now, and how much.
1366
+ *
1367
+ * Present ONLY while something is in flight — an idle pool renders no strip,
1368
+ * so the quiet state stays quiet. The dot is the host `StateDot`'s ongoing
1369
+ * state, so the animation is the platform's.
1370
+ */
1371
+ const liveLine = props.busyNow.length === 0 ? null : (() => {
1372
+ const total = props.busyNow.reduce((sum, entry) => sum + entry.count, 0);
1373
+ const [first] = props.busyNow;
1374
+ const subject = first === void 0 ? "" : first.email ?? `#${first.index}`;
1375
+ return h("div", { className: "agy-live" }, h(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
1376
+ state: "ongoing",
1377
+ size: 8
1378
+ }), h("span", null, props.busyNow.length === 1 ? t("liveOne", {
1379
+ email: subject,
1380
+ count: total
1381
+ }) : t("liveMany", {
1382
+ count: total,
1383
+ accounts: props.busyNow.length
1384
+ })));
1385
+ })();
1174
1386
  const index = resolveSelectedAccountIndex(accounts, selected);
1175
1387
  const current = accounts[index];
1176
1388
  (0, react.useEffect)(() => {
@@ -1194,32 +1406,35 @@ window.__ModuleLoader__.load({
1194
1406
  event.preventDefault();
1195
1407
  setSelected(at);
1196
1408
  }
1197
- }, h("div", { className: "agy-rowmain" }, h("div", { className: "agy-rowtitle" }, h("span", { className: "agy-rowname" }, account.email ?? `#${account.index}`), account.active ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, { tone: "info" }, t("currentAccount")) : null), h("div", { className: "agy-rowmeta" }, account.projectId ?? t("noProject"), account.usage === null || account.usage.totals.requests === 0 ? null : ` · ${t("colRequests")} ${account.usage.totals.requests}`)), h("div", { className: "agy-rowactions" }, stateBadge(account.state, account.state === "cooling" ? `${t("coolingUntil")} ${clockTime(account.cooldownUntil)}` : stateLabel(account.state, t)), account.active ? null : button(t("actionActivate"), () => {
1409
+ }, h("div", { className: "agy-rowmain" }, h("div", { className: "agy-rowtitle" }, h("span", { className: "agy-rowname" }, account.email ?? `#${account.index}`), account.active ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, { tone: "info" }, t("currentAccount")) : null), h("div", { className: "agy-rowmeta" }, account.projectId ?? t("noProject"), account.usage === null || account.usage.totals.requests === 0 ? null : ` · ${t("rowRequestsTotal", { n: account.usage.totals.requests })}`, account.usage === null || account.usage.lastUsedAt === null ? null : ` · ${t("lastActive", { ago: agoText(new Date(account.usage.lastUsedAt).toISOString(), t, now) })}`)), h("div", { className: "agy-rowactions" }, stateBadge(account.state, account.state === "cooling" ? `${t("coolingUntil")} ${clockTime(account.cooldownUntil, props.lang)}` : account.state === "verification-required" ? t("verificationRetry", { time: clockTime(account.cooldownUntil, props.lang) }) : stateLabel(account.state, t)), canActivateAccount(account) ? button(t("actionActivate"), () => {
1198
1410
  setSelected(at);
1199
1411
  handlers.onActivate(account.index);
1200
1412
  }, {
1201
1413
  size: "sm",
1202
1414
  disabled: busy
1203
- }), button(t("actionVerify"), () => {
1415
+ }) : null, button(t("actionVerify"), () => {
1204
1416
  handlers.onVerify(account.index);
1205
1417
  }, {
1206
1418
  size: "sm",
1207
1419
  disabled: busy
1208
- }), button(t("actionDelete"), () => {
1209
- setSelected(null);
1210
- handlers.onDelete(account.index);
1211
- }, {
1212
- size: "sm",
1213
- variant: "danger",
1214
- disabled: busy
1215
1420
  }))));
1216
- return h("div", { className: "agy-root" }, h("div", { className: "agy-split-wrap" }, h("div", { className: "agy-split" }, card(t("colAccount"), h("div", { className: "agy-rows" }, ...rows), `${accounts.length}`), current === void 0 ? null : h(AccountDetail, {
1421
+ return h("div", { className: "agy-root" }, h("div", { className: "agy-split-wrap" }, h("div", { className: "agy-split" }, card(t("colAccount"), h("div", null, liveLine, h("div", { className: "agy-rows" }, ...rows)), `${accounts.length}`), current === void 0 ? null : h(AccountDetail, {
1217
1422
  key: String(current.index),
1218
1423
  account: current,
1219
1424
  busy,
1220
- handlers,
1425
+ handlers: {
1426
+ ...handlers,
1427
+ onDelete: (index) => {
1428
+ setSelected(null);
1429
+ handlers.onDelete(index);
1430
+ }
1431
+ },
1432
+ lang: props.lang,
1221
1433
  t
1222
- }))));
1434
+ })), h(RecentCard, {
1435
+ rpc: props.rpc,
1436
+ t
1437
+ })));
1223
1438
  }
1224
1439
  /**
1225
1440
  * Models tab: per-model visibility switches, plus the account's model quota in a
@@ -1577,6 +1792,110 @@ window.__ModuleLoader__.load({
1577
1792
  }))) : null);
1578
1793
  return card(t("thinkingTitle"), block);
1579
1794
  }
1795
+ /** Result vocabulary for one recent row; rotation events are their own kind. */
1796
+ function recentResultKind(entry) {
1797
+ if (entry.kind === "rotation") return "rotation";
1798
+ if (entry.rateLimited) return "limited";
1799
+ if (!entry.ok) return "fail";
1800
+ return "ok";
1801
+ }
1802
+ /**
1803
+ * Localized label for a failure-classification token, falling back to the raw
1804
+ * token so a classification added upstream still reads as something.
1805
+ */
1806
+ function failureReasonLabel(reason, t) {
1807
+ switch (reason) {
1808
+ case "rate-limit": return t("colRateLimited");
1809
+ case "network-error": return t("cooldownReasonNetworkError");
1810
+ case "auth-failure": return t("disabledCredentials");
1811
+ case "verification-required": return t("cooldownReasonValidationRequired");
1812
+ case "quota-exhausted": return t("cooldownReasonQuotaExhausted");
1813
+ case "project-error": return t("cooldownReasonProjectError");
1814
+ default: return reason;
1815
+ }
1816
+ }
1817
+ /**
1818
+ * The result cell's text. Rotation rows say WHY (`reason` rides the record
1819
+ * since the ring captures the classification); a failed request with a
1820
+ * non-rate-limit classification also names it — the rate-limit case is already
1821
+ * the whole RateLimited label, and doubling it reads as a stutter.
1822
+ */
1823
+ function recentResultText(entry, t) {
1824
+ const kind = recentResultKind(entry);
1825
+ if (kind === "rotation") return entry.reason === null ? t("colRotations") : `${t("colRotations")} · ${failureReasonLabel(entry.reason, t)}`;
1826
+ if (kind === "limited") return t("colRateLimited");
1827
+ if (kind === "fail") return entry.reason === null || entry.reason === "rate-limit" ? t("colFailed") : `${t("colFailed")} · ${failureReasonLabel(entry.reason, t)}`;
1828
+ return t("recentOk");
1829
+ }
1830
+ /**
1831
+ * The "what just happened" list: the most recent records this process saw,
1832
+ * newest first, collapsed by default and polled while open.
1833
+ *
1834
+ * A component (never called directly) — it owns hooks; see ThinkingSamples for
1835
+ * the hook-order rule this file has been bitten by twice. The poll is the same
1836
+ * 3s cadence as the live line and reads the host's memory ring, so an open
1837
+ * list costs nothing upstream.
1838
+ */
1839
+ function RecentCard(props) {
1840
+ const { rpc, t } = props;
1841
+ const [open, setOpen] = (0, react.useState)(false);
1842
+ const [recent, setRecent] = (0, react.useState)(null);
1843
+ const [error, setError] = (0, react.useState)(void 0);
1844
+ const alive = (0, react.useRef)(true);
1845
+ (0, react.useEffect)(() => {
1846
+ alive.current = true;
1847
+ return () => {
1848
+ alive.current = false;
1849
+ };
1850
+ }, []);
1851
+ const load = (0, react.useCallback)(() => {
1852
+ rpc.call("pool.recent", {}).then((result) => {
1853
+ if (alive.current) {
1854
+ setRecent(result.recent);
1855
+ setError(void 0);
1856
+ }
1857
+ }).catch((caught) => {
1858
+ if (alive.current) setError(caught instanceof Error ? caught.message : String(caught));
1859
+ });
1860
+ }, [rpc]);
1861
+ (0, react.useEffect)(() => {
1862
+ if (open && recent === null) load();
1863
+ }, [
1864
+ open,
1865
+ recent,
1866
+ load
1867
+ ]);
1868
+ (0, react.useEffect)(() => {
1869
+ if (!open) return;
1870
+ const timer = setInterval(() => {
1871
+ if (!document.hidden) load();
1872
+ }, POOL_POLL_INTERVAL_MS);
1873
+ return () => {
1874
+ clearInterval(timer);
1875
+ };
1876
+ }, [open, load]);
1877
+ const now = Date.now();
1878
+ return h("div", {
1879
+ className: "agy-disclosure agy-recent",
1880
+ "data-open": open
1881
+ }, h("button", {
1882
+ type: "button",
1883
+ className: "agy-disclosure-toggle",
1884
+ "aria-expanded": open,
1885
+ onClick: () => {
1886
+ setOpen(!open);
1887
+ }
1888
+ }, h("span", { className: "agy-caret" }), h("span", null, t("recentTitle")), recent === null ? null : h("span", { className: "agy-disclosure-meta" }, String(recent.length))), open === false ? null : h("div", { className: "agy-disclosure-body" }, error === void 0 ? null : h("div", { className: "agy-error" }, error), recent === null ? h("div", { className: "agy-empty" }, t("loading")) : recent.length === 0 ? h("div", { className: "agy-empty" }, t("recentEmpty")) : h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", { style: { width: "64px" } }, t("colTime")), h("th", { style: { width: "30%" } }, t("colAccount")), h("th", { style: { width: "24%" } }, t("colModel")), h("th", { style: { width: "96px" } }, t("colResult")), h("th", { style: { width: "56px" } }, t("colDuration")), h("th", { style: { width: "48px" } }, t("colOutput"))), recent.map((entry, index) => h("tr", { key: `${entry.at}-${index}` }, h("td", null, recentAgo(entry.at, now, t)), h("td", null, h("span", {
1889
+ className: "agy-mail",
1890
+ title: entry.account ?? void 0
1891
+ }, entry.account === null ? "—" : truncateIdentity(entry.account))), h("td", null, h("span", {
1892
+ className: "agy-mail",
1893
+ title: entry.model ?? void 0
1894
+ }, entry.model === null ? "—" : truncateIdentity(entry.model))), h("td", null, h("span", {
1895
+ className: "agy-recent-state",
1896
+ "data-kind": recentResultKind(entry)
1897
+ }, recentResultText(entry, t))), h("td", null, entry.latencyMs === null ? "—" : formatDuration(entry.latencyMs)), h("td", { className: "agy-num" }, entry.output === null ? "—" : tokenText(entry.output))))))));
1898
+ }
1580
1899
  const RANGE_IDS = [
1581
1900
  "today",
1582
1901
  "week",
@@ -1688,7 +2007,7 @@ window.__ModuleLoader__.load({
1688
2007
  onClick: () => {
1689
2008
  setRange(id);
1690
2009
  }
1691
- }, rangeLabel(id, t)))), h("span", { className: "agy-grow" }), stats.since === null ? null : h("span", { className: "agy-aside" }, t("since", { date: new Date(stats.since).toLocaleDateString() }))), card(t("usageTitle"), h("div", null, metrics([
2010
+ }, rangeLabel(id, t)))), h("span", { className: "agy-grow" }), stats.since === null ? null : h("span", { className: "agy-aside" }, t("since", { date: new Date(stats.since).toLocaleDateString(props.lang) }))), card(t("usageTitle"), h("div", null, metrics([
1692
2011
  metric(t("kpiTotalTokens"), total, t("kpiTotalDetail", {
1693
2012
  requests: counters.requests,
1694
2013
  failed: counters.failed
@@ -1701,7 +2020,7 @@ window.__ModuleLoader__.load({
1701
2020
  [t("fieldRateLimitRotation"), `${counters.rateLimited} / ${counters.rotations}`],
1702
2021
  [t("labelLatencyAverage"), formatDuration(average(counters.latencyMs, counters.latencyN))],
1703
2022
  [t("labelTtft"), formatDuration(average(counters.ttftMs, counters.ttftN))]
1704
- ])), view.models.length === 0 ? null : card(t("byModel"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colModel")), numHeader(0, t("colRequests")), numHeader(1, t("kpiInput")), numHeader(2, t("kpiCacheRead")), numHeader(3, t("colOutput")), numHeader(4, t("colTokenShare"))), view.models.map((row) => h("tr", { key: row.model }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.model)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(promptTokens(row.counters))), h("td", { className: "agy-num" }, tokenText(row.counters.cacheRead)), h("td", { className: "agy-num" }, tokenText(row.counters.output)), h("td", { className: "agy-num" }, h("span", { className: "agy-bar" }, h("span", { className: "agy-track" }, h("i", { style: { width: `${Math.round(totalTokens(row.counters) / Math.max(1, total) * 100)}%` } }))))))))), view.accounts.length === 0 ? null : card(t("byAccount"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colAccount")), numHeader(0, t("colRequests")), numHeader(1, t("colToken")), numHeader(2, t("colFailed")), numHeader(3, t("colRateLimited")), numHeader(4, t("colRotations"))), view.accounts.map((row) => h("tr", { key: row.account }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.account)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(totalTokens(row.counters))), h("td", { className: "agy-num" }, String(row.counters.failed)), h("td", { className: "agy-num" }, String(row.counters.rateLimited)), h("td", { className: "agy-num" }, String(row.counters.rotations))))))));
2023
+ ])), card(t("trendTitle"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colDay")), numHeader(0, t("colRequests")), numHeader(1, t("colFailed")), numHeader(2, t("colRateLimited")), numHeader(3, t("colRotations"))), stats.days.map((row) => h("tr", { key: row.day }, h("td", null, dayLabel(row.day, props.lang)), h("td", { className: "agy-num" }, String(row.requests)), h("td", { className: "agy-num" }, String(row.failed)), h("td", { className: "agy-num" }, String(row.rateLimited)), h("td", { className: "agy-num" }, String(row.rotations))))))), view.models.length === 0 ? null : card(t("byModel"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colModel")), numHeader(0, t("colRequests")), numHeader(1, t("kpiInput")), numHeader(2, t("kpiCacheRead")), numHeader(3, t("colOutput")), numHeader(4, t("colTokenShare"))), view.models.map((row) => h("tr", { key: row.model }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.model)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(promptTokens(row.counters))), h("td", { className: "agy-num" }, tokenText(row.counters.cacheRead)), h("td", { className: "agy-num" }, tokenText(row.counters.output)), h("td", { className: "agy-num" }, h("span", { className: "agy-bar" }, h("span", { className: "agy-track" }, h("i", { style: { width: `${Math.round(totalTokens(row.counters) / Math.max(1, total) * 100)}%` } }))))))))), view.accounts.length === 0 ? null : card(t("byAccount"), h("div", { className: "agy-table-wrap" }, table(h("tr", null, h("th", null, t("colAccount")), numHeader(0, t("colRequests")), numHeader(1, t("colToken")), numHeader(2, t("colFailed")), numHeader(3, t("colRateLimited")), numHeader(4, t("colRotations"))), view.accounts.map((row) => h("tr", { key: row.account }, h("td", { className: "agy-strong" }, h("span", { className: "agy-mail" }, row.account)), h("td", { className: "agy-num" }, String(row.counters.requests)), h("td", { className: "agy-num" }, tokenText(totalTokens(row.counters))), h("td", { className: "agy-num" }, String(row.counters.failed)), h("td", { className: "agy-num" }, String(row.counters.rateLimited)), h("td", { className: "agy-num" }, String(row.counters.rotations))))))));
1705
2024
  }
1706
2025
  function CredentialsTab(props) {
1707
2026
  const { t } = props;
@@ -1741,6 +2060,15 @@ window.__ModuleLoader__.load({
1741
2060
  /** Model ids whose own test call is in flight (see the per-row test button). */
1742
2061
  const [modelTesting, setModelTesting] = (0, react.useState)(() => /* @__PURE__ */ new Set());
1743
2062
  const [stats, setStats] = (0, react.useState)(null);
2063
+ /**
2064
+ * Accounts with upstream requests in flight, from the polled `pool.status`.
2065
+ *
2066
+ * A poll, not a push: the section has no push channel, and the call is a pure
2067
+ * in-memory read on the host. A failed poll is display-only and stays silent —
2068
+ * the previous frame (including "idle") holds until the next tick, which is
2069
+ * the honest degradation for a status line.
2070
+ */
2071
+ const [poolBusy, setPoolBusy] = (0, react.useState)([]);
1744
2072
  const [error, setError] = (0, react.useState)(void 0);
1745
2073
  /** A non-fatal outcome worth reporting (e.g. a partial credential import). */
1746
2074
  const [notice, setNoticeState] = (0, react.useState)(void 0);
@@ -1842,7 +2170,8 @@ window.__ModuleLoader__.load({
1842
2170
  return {
1843
2171
  ...account,
1844
2172
  limits: entry.groups,
1845
- limitsUpdatedAt: entry.updatedAt
2173
+ limitsUpdatedAt: entry.updatedAt,
2174
+ limitBurn: entry.burn ?? account.limitBurn
1846
2175
  };
1847
2176
  }));
1848
2177
  if (report) {
@@ -1862,6 +2191,19 @@ window.__ModuleLoader__.load({
1862
2191
  (0, react.useEffect)(() => {
1863
2192
  refresh();
1864
2193
  }, [refresh]);
2194
+ (0, react.useEffect)(() => {
2195
+ const tick = () => {
2196
+ if (document.hidden) return;
2197
+ rpc.call("pool.status", {}).then((result) => {
2198
+ if (alive.current) setPoolBusy(result.busy);
2199
+ }).catch(() => {});
2200
+ };
2201
+ tick();
2202
+ const timer = setInterval(tick, POOL_POLL_INTERVAL_MS);
2203
+ return () => {
2204
+ clearInterval(timer);
2205
+ };
2206
+ }, [rpc]);
1865
2207
  (0, react.useEffect)(() => {
1866
2208
  if (tab === "accounts" && accounts.length > 0) loadLimits();
1867
2209
  }, [
@@ -2075,7 +2417,10 @@ window.__ModuleLoader__.load({
2075
2417
  const body = tab === "accounts" ? h(AccountsTab, {
2076
2418
  accounts,
2077
2419
  busy,
2420
+ busyNow: poolBusy,
2078
2421
  handlers,
2422
+ lang: props.lang,
2423
+ rpc,
2079
2424
  t
2080
2425
  }) : tab === "models" ? modelError === void 0 ? h(ModelsTab, {
2081
2426
  models,
@@ -2152,6 +2497,7 @@ window.__ModuleLoader__.load({
2152
2497
  loadModels();
2153
2498
  }, { size: "sm" })) : tab === "usage" ? h(UsageTab, {
2154
2499
  stats,
2500
+ lang: props.lang,
2155
2501
  t
2156
2502
  }) : h(CredentialsTab, {
2157
2503
  busy,
@@ -2205,6 +2551,7 @@ window.__ModuleLoader__.load({
2205
2551
  }
2206
2552
  const rpc = createRpc(connection);
2207
2553
  const t = ctx.locale.bind(NS);
2554
+ const lang = ctx.locale.getLocale?.().active;
2208
2555
  ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
2209
2556
  name: "settings.section",
2210
2557
  id: "agy",
@@ -2213,16 +2560,20 @@ window.__ModuleLoader__.load({
2213
2560
  label: () => t("title")
2214
2561
  }, () => h(AgySettings, {
2215
2562
  rpc,
2216
- t
2563
+ t,
2564
+ lang
2217
2565
  }))), "dsh-agy: Settings section");
2218
2566
  }
2219
2567
  //#endregion
2220
2568
  exports.AgySettings = AgySettings;
2221
2569
  exports.apply = apply;
2570
+ exports.canActivateAccount = canActivateAccount;
2222
2571
  exports.inject = inject;
2223
2572
  exports.orderModels = orderModels;
2224
2573
  exports.resolveSelectedAccountIndex = resolveSelectedAccountIndex;
2574
+ exports.throughputTokenPerSecond = throughputTokenPerSecond;
2225
2575
  exports.tokenText = tokenText;
2576
+ exports.truncateIdentity = truncateIdentity;
2226
2577
  return module.exports;
2227
2578
  }
2228
2579
  });