dsh-agy 0.4.0 → 0.4.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
@@ -103,6 +103,17 @@ 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
118
  * row and the panel it opens stay in view together.
108
119
  *
@@ -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);
@@ -401,16 +431,35 @@ window.__ModuleLoader__.load({
401
431
  tabModels: "模型",
402
432
  tabUsage: "用量",
403
433
  tabCredentials: "凭据",
404
- stateActive: "使用中",
434
+ stateActive: "可用",
405
435
  stateCooling: "冷却中",
406
436
  stateVerificationRequired: "待验证",
407
437
  stateDisabled: "已停用",
408
438
  coolingUntil: "冷却至",
439
+ verificationRetry: "待验证 · {time} 重试",
409
440
  currentAccount: "当前账号",
441
+ fieldDisabled: "停用",
442
+ disabledCredentials: "凭据失效",
443
+ disabledSince: "停用于 {ago}",
444
+ disabledHint: "凭据已被上游拒绝。运行该账号所在行的「验证」尝试恢复;若仍失败,请重新登录导入。",
410
445
  noProject: "—",
411
446
  valueUnknown: "—",
412
447
  colAccount: "账号",
413
448
  colRequests: "请求",
449
+ colTime: "时间",
450
+ colResult: "结果",
451
+ colDuration: "耗时",
452
+ recentTitle: "最近请求",
453
+ recentHelp: "本进程内存中的最近 200 条(含轮换事件),不落盘、不跨进程合并。",
454
+ recentEmpty: "暂无最近记录",
455
+ recentOk: "成功",
456
+ rowRequestsTotal: "累计请求 {n}",
457
+ lastActive: "{ago}活跃",
458
+ liveOne: "正在通过 {email} 生成 · {count} 个并发",
459
+ liveMany: "正在生成 · {count} 个并发 · {accounts} 个账号",
460
+ fieldThroughput: "吞吐",
461
+ throughputValue: "≈ {n} token/s",
462
+ throughputNote: "首 token 后 · 累计平均",
414
463
  colActions: "操作",
415
464
  emptyAccounts: "还没有账号。切到「凭据」标签导入,或点击上方「登录」。",
416
465
  detailTitle: "已选账号",
@@ -420,6 +469,7 @@ window.__ModuleLoader__.load({
420
469
  limitsRefreshOk: "已刷新 {measured} 个账号的限额",
421
470
  limitsRefreshFresh: "限额仍是新鲜的,无需刷新",
422
471
  limitsRefreshFailed: "{failed} 个账号的限额刷新失败",
472
+ limitBurnWarn: "按此速度{value}后耗尽",
423
473
  quotaWindow5h: "5 小时",
424
474
  quotaWindowWeekly: "每周",
425
475
  fieldProject: "项目",
@@ -447,13 +497,14 @@ window.__ModuleLoader__.load({
447
497
  quotaResetIn: "重置 {value}",
448
498
  relNow: "即将",
449
499
  relJustNow: "刚刚",
500
+ relSeconds: "{n} 秒",
450
501
  relAgo: "{value}前",
451
502
  relMinutes: "{n} 分钟",
452
503
  relHours: "{n} 小时",
453
504
  relDays: "{n} 天",
454
505
  relMonths: "{n} 个月",
455
506
  relYears: "{n} 年",
456
- actionActivate: "激活",
507
+ actionActivate: "设为当前",
457
508
  actionVerify: "验证",
458
509
  actionDelete: "删除",
459
510
  actionTest: "测试调用",
@@ -494,6 +545,8 @@ window.__ModuleLoader__.load({
494
545
  fieldRateLimitRotation: "限流 / 轮换",
495
546
  byModel: "按模型",
496
547
  byAccount: "按账号",
548
+ trendTitle: "近 7 天",
549
+ colDay: "日期",
497
550
  colModel: "模型",
498
551
  colTokenShare: "Token 占比",
499
552
  colOutput: "输出",
@@ -561,16 +614,35 @@ window.__ModuleLoader__.load({
561
614
  tabModels: "Models",
562
615
  tabUsage: "Usage",
563
616
  tabCredentials: "Credentials",
564
- stateActive: "Active",
617
+ stateActive: "Ready",
565
618
  stateCooling: "Cooling down",
566
619
  stateVerificationRequired: "Needs verification",
567
620
  stateDisabled: "Disabled",
568
621
  coolingUntil: "Cooling until",
569
- currentAccount: "current",
622
+ verificationRetry: "Needs verification · retries {time}",
623
+ currentAccount: "Current",
624
+ fieldDisabled: "Disabled",
625
+ disabledCredentials: "Credentials rejected",
626
+ disabledSince: "disabled {ago}",
627
+ 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
628
  noProject: "—",
571
629
  valueUnknown: "—",
572
630
  colAccount: "Account",
573
631
  colRequests: "Requests",
632
+ colTime: "Time",
633
+ colResult: "Result",
634
+ colDuration: "Duration",
635
+ recentTitle: "Recent requests",
636
+ recentHelp: "Last 200 records in this process's memory (rotation events included); never persisted, never merged across processes.",
637
+ recentEmpty: "No recent activity",
638
+ recentOk: "ok",
639
+ rowRequestsTotal: "{n} requests in total",
640
+ lastActive: "active {ago}",
641
+ liveOne: "Generating via {email} · {count} in flight",
642
+ liveMany: "Generating · {count} in flight across {accounts} account(s)",
643
+ fieldThroughput: "Throughput",
644
+ throughputValue: "≈ {n} token/s",
645
+ throughputNote: "after first token · cumulative",
574
646
  colActions: "Actions",
575
647
  emptyAccounts: "No accounts yet. Import one from the Credentials tab, or use Sign in above.",
576
648
  detailTitle: "Selected account",
@@ -580,6 +652,7 @@ window.__ModuleLoader__.load({
580
652
  limitsRefreshOk: "Refreshed limits for {measured} account(s)",
581
653
  limitsRefreshFresh: "Limits are already fresh — nothing to refresh",
582
654
  limitsRefreshFailed: "Limit refresh failed for {failed} account(s)",
655
+ limitBurnWarn: "at this rate, empty in {value}",
583
656
  quotaWindow5h: "5 hours",
584
657
  quotaWindowWeekly: "Weekly",
585
658
  fieldProject: "Project",
@@ -607,13 +680,14 @@ window.__ModuleLoader__.load({
607
680
  quotaResetIn: "resets {value}",
608
681
  relNow: "shortly",
609
682
  relJustNow: "just now",
683
+ relSeconds: "{n}s",
610
684
  relAgo: "{value} ago",
611
685
  relMinutes: "{n} min",
612
686
  relHours: "{n} h",
613
687
  relDays: "{n} d",
614
688
  relMonths: "{n} mo",
615
689
  relYears: "{n} y",
616
- actionActivate: "Activate",
690
+ actionActivate: "Set as current",
617
691
  actionVerify: "Verify",
618
692
  actionDelete: "Delete",
619
693
  actionTest: "Test call",
@@ -654,6 +728,8 @@ window.__ModuleLoader__.load({
654
728
  fieldRateLimitRotation: "Rate limits / rotations",
655
729
  byModel: "By model",
656
730
  byAccount: "By account",
731
+ trendTitle: "Last 7 days",
732
+ colDay: "Day",
657
733
  colModel: "Model",
658
734
  colTokenShare: "Token share",
659
735
  colOutput: "Output",
@@ -782,6 +858,14 @@ window.__ModuleLoader__.load({
782
858
  * it — see `actionError` in `AgySettings`.
783
859
  */
784
860
  const ACTION_MESSAGE_TTL_MS = 3500;
861
+ /**
862
+ * How often the section re-reads the pool's in-flight snapshot.
863
+ *
864
+ * The call is a pure in-memory read on the host (no token refresh, no quota
865
+ * probe), so a tight-ish interval is free; 3s makes the live line feel live
866
+ * without a visible request cost.
867
+ */
868
+ const POOL_POLL_INTERVAL_MS = 3e3;
785
869
  function h(tag, props, ...children) {
786
870
  return (0, react.createElement)(tag, props ?? null, ...children);
787
871
  }
@@ -854,6 +938,28 @@ window.__ModuleLoader__.load({
854
938
  return counters.input + counters.output + counters.cacheRead + counters.cacheWrite;
855
939
  }
856
940
  /**
941
+ * Cumulative average OUTPUT rate for one scope, in tokens per second.
942
+ *
943
+ * Pure and exported for a direct unit test. The denominator is the STREAMING
944
+ * window (average latency minus average time-to-first-token), NOT the wall
945
+ * clock: on this channel the pre-first-token wait (upstream queue, routing,
946
+ * prompt processing, silent thinking) is ~90% of the request — measured 7.5s
947
+ * of an 8.3s average — so dividing by wall time understated the decode rate
948
+ * about 8× (40 tok/s shown where the account really streamed ~300).
949
+ *
950
+ * Per-request AVERAGES, not raw sums: a failed request carries wall time but
951
+ * never a first token (`latencyN` > `ttftN`), so subtracting raw sums would
952
+ * hand its wait to the decode window. Both averages must exist — a scope with
953
+ * no timed request or no reported first token gets null, hiding the row rather
954
+ * than showing a fake rate.
955
+ */
956
+ function throughputTokenPerSecond(totals) {
957
+ if (totals.latencyN === 0 || totals.ttftN === 0 || totals.output <= 0) return null;
958
+ const decodeMs = totals.latencyMs / totals.latencyN - totals.ttftMs / totals.ttftN;
959
+ if (decodeMs <= 0) return null;
960
+ return Math.round(totals.output / totals.latencyN / (decodeMs / 1e3));
961
+ }
962
+ /**
857
963
  * The whole prompt side: everything the model read, cached or not.
858
964
  *
859
965
  * The LEDGER keeps `input` as the uncached portion alone, because that is DSH's
@@ -918,28 +1024,58 @@ window.__ModuleLoader__.load({
918
1024
  }
919
1025
  }
920
1026
  /**
1027
+ * Humanize a burn horizon (hours until a window runs dry at the sampled rate).
1028
+ *
1029
+ * Reuses the shared `rel*` magnitudes so the phrasing matches every other
1030
+ * duration on the panel; under an hour falls to minutes rather than rounding
1031
+ * to a false zero.
1032
+ */
1033
+ function burnHorizon(hours, t) {
1034
+ if (hours < 1) return t("relMinutes", { n: Math.max(1, Math.round(hours * 60)) });
1035
+ if (hours < 48) return t("relHours", { n: Math.round(hours) });
1036
+ return t("relDays", { n: Math.round(hours / 24) });
1037
+ }
1038
+ /**
921
1039
  * A wall-clock moment for a state label (a cooldown end).
922
1040
  *
923
1041
  * Time-of-day alone is enough while the wall is today; past midnight it must
924
1042
  * carry the date, or a 24h quota cooldown reads as though it ends in a few
925
1043
  * minutes. (`untilText` is the relative form, used where "how long from now" is
926
1044
  * the question rather than "when".)
1045
+ *
1046
+ * `lang` is the UI language ('zh' | 'en'), not the browser locale: the words on
1047
+ * this panel follow the host's language setting, so the dates must too — a zh
1048
+ * panel rendering `9/24/2026` (the browser's en-US ordering) was the symptom.
1049
+ * `undefined` degrades to the browser default, the pre-`lang` behaviour.
927
1050
  */
928
- function clockTime(iso) {
1051
+ function clockTime(iso, lang) {
929
1052
  if (iso === null) return "—";
930
1053
  const date = new Date(iso);
931
1054
  if (Number.isNaN(date.getTime())) return "—";
932
1055
  const now = /* @__PURE__ */ new Date();
933
- return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString([], {
1056
+ return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate() ? date.toLocaleTimeString(lang, {
934
1057
  hour: "2-digit",
935
1058
  minute: "2-digit"
936
- }) : date.toLocaleString([], {
1059
+ }) : date.toLocaleString(lang, {
937
1060
  month: "short",
938
1061
  day: "numeric",
939
1062
  hour: "2-digit",
940
1063
  minute: "2-digit"
941
1064
  });
942
1065
  }
1066
+ /**
1067
+ * A ledger day key ('YYYY-MM-DD', local by construction) as a short label.
1068
+ * Parsed as LOCAL date parts — a UTC parse would shift the label a day for
1069
+ * half the planet.
1070
+ */
1071
+ function dayLabel(day, lang) {
1072
+ const [y, m, d] = day.split("-").map(Number);
1073
+ if (y === void 0 || m === void 0 || d === void 0 || Number.isNaN(y)) return day;
1074
+ return new Date(y, m - 1, d).toLocaleDateString(lang, {
1075
+ month: "short",
1076
+ day: "numeric"
1077
+ });
1078
+ }
943
1079
  const MINUTE_MS = 6e4;
944
1080
  const HOUR_MS = 60 * MINUTE_MS;
945
1081
  const DAY_MS = 24 * HOUR_MS;
@@ -993,6 +1129,34 @@ window.__ModuleLoader__.load({
993
1129
  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
1130
  }
995
1131
  /**
1132
+ * Ago label with seconds resolution, for the recent list. `agoText` collapses
1133
+ * the whole first minute into "just now", which is too coarse when the reader
1134
+ * is watching live activity — here the seconds carry the information.
1135
+ */
1136
+ function recentAgo(at, now, t) {
1137
+ const diff = now - at;
1138
+ if (diff < 1e4) return t("relJustNow");
1139
+ if (diff < MINUTE_MS) return t("relSeconds", { n: Math.floor(diff / 1e3) });
1140
+ return agoText(new Date(at).toISOString(), t, now);
1141
+ }
1142
+ /**
1143
+ * Middle-truncate an identity for the recent list.
1144
+ *
1145
+ * Both ends carry the signal — an email's domain, a model id's tier suffix —
1146
+ * so the cut is taken from the MIDDLE, and bounding the rendered length (not
1147
+ * relying on CSS clipping of a `table-layout: fixed` cell) is what keeps the
1148
+ * columns honest. The full value stays on the cell's title.
1149
+ * @param text - the full identity.
1150
+ * @param max - rendered character budget including the ellipsis.
1151
+ * @returns the truncated display string.
1152
+ */
1153
+ function truncateIdentity(text, max = 22) {
1154
+ if (text.length <= max) return text;
1155
+ const head = Math.ceil((max - 1) / 2);
1156
+ const tail = max - 1 - head;
1157
+ return `${text.slice(0, head)}…${text.slice(-tail)}`;
1158
+ }
1159
+ /**
996
1160
  * A host-styled button.
997
1161
  *
998
1162
  * Delegates to the host's `Button` so focus rings, disabled states, size tiers
@@ -1061,36 +1225,44 @@ window.__ModuleLoader__.load({
1061
1225
  const [proxyDraft, setProxyDraft] = (0, react.useState)("");
1062
1226
  const usage = account.usage;
1063
1227
  const now = Date.now();
1228
+ const throughput = usage === null ? null : throughputTokenPerSecond(usage.totals);
1064
1229
  const identityRows = [
1065
1230
  [t("fieldProject"), account.projectId ?? t("noProject")],
1066
1231
  [t("fieldProxy"), h("span", { className: "agy-mono" }, account.proxy ?? t("proxyDirect"))],
1067
1232
  [t("fieldFingerprint"), account.fingerprint === null ? t("fingerprintNone") : t("fingerprintRegenerated", {
1068
1233
  count: account.fingerprintHistory,
1069
- date: new Date(account.fingerprint.createdAt).toLocaleDateString()
1234
+ date: new Date(account.fingerprint.createdAt).toLocaleDateString(props.lang)
1070
1235
  })],
1071
- [t("fieldCooldownReason"), account.cooldownReason === null ? t("noProject") : `${cooldownReasonLabel(account.cooldownReason, t)} · ${agoText(account.cooldownSetAt, t, now)}`],
1236
+ ...account.cooldownReason === null ? [] : [[t("fieldCooldownReason"), `${cooldownReasonLabel(account.cooldownReason, t)} · ${agoText(account.cooldownSetAt, t, now)}`]],
1072
1237
  [t("fieldSources"), usage === null ? t("noProject") : t("sourcesSummary", {
1073
1238
  chat: usage.sources.chat,
1074
1239
  cli: usage.sources.cli,
1075
1240
  verify: usage.sources.verify,
1076
1241
  test: usage.sources.test
1077
1242
  })],
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)) })}`]
1243
+ [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)) })}`],
1244
+ ...throughput === null ? [] : [[t("fieldThroughput"), `${t("throughputValue", { n: throughput })} · ${t("throughputNote")}`]]
1079
1245
  ];
1246
+ if (account.state === "disabled") identityRows.push([t("fieldDisabled"), account.disabledAt === null ? t("disabledCredentials") : `${t("disabledCredentials")} · ${t("disabledSince", { ago: agoText(account.disabledAt, t, now) })}`]);
1080
1247
  if (account.verificationRequired) identityRows.push([t("fieldVerification"), account.verificationUrl === null ? t("verificationNoUrl") : h("a", {
1081
1248
  className: "agy-link",
1082
1249
  href: account.verificationUrl,
1083
1250
  target: "_blank",
1084
1251
  rel: "noreferrer noopener"
1085
1252
  }, t("verificationOpen"))]);
1086
- const identity = card(t("detailTitle"), defs(identityRows), account.email ?? `#${account.index}`);
1253
+ const identity = card(t("detailTitle"), h("div", null, defs(identityRows), account.state === "disabled" ? hint(t("disabledHint")) : null), account.email ?? `#${account.index}`);
1087
1254
  const actions = card(t("colActions"), h("div", { className: "agy-actions" }, button(t("actionTest"), () => {
1088
1255
  handlers.onTest(account.index);
1089
1256
  }, { disabled: busy }), button(t("actionExport"), () => {
1090
1257
  handlers.onExport(account.index);
1091
1258
  }, { disabled: busy }), button(t("actionRegenerateFingerprint"), () => {
1092
1259
  handlers.onRegenerateFingerprint(account.index);
1093
- }, { disabled: busy })));
1260
+ }, { disabled: busy }), button(t("actionDelete"), () => {
1261
+ handlers.onDelete(account.index);
1262
+ }, {
1263
+ variant: "danger",
1264
+ disabled: busy
1265
+ })));
1094
1266
  /**
1095
1267
  * The 5-hour / weekly windows, placed ABOVE the cumulative usage card.
1096
1268
  *
@@ -1111,13 +1283,14 @@ window.__ModuleLoader__.load({
1111
1283
  key: group.name
1112
1284
  }, h("div", { className: "agy-limit-group-name" }, group.name), ...group.windows.map((window) => {
1113
1285
  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: {
1286
+ const burn = account.limitBurn?.[window.bucketId];
1287
+ const hoursLeft = fraction !== null && burn !== void 0 && burn > 0 ? fraction / burn : null;
1288
+ const resetHours = window.resetTime === null ? null : (new Date(window.resetTime).getTime() - now) / HOUR_MS;
1289
+ const exhaustsFirst = hoursLeft !== null && resetHours !== null && hoursLeft < resetHours;
1290
+ 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
1291
  width: `${Math.round(fraction * 100)}%`,
1119
1292
  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)));
1293
+ } })), 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
1294
  })))));
1122
1295
  const usageBlock = usage === null ? null : card(t("usageCumulative"), metrics([
1123
1296
  metric(t("kpiInput"), promptTokens(usage.totals), t("kpiInputMissed", { tokens: tokenText(usage.totals.input) })),
@@ -1154,6 +1327,19 @@ window.__ModuleLoader__.load({
1154
1327
  })())));
1155
1328
  }
1156
1329
  /**
1330
+ * Whether the row should offer "set as current".
1331
+ *
1332
+ * A pure rule, exported for a direct unit test. Two cases hide the action:
1333
+ * the account is already the pool's preference, or it is DISABLED — `activate`
1334
+ * only writes the preference and cannot re-enable, so on a disabled row the
1335
+ * button was a no-op the user would read as broken. The repair path for a
1336
+ * button was a no-op the user would read as broken. The repair path for a
1337
+ * disabled account is `actionVerify`, which stays on the row.
1338
+ */
1339
+ function canActivateAccount(account) {
1340
+ return !account.active && account.state !== "disabled";
1341
+ }
1342
+ /**
1157
1343
  * Pick the selected account index: defaults to the active account (badge "current")
1158
1344
  * when present, otherwise falls back to index 0. Clamps to valid bounds.
1159
1345
  */
@@ -1171,6 +1357,29 @@ window.__ModuleLoader__.load({
1171
1357
  const { accounts, busy, handlers, t } = props;
1172
1358
  const [selected, setSelected] = (0, react.useState)(null);
1173
1359
  const selectedRef = (0, react.useRef)(null);
1360
+ const now = Date.now();
1361
+ /**
1362
+ * The live line: who is serving right now, and how much.
1363
+ *
1364
+ * Present ONLY while something is in flight — an idle pool renders no strip,
1365
+ * so the quiet state stays quiet. The dot is the host `StateDot`'s ongoing
1366
+ * state, so the animation is the platform's.
1367
+ */
1368
+ const liveLine = props.busyNow.length === 0 ? null : (() => {
1369
+ const total = props.busyNow.reduce((sum, entry) => sum + entry.count, 0);
1370
+ const [first] = props.busyNow;
1371
+ const subject = first === void 0 ? "" : first.email ?? `#${first.index}`;
1372
+ return h("div", { className: "agy-live" }, h(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
1373
+ state: "ongoing",
1374
+ size: 8
1375
+ }), h("span", null, props.busyNow.length === 1 ? t("liveOne", {
1376
+ email: subject,
1377
+ count: total
1378
+ }) : t("liveMany", {
1379
+ count: total,
1380
+ accounts: props.busyNow.length
1381
+ })));
1382
+ })();
1174
1383
  const index = resolveSelectedAccountIndex(accounts, selected);
1175
1384
  const current = accounts[index];
1176
1385
  (0, react.useEffect)(() => {
@@ -1194,32 +1403,35 @@ window.__ModuleLoader__.load({
1194
1403
  event.preventDefault();
1195
1404
  setSelected(at);
1196
1405
  }
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"), () => {
1406
+ }, 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
1407
  setSelected(at);
1199
1408
  handlers.onActivate(account.index);
1200
1409
  }, {
1201
1410
  size: "sm",
1202
1411
  disabled: busy
1203
- }), button(t("actionVerify"), () => {
1412
+ }) : null, button(t("actionVerify"), () => {
1204
1413
  handlers.onVerify(account.index);
1205
1414
  }, {
1206
1415
  size: "sm",
1207
1416
  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
1417
  }))));
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, {
1418
+ 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
1419
  key: String(current.index),
1218
1420
  account: current,
1219
1421
  busy,
1220
- handlers,
1422
+ handlers: {
1423
+ ...handlers,
1424
+ onDelete: (index) => {
1425
+ setSelected(null);
1426
+ handlers.onDelete(index);
1427
+ }
1428
+ },
1429
+ lang: props.lang,
1221
1430
  t
1222
- }))));
1431
+ })), h(RecentCard, {
1432
+ rpc: props.rpc,
1433
+ t
1434
+ })));
1223
1435
  }
1224
1436
  /**
1225
1437
  * Models tab: per-model visibility switches, plus the account's model quota in a
@@ -1577,6 +1789,110 @@ window.__ModuleLoader__.load({
1577
1789
  }))) : null);
1578
1790
  return card(t("thinkingTitle"), block);
1579
1791
  }
1792
+ /** Result vocabulary for one recent row; rotation events are their own kind. */
1793
+ function recentResultKind(entry) {
1794
+ if (entry.kind === "rotation") return "rotation";
1795
+ if (entry.rateLimited) return "limited";
1796
+ if (!entry.ok) return "fail";
1797
+ return "ok";
1798
+ }
1799
+ /**
1800
+ * Localized label for a failure-classification token, falling back to the raw
1801
+ * token so a classification added upstream still reads as something.
1802
+ */
1803
+ function failureReasonLabel(reason, t) {
1804
+ switch (reason) {
1805
+ case "rate-limit": return t("colRateLimited");
1806
+ case "network-error": return t("cooldownReasonNetworkError");
1807
+ case "auth-failure": return t("disabledCredentials");
1808
+ case "verification-required": return t("cooldownReasonValidationRequired");
1809
+ case "quota-exhausted": return t("cooldownReasonQuotaExhausted");
1810
+ case "project-error": return t("cooldownReasonProjectError");
1811
+ default: return reason;
1812
+ }
1813
+ }
1814
+ /**
1815
+ * The result cell's text. Rotation rows say WHY (`reason` rides the record
1816
+ * since the ring captures the classification); a failed request with a
1817
+ * non-rate-limit classification also names it — the rate-limit case is already
1818
+ * the whole RateLimited label, and doubling it reads as a stutter.
1819
+ */
1820
+ function recentResultText(entry, t) {
1821
+ const kind = recentResultKind(entry);
1822
+ if (kind === "rotation") return entry.reason === null ? t("colRotations") : `${t("colRotations")} · ${failureReasonLabel(entry.reason, t)}`;
1823
+ if (kind === "limited") return t("colRateLimited");
1824
+ if (kind === "fail") return entry.reason === null || entry.reason === "rate-limit" ? t("colFailed") : `${t("colFailed")} · ${failureReasonLabel(entry.reason, t)}`;
1825
+ return t("recentOk");
1826
+ }
1827
+ /**
1828
+ * The "what just happened" list: the most recent records this process saw,
1829
+ * newest first, collapsed by default and polled while open.
1830
+ *
1831
+ * A component (never called directly) — it owns hooks; see ThinkingSamples for
1832
+ * the hook-order rule this file has been bitten by twice. The poll is the same
1833
+ * 3s cadence as the live line and reads the host's memory ring, so an open
1834
+ * list costs nothing upstream.
1835
+ */
1836
+ function RecentCard(props) {
1837
+ const { rpc, t } = props;
1838
+ const [open, setOpen] = (0, react.useState)(false);
1839
+ const [recent, setRecent] = (0, react.useState)(null);
1840
+ const [error, setError] = (0, react.useState)(void 0);
1841
+ const alive = (0, react.useRef)(true);
1842
+ (0, react.useEffect)(() => {
1843
+ alive.current = true;
1844
+ return () => {
1845
+ alive.current = false;
1846
+ };
1847
+ }, []);
1848
+ const load = (0, react.useCallback)(() => {
1849
+ rpc.call("pool.recent", {}).then((result) => {
1850
+ if (alive.current) {
1851
+ setRecent(result.recent);
1852
+ setError(void 0);
1853
+ }
1854
+ }).catch((caught) => {
1855
+ if (alive.current) setError(caught instanceof Error ? caught.message : String(caught));
1856
+ });
1857
+ }, [rpc]);
1858
+ (0, react.useEffect)(() => {
1859
+ if (open && recent === null) load();
1860
+ }, [
1861
+ open,
1862
+ recent,
1863
+ load
1864
+ ]);
1865
+ (0, react.useEffect)(() => {
1866
+ if (!open) return;
1867
+ const timer = setInterval(() => {
1868
+ if (!document.hidden) load();
1869
+ }, POOL_POLL_INTERVAL_MS);
1870
+ return () => {
1871
+ clearInterval(timer);
1872
+ };
1873
+ }, [open, load]);
1874
+ const now = Date.now();
1875
+ return h("div", {
1876
+ className: "agy-disclosure agy-recent",
1877
+ "data-open": open
1878
+ }, h("button", {
1879
+ type: "button",
1880
+ className: "agy-disclosure-toggle",
1881
+ "aria-expanded": open,
1882
+ onClick: () => {
1883
+ setOpen(!open);
1884
+ }
1885
+ }, 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), hint(t("recentHelp")), 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", {
1886
+ className: "agy-mail",
1887
+ title: entry.account ?? void 0
1888
+ }, entry.account === null ? "—" : truncateIdentity(entry.account))), h("td", null, h("span", {
1889
+ className: "agy-mail",
1890
+ title: entry.model ?? void 0
1891
+ }, entry.model === null ? "—" : truncateIdentity(entry.model))), h("td", null, h("span", {
1892
+ className: "agy-recent-state",
1893
+ "data-kind": recentResultKind(entry)
1894
+ }, recentResultText(entry, t))), h("td", null, entry.latencyMs === null ? "—" : formatDuration(entry.latencyMs)), h("td", { className: "agy-num" }, entry.output === null ? "—" : tokenText(entry.output))))))));
1895
+ }
1580
1896
  const RANGE_IDS = [
1581
1897
  "today",
1582
1898
  "week",
@@ -1688,7 +2004,7 @@ window.__ModuleLoader__.load({
1688
2004
  onClick: () => {
1689
2005
  setRange(id);
1690
2006
  }
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([
2007
+ }, 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
2008
  metric(t("kpiTotalTokens"), total, t("kpiTotalDetail", {
1693
2009
  requests: counters.requests,
1694
2010
  failed: counters.failed
@@ -1701,7 +2017,7 @@ window.__ModuleLoader__.load({
1701
2017
  [t("fieldRateLimitRotation"), `${counters.rateLimited} / ${counters.rotations}`],
1702
2018
  [t("labelLatencyAverage"), formatDuration(average(counters.latencyMs, counters.latencyN))],
1703
2019
  [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))))))));
2020
+ ])), 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
2021
  }
1706
2022
  function CredentialsTab(props) {
1707
2023
  const { t } = props;
@@ -1741,6 +2057,15 @@ window.__ModuleLoader__.load({
1741
2057
  /** Model ids whose own test call is in flight (see the per-row test button). */
1742
2058
  const [modelTesting, setModelTesting] = (0, react.useState)(() => /* @__PURE__ */ new Set());
1743
2059
  const [stats, setStats] = (0, react.useState)(null);
2060
+ /**
2061
+ * Accounts with upstream requests in flight, from the polled `pool.status`.
2062
+ *
2063
+ * A poll, not a push: the section has no push channel, and the call is a pure
2064
+ * in-memory read on the host. A failed poll is display-only and stays silent —
2065
+ * the previous frame (including "idle") holds until the next tick, which is
2066
+ * the honest degradation for a status line.
2067
+ */
2068
+ const [poolBusy, setPoolBusy] = (0, react.useState)([]);
1744
2069
  const [error, setError] = (0, react.useState)(void 0);
1745
2070
  /** A non-fatal outcome worth reporting (e.g. a partial credential import). */
1746
2071
  const [notice, setNoticeState] = (0, react.useState)(void 0);
@@ -1842,7 +2167,8 @@ window.__ModuleLoader__.load({
1842
2167
  return {
1843
2168
  ...account,
1844
2169
  limits: entry.groups,
1845
- limitsUpdatedAt: entry.updatedAt
2170
+ limitsUpdatedAt: entry.updatedAt,
2171
+ limitBurn: entry.burn ?? account.limitBurn
1846
2172
  };
1847
2173
  }));
1848
2174
  if (report) {
@@ -1862,6 +2188,19 @@ window.__ModuleLoader__.load({
1862
2188
  (0, react.useEffect)(() => {
1863
2189
  refresh();
1864
2190
  }, [refresh]);
2191
+ (0, react.useEffect)(() => {
2192
+ const tick = () => {
2193
+ if (document.hidden) return;
2194
+ rpc.call("pool.status", {}).then((result) => {
2195
+ if (alive.current) setPoolBusy(result.busy);
2196
+ }).catch(() => {});
2197
+ };
2198
+ tick();
2199
+ const timer = setInterval(tick, POOL_POLL_INTERVAL_MS);
2200
+ return () => {
2201
+ clearInterval(timer);
2202
+ };
2203
+ }, [rpc]);
1865
2204
  (0, react.useEffect)(() => {
1866
2205
  if (tab === "accounts" && accounts.length > 0) loadLimits();
1867
2206
  }, [
@@ -2075,7 +2414,10 @@ window.__ModuleLoader__.load({
2075
2414
  const body = tab === "accounts" ? h(AccountsTab, {
2076
2415
  accounts,
2077
2416
  busy,
2417
+ busyNow: poolBusy,
2078
2418
  handlers,
2419
+ lang: props.lang,
2420
+ rpc,
2079
2421
  t
2080
2422
  }) : tab === "models" ? modelError === void 0 ? h(ModelsTab, {
2081
2423
  models,
@@ -2152,6 +2494,7 @@ window.__ModuleLoader__.load({
2152
2494
  loadModels();
2153
2495
  }, { size: "sm" })) : tab === "usage" ? h(UsageTab, {
2154
2496
  stats,
2497
+ lang: props.lang,
2155
2498
  t
2156
2499
  }) : h(CredentialsTab, {
2157
2500
  busy,
@@ -2205,6 +2548,7 @@ window.__ModuleLoader__.load({
2205
2548
  }
2206
2549
  const rpc = createRpc(connection);
2207
2550
  const t = ctx.locale.bind(NS);
2551
+ const lang = ctx.locale.getLocale?.().active;
2208
2552
  ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
2209
2553
  name: "settings.section",
2210
2554
  id: "agy",
@@ -2213,16 +2557,20 @@ window.__ModuleLoader__.load({
2213
2557
  label: () => t("title")
2214
2558
  }, () => h(AgySettings, {
2215
2559
  rpc,
2216
- t
2560
+ t,
2561
+ lang
2217
2562
  }))), "dsh-agy: Settings section");
2218
2563
  }
2219
2564
  //#endregion
2220
2565
  exports.AgySettings = AgySettings;
2221
2566
  exports.apply = apply;
2567
+ exports.canActivateAccount = canActivateAccount;
2222
2568
  exports.inject = inject;
2223
2569
  exports.orderModels = orderModels;
2224
2570
  exports.resolveSelectedAccountIndex = resolveSelectedAccountIndex;
2571
+ exports.throughputTokenPerSecond = throughputTokenPerSecond;
2225
2572
  exports.tokenText = tokenText;
2573
+ exports.truncateIdentity = truncateIdentity;
2226
2574
  return module.exports;
2227
2575
  }
2228
2576
  });