dsh-connect-workbuddy 3.1.0 → 3.4.0

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
@@ -22,12 +22,19 @@ window.__ModuleLoader__.load({
22
22
  */
23
23
  /** Plugin-owned usage endpoint consumed by its browser half. */
24
24
  const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
25
+ /**
26
+ * The project's public repository.
27
+ *
28
+ * Lives here rather than beside one component because TWO sections link to it
29
+ * now — the model section and the account pool — and a second copy of a URL is
30
+ * a second thing to forget when it moves. Node-free and side-effect free, so it
31
+ * costs nothing on the Host side either.
32
+ */
33
+ const WORKBUDDY_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-workbuddy";
25
34
  /** Plugin-owned live model refresh endpoint. */
26
35
  const WORKBUDDY_MODELS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/models/refresh";
27
36
  /** Plugin-owned local account rescan endpoint. */
28
37
  const WORKBUDDY_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-workbuddy/accounts/refresh";
29
- /** Plugin-owned daily check-in action endpoint. */
30
- const WORKBUDDY_CHECKIN_PATH = "/plugins/dsh-connect-workbuddy/checkin";
31
38
  /**
32
39
  * Plugin-owned model probe endpoint.
33
40
  *
@@ -501,7 +508,7 @@ window.__ModuleLoader__.load({
501
508
  *
502
509
  * @param scope - the bound settings scope for this plugin's namespace.
503
510
  * @param region - the region whose pool preferences are written.
504
- * @param preferences - the four preference fields, as the user set them.
511
+ * @param preferences - the preference fields, as the user set them.
505
512
  * @param slot - the region's COMPLETE next slot (caller owns the merge).
506
513
  * @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
507
514
  */
@@ -510,7 +517,7 @@ window.__ModuleLoader__.load({
510
517
  await writeField(scope, "regions", region, next, (readBack) => {
511
518
  const pool = readBack?.pool;
512
519
  if (pool === void 0 || pool === null) return false;
513
- if (pool["enabled"] !== preferences.enabled || pool["rotateByCredits"] !== preferences.rotateByCredits || pool["autoTestIntervalMinutes"] !== preferences.autoTestIntervalMinutes || pool["targetModelId"] !== preferences.targetModelId) return false;
520
+ if (pool["enabled"] !== preferences.enabled || pool["targetModelId"] !== preferences.targetModelId) return false;
514
521
  const stored = pool["memberAccountIds"];
515
522
  if (!Array.isArray(stored) || stored.length !== preferences.memberAccountIds.length) return false;
516
523
  const wanted = new Set(preferences.memberAccountIds);
@@ -518,6 +525,133 @@ window.__ModuleLoader__.load({
518
525
  });
519
526
  }
520
527
  //#endregion
528
+ //#region src/client/ndjson.ts
529
+ /**
530
+ * Read a newline-delimited JSON body, one object at a time.
531
+ *
532
+ * NDJSON rather than SSE or a hand-rolled frame format because the pool's batch
533
+ * is a list of INDEPENDENT results: one line per result needs no state machine,
534
+ * survives a partially-delivered batch, and lets the reader act on account N
535
+ * while account N+1 is still being tested.
536
+ *
537
+ * A malformed line is SKIPPED, not fatal. The stream carries live progress, so
538
+ * discarding the rest of a batch because one frame did not parse would throw away
539
+ * rows the user already watched arrive — and a truncated final line is a normal
540
+ * way for a stream to end when something upstream misbehaves.
541
+ *
542
+ * @module dsh-connect-workbuddy/client/ndjson
543
+ */
544
+ /** Hand each parsed object to `onLine`, in order, as it arrives. */
545
+ async function readNdjson(body, onLine) {
546
+ const reader = body?.getReader();
547
+ if (reader === void 0) return;
548
+ const decoder = new TextDecoder();
549
+ const emit = (line) => {
550
+ const trimmed = line.trim();
551
+ if (trimmed === "") return;
552
+ try {
553
+ const parsed = JSON.parse(trimmed);
554
+ if (typeof parsed === "object" && parsed !== null) onLine(parsed);
555
+ } catch {}
556
+ };
557
+ let buffer = "";
558
+ try {
559
+ for (;;) {
560
+ const { done, value } = await reader.read();
561
+ if (done) break;
562
+ buffer += decoder.decode(value, { stream: true });
563
+ const lines = buffer.split("\n");
564
+ buffer = lines.pop() ?? "";
565
+ for (const line of lines) emit(line);
566
+ }
567
+ buffer += decoder.decode();
568
+ emit(buffer);
569
+ } finally {
570
+ reader.releaseLock();
571
+ }
572
+ }
573
+ //#endregion
574
+ //#region src/client/probe-reason.ts
575
+ /**
576
+ * Outcomes whose label + stated recovery time ARE the whole story.
577
+ *
578
+ * Both carry a reset instant, which the row prints right beside them; their
579
+ * bodies restate that instant and add a `code`/`requestId` pair nobody reads.
580
+ *
581
+ * `credential-rejected` and `not-found` are deliberately NOT here: their labels
582
+ * state a conclusion ("sign in again", "no such model"), and the upstream's own
583
+ * words are what make that conclusion checkable rather than asserted.
584
+ */
585
+ const SELF_EXPLANATORY = /* @__PURE__ */ new Set(["rate-limited", "out-of-credit"]);
586
+ /**
587
+ * Longest inline reason kept.
588
+ *
589
+ * A cap rather than a wrapper: one pathological body must not push the rest of
590
+ * the table off screen. 72 characters holds a full transport error and a full
591
+ * upstream sentence, while stopping well short of a raw JSON document.
592
+ */
593
+ const REASON_LIMIT = 72;
594
+ /** The inline reason for one measurement, or `undefined` when it adds nothing. */
595
+ function inlineProbeReason(outcome, message) {
596
+ if (message === void 0 || message === "") return void 0;
597
+ if (SELF_EXPLANATORY.has(outcome)) return void 0;
598
+ const text = (humanSentenceOf(message) ?? stripTransportPrefix(message)).trim();
599
+ return text === "" ? void 0 : truncate(text);
600
+ }
601
+ function truncate(text) {
602
+ return text.length <= REASON_LIMIT ? text : `${text.slice(0, 71)}…`;
603
+ }
604
+ /** `{"code":6004,"msg":"…","requestId":"…"}` → the `msg` sentence, if it has one. */
605
+ function humanSentenceOf(message) {
606
+ if (!message.startsWith("{")) return void 0;
607
+ let parsed;
608
+ try {
609
+ parsed = JSON.parse(message);
610
+ } catch {
611
+ return;
612
+ }
613
+ if (typeof parsed !== "object" || parsed === null) return void 0;
614
+ const msg = parsed["msg"];
615
+ return typeof msg === "string" && msg !== "" ? msg : void 0;
616
+ }
617
+ /** `transport error: TimeoutError: …` → `TimeoutError: …`. */
618
+ function stripTransportPrefix(message) {
619
+ return message.replace(/^transport error:\s*/u, "");
620
+ }
621
+ //#endregion
622
+ //#region src/client/remaining.ts
623
+ const MINUTE_MS = 6e4;
624
+ const HOUR_MS = 60 * MINUTE_MS;
625
+ const DAY_MS = 24 * HOUR_MS;
626
+ /**
627
+ * How far out hours stop being the useful unit.
628
+ *
629
+ * Below this, hours; at or above it, days. The cut is at TWO days rather than
630
+ * one so that 30 hours reads as "约 30 小时后" instead of rounding to "约 1 天后",
631
+ * which would understate it by six hours.
632
+ */
633
+ const DAYS_AT_MS = 2 * DAY_MS;
634
+ /**
635
+ * A short "约 N 分钟后 / 约 N 小时后 / 约 N 天后", or `''` once the moment passed.
636
+ *
637
+ * The empty string is the contract callers depend on: an elapsed cooldown means
638
+ * the account is already back, so appending "in 0 minutes" would assert the
639
+ * opposite of the state the card is showing. Callers append this only when it is
640
+ * non-empty.
641
+ *
642
+ * Rounding is deliberately coarse — "约 1 小时后" for 69 minutes is what a reader
643
+ * wants, and a precision that changes every minute would be noise rather than
644
+ * information. The absolute instant sits right beside it for anyone who needs it.
645
+ */
646
+ function remainingText(t, untilMs, nowMs) {
647
+ const left = untilMs - nowMs;
648
+ if (left <= 0) return "";
649
+ if (left < MINUTE_MS) return t("row.remainingImminent");
650
+ if (left < HOUR_MS) return t("row.remainingMinutes", { count: Math.round(left / MINUTE_MS) });
651
+ if (left < DAYS_AT_MS) return t("row.remainingHours", { count: Math.round(left / HOUR_MS) });
652
+ return t("row.remainingDays", { count: Math.round(left / DAY_MS) });
653
+ }
654
+ //#endregion
521
655
  //#region src/account-pool.ts
522
656
  /**
523
657
  * The checked ids that still resolve to a local sign-in, in the caller's order.
@@ -582,28 +716,6 @@ window.__ModuleLoader__.load({
582
716
  * @module dsh-connect-workbuddy/client/pool-state
583
717
  */
584
718
  /**
585
- * Whether rotation currently owns the region's billing choice, and whether the
586
- * user has drafted turning it off.
587
- *
588
- * `locked` reads the COMMITTED preferences, never the draft. The Host decides
589
- * who is billed from the saved config, so a draft-based lock let one click of
590
- * "turn off rotation" re-enable the manual dropdown and hide the conflict
591
- * notice while the Host kept rotating — the user then picked an account, the
592
- * card showed it as current, and a different account was billed. That is
593
- * exactly the failure the lock exists to prevent.
594
- *
595
- * `unlockPending` is the honest intermediate state: the draft says "stop
596
- * rotating" but the Host has not been told yet, so the lock still applies and
597
- * the UI must say so rather than looking broken.
598
- */
599
- function rotationLockState(saved, active) {
600
- const locked = saved.enabled && saved.rotateByCredits;
601
- return {
602
- locked,
603
- unlockPending: locked && !(active.enabled && active.rotateByCredits)
604
- };
605
- }
606
- /**
607
719
  * The base a draft edit starts from.
608
720
  *
609
721
  * Resolved at EDIT time rather than captured when the callback was built. The
@@ -667,7 +779,6 @@ window.__ModuleLoader__.load({
667
779
  function announcedBatchCount(input) {
668
780
  return (input.savedEffective ?? input.savedMembers).length;
669
781
  }
670
- const POOL_INTERVAL_MAX = 1440;
671
782
  /**
672
783
  * Localize a pool route failure from its structured cause.
673
784
  *
@@ -689,11 +800,11 @@ window.__ModuleLoader__.load({
689
800
  */
690
801
  function poolErrorText(t, reason, detail) {
691
802
  switch (reason) {
692
- case "pool-disabled": return t("row.poolErrDisabled");
693
803
  case "no-members": return t("row.poolErrNoMembers");
694
804
  case "no-live-members": return t("row.poolErrNoLiveMembers");
695
805
  case "no-free-model": return t("row.poolErrNoFreeModel");
696
806
  case "target-model-stale": return t("row.poolErrStaleModel");
807
+ case "checkin-unsupported": return t("row.poolErrNoCheckin");
697
808
  case "pool-unavailable": return t("row.poolErrUnavailable");
698
809
  case "pool-failed": return t("row.poolErrFailed", { message: detail ?? "" });
699
810
  default: return;
@@ -722,46 +833,6 @@ window.__ModuleLoader__.load({
722
833
  return poolErrorText(t, input.reason, input.error) ?? input.error ?? t("row.poolErrHttp", { status: String(input.status) });
723
834
  }
724
835
  /**
725
- * Parse an in-progress interval edit.
726
- *
727
- * Returns `undefined` while the text is not yet a plain non-negative integer —
728
- * empty, `-`, `12e`, `1.5` — so the caller can leave EXACTLY what the user typed
729
- * on screen instead of rewriting the control from a parsed number.
730
- *
731
- * That rewriting was the defect this replaces: the field clamped on every
732
- * keystroke and wrote the clamped result back into a controlled `value`, so
733
- * typing `120` produced `520` (each keystroke's clamped `5` was rendered, and the
734
- * next digit appended to it) and clearing the field immediately snapped it to
735
- * `5`. The user's original request was "设置一个时间间隔自动测试" — a field that
736
- * cannot be typed into is that requirement unmet, not a cosmetic wart.
737
- */
738
- function parseIntervalInput(raw) {
739
- const trimmed = raw.trim();
740
- if (!/^[0-9]+$/.test(trimmed)) return void 0;
741
- const parsed = Number(trimmed);
742
- if (!Number.isFinite(parsed)) return void 0;
743
- return Math.min(POOL_INTERVAL_MAX, Math.max(5, Math.round(parsed)));
744
- }
745
- /**
746
- * One keystroke in the interval field: what to DISPLAY, and what to commit.
747
- *
748
- * `text` is ALWAYS the raw keystroke, never a re-serialized number — that
749
- * distinction is the whole defect. A field whose displayed value is derived from
750
- * the parsed-and-clamped number cannot be typed into: each keystroke's clamped
751
- * `5` is rendered, and the next digit appends to it (`120` → `520`).
752
- *
753
- * `commit` is the clamped number to fold into the draft, or `undefined` while the
754
- * text is not yet a plain integer (so the draft is left alone until the entry is
755
- * meaningful). Splitting the two makes the invariant unit-testable without a DOM,
756
- * which this project has no way to render.
757
- */
758
- function intervalEditOnInput(raw) {
759
- return {
760
- text: raw,
761
- commit: parseIntervalInput(raw)
762
- };
763
- }
764
- /**
765
836
  * Pool MEMBERS that can serve right now.
766
837
  *
767
838
  * Judged over members only. Using the whole account table made the "no usable
@@ -785,15 +856,15 @@ window.__ModuleLoader__.load({
785
856
  * 改动:把「账号池」这一整块独立成模块。原因是它有三条别的区块没有的规则,
786
857
  * 放在卡片里会被淹没:
787
858
  *
788
- * 1. **两项互斥。** 自动轮换开启时,上方卡片原有的手动账号下拉必须停用 ——
789
- * 否则会出现"下拉显示 A、实际计费 B"。本模块通过 `onRotationChange`
790
- * 把这个状态交回卡片,由卡片去禁用那个下拉。
791
- * 2. **两种状态、两套写入。** 四项偏好走草稿 + 保存(保存成功才丢弃草稿);
792
- * 测试结果与轮换是**运行时事实**,由 Host 自动落盘,绝不经这个保存按钮。
793
- * 把它们混在一起,一次「保存」就会用旧草稿覆盖刚测出的结果。
794
- * 3. **免费模型可能不存在。** 国内版静态目录不带倍率,所以「自动挑免费模型」
859
+ * 1. **一种状态、两套写入。** 偏好(启用、目标模型、成员)走草稿 + 保存
860
+ * (保存成功才丢弃草稿);**测试结果**是运行时事实,由 Host 在批量测试后
861
+ * 自动落盘,绝不经这个保存按钮。把它们混在一起,一次「保存」就会用旧草稿
862
+ * 覆盖刚测出的结果。
863
+ * 2. **免费模型可能不存在。** 国内版静态目录不带倍率,所以「自动挑免费模型」
795
864
  * 在国内版首次刷新前会挑到空。此时**不回退到收费模型**,而是如实说明并
796
865
  * 停用测试按钮。
866
+ * 3. **签到只在有它的区域出现。** 海外区域没有签到,按钮与那一列都不渲染;
867
+ * Host 侧同样拒绝该动作,免得留下一个界面看不见、接口却还能打的死角。
797
868
  *
798
869
  * @module dsh-connect-workbuddy/client/AccountPool
799
870
  */
@@ -805,12 +876,10 @@ window.__ModuleLoader__.load({
805
876
  * The card unmounts its whole body when collapsed (`WorkBuddyCard.tsx:832`
806
877
  * renders it only while `open`), and again whenever a poll briefly leaves the
807
878
  * region unsigned-in. Component state therefore lost the entire log on every
808
- * collapse, and — because the rotation bookkeeping reset with it — wrote a
809
- * PHANTOM "switched from — to X" record on the next open. Holding both here
810
- * keeps the history across remounts and across tab switches.
879
+ * collapse. Holding it here keeps the history across remounts and across tab
880
+ * switches.
811
881
  */
812
882
  const logStore = /* @__PURE__ */ new Map();
813
- const rotationMemory = /* @__PURE__ */ new Map();
814
883
  /**
815
884
  * Whether two id sets are equal, ignoring order.
816
885
  *
@@ -899,7 +968,7 @@ window.__ModuleLoader__.load({
899
968
  * section implying the feature exists.
900
969
  */
901
970
  function AccountPool(props) {
902
- const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRotationChange } = props;
971
+ const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount } = props;
903
972
  const [draft, setDraft] = (0, react.useState)(null);
904
973
  const [saving, setSaving] = (0, react.useState)(false);
905
974
  const [saveError, setSaveError] = (0, react.useState)(void 0);
@@ -908,19 +977,8 @@ window.__ModuleLoader__.load({
908
977
  const [log, setLog] = (0, react.useState)(() => logStore.get(region) ?? []);
909
978
  (0, react.useEffect)(() => {
910
979
  setLog(logStore.get(region) ?? []);
911
- setIntervalText(void 0);
912
980
  }, [region]);
913
981
  const [draftRegion, setDraftRegion] = (0, react.useState)(void 0);
914
- /**
915
- * The interval field's raw text while it is being typed.
916
- *
917
- * `undefined` means "not editing" and the control renders the committed
918
- * number. Keeping the in-progress string here is what makes the field
919
- * typeable: clamping on every keystroke and writing the result straight back
920
- * into the controlled `value` turned `120` into `520` and made an empty field
921
- * snap to `5`. See `parseIntervalInput`.
922
- */
923
- const [intervalText, setIntervalText] = (0, react.useState)(void 0);
924
982
  const mounted = (0, react.useRef)(true);
925
983
  (0, react.useEffect)(() => {
926
984
  mounted.current = true;
@@ -930,14 +988,11 @@ window.__ModuleLoader__.load({
930
988
  }, []);
931
989
  const saved = {
932
990
  enabled: pool?.enabled ?? false,
933
- rotateByCredits: pool?.rotateByCredits ?? false,
934
- autoTestIntervalMinutes: pool?.autoTestIntervalMinutes ?? 30,
935
991
  targetModelId: pool?.targetModelId || pool?.staleTargetModelId || "",
936
992
  memberAccountIds: pool?.memberAccountIds ?? []
937
993
  };
938
994
  const active = draftRegion === region && draft !== null ? draft : saved;
939
- const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.rotateByCredits !== saved.rotateByCredits || draft.autoTestIntervalMinutes !== saved.autoTestIntervalMinutes || draft.targetModelId !== saved.targetModelId || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
940
- const current = pool?.accounts.find((account) => account.current);
995
+ const dirty = draftRegion === region && draft !== null && (draft.enabled !== saved.enabled || draft.targetModelId !== saved.targetModelId || !sameIds(draft.memberAccountIds, saved.memberAccountIds));
941
996
  /**
942
997
  * Membership that actually resolves to a local sign-in — what a batch runs on.
943
998
  *
@@ -970,29 +1025,7 @@ window.__ModuleLoader__.load({
970
1025
  */
971
1026
  const usableMemberSet = new Set(usableMemberIds(pool?.accounts ?? [], effectiveMembers, active.memberAccountIds));
972
1027
  const usable = (pool?.accounts ?? []).filter((account) => usableMemberSet.has(account.accountId));
973
- /**
974
- * Whether rotation currently owns this region's choice — read from the
975
- * COMMITTED preferences, not the draft.
976
- *
977
- * The distinction is the whole point. The Host decides who is billed from the
978
- * saved config, so a draft-based lock let one click of "turn off rotation"
979
- * re-enable the manual dropdown and hide the conflict notice while the Host
980
- * kept rotating — the user then picked an account, the card showed it as
981
- * current, and a different account was billed. That is exactly the failure
982
- * the lock exists to prevent.
983
- *
984
- * Reading the committed value means the dropdown stays disabled until the
985
- * change is SAVED, which is honest: until then the Host really is still
986
- * rotating. The conflict notice says so and offers to save.
987
- */
988
- const { locked: rotationLocked, unlockPending: rotationUnlockPending } = rotationLockState(saved, active);
989
1028
  const canEditPool = settingsScope !== void 0;
990
- (0, react.useEffect)(() => {
991
- onRotationChange?.(rotationLocked);
992
- return () => {
993
- onRotationChange?.(false);
994
- };
995
- }, [onRotationChange, rotationLocked]);
996
1029
  (0, react.useEffect)(() => {
997
1030
  onBusyChange?.(saving);
998
1031
  return () => {
@@ -1011,75 +1044,6 @@ window.__ModuleLoader__.load({
1011
1044
  });
1012
1045
  }, [region]);
1013
1046
  /**
1014
- * Record a rotation the HOST performed.
1015
- *
1016
- * Rotation can happen on a timer with no card interaction, so the browser
1017
- * cannot infer it — the Host reports which account it moved to and this logs
1018
- * the transition. Without it "every switch is recorded" was a promise the UI
1019
- * had no way to keep.
1020
- */
1021
- /**
1022
- * Record rotation transitions the HOST performed.
1023
- *
1024
- * Rotation can happen on a timer with no card interaction, so the browser
1025
- * cannot infer it — the Host reports which account it moved to and this logs
1026
- * the change.
1027
- *
1028
- * Three defects are fixed here, all from keeping one `useRef` for a value
1029
- * that is per-region and long-lived:
1030
- *
1031
- * 1. **No phantom first record.** The ref used to start `undefined`, so the
1032
- * first rotation the card ever SAW was reported as a fresh switch ("from
1033
- * — to X") even when it had happened long before. The first observation
1034
- * now PRIMES the ref instead of logging.
1035
- * 2. **Per-region state.** The card keeps ONE component instance across
1036
- * tabs and only swaps the `region` prop, so a shared ref explained one
1037
- * region's rotation with another region's account list — producing
1038
- * records for switches that never happened. State is keyed by region.
1039
- * 3. **The stop event is logged.** Rotation ending (`undefined`) used to
1040
- * return early AND leave the ref set, so "rotation stopped, back to your
1041
- * own account" never appeared, and rotating to the SAME account again
1042
- * was silently skipped.
1043
- */
1044
- const rotatedTo = pool?.rotatedToAccountId;
1045
- (0, react.useEffect)(() => {
1046
- const memory = rotationMemory.get(region) ?? {
1047
- last: void 0,
1048
- primed: false
1049
- };
1050
- const previous = memory.last;
1051
- if (!memory.primed) {
1052
- rotationMemory.set(region, {
1053
- last: rotatedTo,
1054
- primed: true
1055
- });
1056
- return;
1057
- }
1058
- if (rotatedTo === previous) return;
1059
- if (rotatedTo === void 0) {
1060
- rotationMemory.set(region, {
1061
- last: void 0,
1062
- primed: true
1063
- });
1064
- if (previous !== void 0) appendLog(t("row.poolLogRotationStopped", { from: nameOf(previous, pool) || t("row.accountUnnamed") }), "info");
1065
- return;
1066
- }
1067
- rotationMemory.set(region, {
1068
- last: rotatedTo,
1069
- primed: true
1070
- });
1071
- appendLog(t("row.poolLogRotated", {
1072
- from: previous === void 0 ? "—" : nameOf(previous, pool) || t("row.accountUnnamed"),
1073
- to: nameOf(rotatedTo, pool) || t("row.accountUnnamed")
1074
- }), "info");
1075
- }, [
1076
- appendLog,
1077
- pool,
1078
- region,
1079
- rotatedTo,
1080
- t
1081
- ]);
1082
- /**
1083
1047
  * The latest saved preferences, for callbacks that must not capture a stale
1084
1048
  * copy.
1085
1049
  *
@@ -1121,10 +1085,9 @@ window.__ModuleLoader__.load({
1121
1085
  const discard = (0, react.useCallback)(() => {
1122
1086
  setDraft(null);
1123
1087
  setDraftRegion(void 0);
1124
- setIntervalText(void 0);
1125
1088
  setSaveError(void 0);
1126
1089
  }, []);
1127
- /** Save the four preferences through the plugin's VERIFIED write path. */
1090
+ /** Save the pool preferences through the plugin's VERIFIED write path. */
1128
1091
  const save = (0, react.useCallback)(async () => {
1129
1092
  if (settingsScope === void 0 || !dirty || draft === null) return;
1130
1093
  setSaving(true);
@@ -1174,27 +1137,6 @@ window.__ModuleLoader__.load({
1174
1137
  memberAccountIds: next ? (pool?.accounts ?? []).map((account) => account.accountId) : []
1175
1138
  }));
1176
1139
  }, [editDraft, pool]);
1177
- /**
1178
- * The interval control's two halves, kept apart so the field is typeable.
1179
- *
1180
- * `onIntervalInput` records what the user typed and only folds a VALID value
1181
- * into the draft; `onIntervalCommit` (blur / Enter) forces the text back to
1182
- * the committed number, clamping an out-of-range or non-numeric entry exactly
1183
- * once, after the user is done. Committing on every keystroke is what made
1184
- * `120` land as `520`.
1185
- */
1186
- const onIntervalInput = (0, react.useCallback)((raw) => {
1187
- const { text, commit } = intervalEditOnInput(raw);
1188
- setIntervalText(text);
1189
- if (commit === void 0) return;
1190
- editDraft((current) => ({
1191
- ...current,
1192
- autoTestIntervalMinutes: commit
1193
- }));
1194
- }, [editDraft]);
1195
- const onIntervalCommit = (0, react.useCallback)(() => {
1196
- setIntervalText(void 0);
1197
- }, []);
1198
1140
  /** Run one batch action against the Host. */
1199
1141
  const runAction = (0, react.useCallback)(async (action) => {
1200
1142
  setBusy(action);
@@ -1207,44 +1149,83 @@ window.__ModuleLoader__.load({
1207
1149
  count: memberCount,
1208
1150
  model: pool?.targetModelId ?? ""
1209
1151
  }), "info");
1152
+ /**
1153
+ * One test row into the activity log.
1154
+ *
1155
+ * Shared by the STREAMED shape (a line per account) and the whole-batch
1156
+ * shape (an older Host's single document), so the two can never report the
1157
+ * same measurement differently — the failure mode this file's history is
1158
+ * mostly made of.
1159
+ */
1160
+ const logTestRow = (row) => {
1161
+ const outcome = row?.result?.outcome;
1162
+ if (outcome === void 0) {
1163
+ appendLog(t("row.poolLogTestRowMalformed", { accountName: row?.accountName === "" || row?.accountName === void 0 ? t("row.accountUnnamed") : row.accountName }), "warn");
1164
+ return;
1165
+ }
1166
+ appendLog(t("row.poolLogTestRow", {
1167
+ accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
1168
+ outcome: outcomeText(t, outcome)
1169
+ }), outcomeOk(outcome) ? "ok" : "warn");
1170
+ };
1210
1171
  try {
1211
1172
  const response = await fetch(withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, action), {
1212
1173
  method: "POST",
1213
1174
  headers: { accept: "application/json" },
1214
1175
  credentials: "same-origin"
1215
1176
  });
1216
- const body = await response.json().catch(() => void 0);
1217
- if (!response.ok) throw new Error(poolFailureText(t, {
1218
- status: response.status,
1219
- reason: body?.reason,
1220
- error: body?.error
1221
- }));
1222
- if (!mounted.current) return;
1223
- if (action === "checkin") {
1224
- for (const row of body?.rows ?? []) if (row.status === "claimed") appendLog(t("row.poolLogCheckinClaimed", {
1225
- accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
1226
- credit: String(row.credit ?? 0)
1227
- }), "ok");
1228
- else if (row.status === "already") appendLog(t("row.poolLogCheckinAlready", { accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName }), "info");
1229
- else appendLog(t("row.poolLogCheckinFailed", {
1230
- accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
1231
- message: row.message ?? t("row.requestFailed")
1232
- }), "error");
1233
- appendLog(t("row.poolLogCheckinDone"), "ok");
1234
- } else {
1235
- for (const row of body?.rows ?? []) {
1236
- const outcome = row?.result?.outcome;
1237
- if (outcome === void 0) {
1238
- appendLog(t("row.poolLogTestRowMalformed", { accountName: row?.accountName === "" || row?.accountName === void 0 ? t("row.accountUnnamed") : row.accountName }), "warn");
1239
- continue;
1177
+ let failed = false;
1178
+ if ((response.headers.get("content-type") ?? "").includes("ndjson")) {
1179
+ if (!response.ok) throw new Error(poolFailureText(t, { status: response.status }));
1180
+ let sawRow = false;
1181
+ await readNdjson(response.body, (line) => {
1182
+ const row = line["row"];
1183
+ if (row !== void 0) {
1184
+ sawRow = true;
1185
+ if (mounted.current) logTestRow(row);
1186
+ return;
1187
+ }
1188
+ const reason = line["reason"];
1189
+ if (typeof reason === "string") {
1190
+ failed = true;
1191
+ const message = poolFailureText(t, {
1192
+ status: 200,
1193
+ reason,
1194
+ error: typeof line["error"] === "string" ? line["error"] : void 0
1195
+ });
1196
+ if (mounted.current) {
1197
+ setActionError(message);
1198
+ appendLog(t("row.poolLogBatchFailed", { message }), "error");
1199
+ }
1240
1200
  }
1241
- appendLog(t("row.poolLogTestRow", {
1201
+ });
1202
+ if (!mounted.current) return;
1203
+ if (!failed && sawRow) appendLog(t("row.poolLogTestDone"), "ok");
1204
+ } else {
1205
+ const body = await response.json().catch(() => void 0);
1206
+ if (!response.ok) throw new Error(poolFailureText(t, {
1207
+ status: response.status,
1208
+ reason: body?.reason,
1209
+ error: body?.error
1210
+ }));
1211
+ if (!mounted.current) return;
1212
+ if (action === "checkin") {
1213
+ for (const row of body?.rows ?? []) if (row.status === "claimed") appendLog(t("row.poolLogCheckinClaimed", {
1214
+ accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
1215
+ credit: String(row.credit ?? 0)
1216
+ }), "ok");
1217
+ else if (row.status === "already") appendLog(t("row.poolLogCheckinAlready", { accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName }), "info");
1218
+ else appendLog(t("row.poolLogCheckinFailed", {
1242
1219
  accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
1243
- outcome: outcomeText(t, outcome)
1244
- }), outcomeOk(outcome) ? "ok" : "warn");
1220
+ message: row.message ?? t("row.requestFailed")
1221
+ }), "error");
1222
+ appendLog(t("row.poolLogCheckinDone"), "ok");
1223
+ } else {
1224
+ for (const row of body?.rows ?? []) logTestRow(row);
1225
+ appendLog(t("row.poolLogTestDone"), "ok");
1245
1226
  }
1246
- appendLog(t("row.poolLogTestDone"), "ok");
1247
1227
  }
1228
+ if (failed) return;
1248
1229
  onRefresh?.();
1249
1230
  } catch (error) {
1250
1231
  const message = error instanceof Error ? error.message : t("row.requestFailed");
@@ -1264,32 +1245,61 @@ window.__ModuleLoader__.load({
1264
1245
  ]);
1265
1246
  if (pool === void 0) return null;
1266
1247
  /**
1267
- * Where the target model came from.
1248
+ * Where the target model came from, and which model it is.
1268
1249
  *
1269
- * Rendered under the buttons. This was computed and then never used, so the
1270
- * card never explained its own target — which is the entire reason
1271
- * `targetModelSource` is sent by the Host.
1250
+ * Shown INSIDE the 「目标 test model」 setting row (as its description line)
1251
+ * rather than as a note under the buttons, which is where it used to be — one
1252
+ * of two places that stated the target model, the other being the header
1253
+ * subtitle. Both are gone from the header, so this is now the single place the
1254
+ * provenance is stated, right beside the control it describes.
1255
+ *
1256
+ * Returns null when there is nothing to say, so the caller can fall back to the
1257
+ * static explanation instead of rendering an empty "current:" prefix.
1272
1258
  */
1273
- const targetLabel = pool.targetModelSource === "preferred" ? t("row.poolTargetPreferred", { model: pool.targetModelId ?? "" }) : pool.targetModelSource === "free" ? t("row.poolTargetFree", { model: pool.targetModelId ?? "" }) : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : t("row.poolTargetNone");
1274
- return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("div", null, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-summary" }, t("row.poolSummary", {
1275
- count: effectiveMembers.length,
1276
- model: pool.targetModelId ?? (pool.targetModelSource === "stale" ? t("row.poolTargetStaleShort") : pool.targetModelSource === "none" ? t("row.poolTargetNoneShort") : t("row.poolTargetAuto"))
1277
- }))), rotationLocked && current !== void 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-badge" }, t("row.poolRotating")) : null), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-actions" }, (0, react.createElement)("button", {
1259
+ const targetProvenance = pool.targetModelSource === "preferred" ? t("row.poolTargetCurrentPreferred", { model: pool.targetModelId ?? "" }) : pool.targetModelSource === "free" ? t("row.poolTargetCurrentFree", { model: pool.targetModelId ?? "" }) : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : pool.targetModelSource === "none" ? t("row.poolTargetNone") : null;
1260
+ /**
1261
+ * The account the next request will start from.
1262
+ *
1263
+ * Read from the Host's `current` flag rather than re-derived here: the Host
1264
+ * owns the selection, and a browser-side guess is how the card would start
1265
+ * disagreeing with the account actually billed.
1266
+ */
1267
+ const currentAccount = pool.accounts.find((account) => account.current);
1268
+ const currentName = currentAccount === void 0 ? void 0 : currentAccount.accountName === "" ? t("row.accountUnnamed") : currentAccount.accountName;
1269
+ return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")), currentName === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-badge" }, t("row.poolCurrentHeader", { account: currentName }))), !active.enabled && onSelectAccount !== void 0 && pool.accounts.length > 0 ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolManualAccountLabel")), (0, react.createElement)("span", null, t("row.poolManualAccountHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
1270
+ className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
1271
+ value: currentAccount?.accountId ?? "",
1272
+ disabled: selectingAccount === true,
1273
+ onChange: (event) => {
1274
+ onSelectAccount(event.currentTarget.value);
1275
+ }
1276
+ }, pool.accounts.some((account) => account.current) ? null : (0, react.createElement)("option", {
1277
+ value: "",
1278
+ disabled: true
1279
+ }, t("row.poolManualAccountNone")), ...pool.accounts.map((account) => (0, react.createElement)("option", {
1280
+ key: account.accountId,
1281
+ value: account.accountId
1282
+ }, account.accountName === "" ? t("row.accountUnnamed") : account.accountName))))) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-actions" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-actions-group" }, pool.checkinSupported ? (0, react.createElement)("button", {
1278
1283
  type: "button",
1279
1284
  className: "dsm-btn dsm-btn-primary",
1280
- disabled: busy !== void 0 || !active.enabled || effectiveMembers.length === 0,
1285
+ disabled: busy !== void 0 || effectiveMembers.length === 0,
1281
1286
  onClick: () => {
1282
1287
  runAction("checkin");
1283
1288
  }
1284
- }, busy === "checkin" ? t("row.poolCheckingIn") : t("row.poolCheckinAll")), (0, react.createElement)("button", {
1289
+ }, busy === "checkin" ? t("row.poolCheckingIn") : t("row.poolCheckinAll")) : null, (0, react.createElement)("button", {
1285
1290
  type: "button",
1286
1291
  className: "dsm-btn dsm-btn-outline",
1287
- disabled: busy !== void 0 || !active.enabled || effectiveMembers.length === 0 || pool.targetModelSource === "none" || pool.targetModelSource === "stale",
1292
+ disabled: busy !== void 0 || effectiveMembers.length === 0 || pool.targetModelSource === "none" || pool.targetModelSource === "stale",
1288
1293
  title: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
1289
1294
  onClick: () => {
1290
1295
  runAction("test");
1291
1296
  }
1292
- }, busy === "test" ? t("row.poolTesting") : t("row.poolTestAll")), busy === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolBusyHint"))), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, targetLabel), !active.enabled ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, rotationLocked && usable.length === 0 ? (0, react.createElement)("p", {
1297
+ }, busy === "test" ? t("row.poolTesting") : t("row.poolTestAll")), busy === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolBusyHint"))), onRescan === void 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-actions-group" }, (0, react.createElement)("button", {
1298
+ type: "button",
1299
+ className: "dsm-btn dsm-btn-outline",
1300
+ disabled: rescanning === true,
1301
+ onClick: onRescan
1302
+ }, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !active.enabled ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, active.enabled && usable.length === 0 ? (0, react.createElement)("p", {
1293
1303
  className: "dsm-workbuddy-pool-warn",
1294
1304
  role: "status"
1295
1305
  }, `${t("row.poolNoCandidate")} ${t("row.poolNoCandidateHint")}`) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-head" }, (0, react.createElement)("div", null, (0, react.createElement)("h4", { className: "dsm-workbuddy-pool-members-title" }, t("row.poolMembersTitle")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolMembersHint"))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-members-actions" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolSelectedCount", {
@@ -1305,11 +1315,12 @@ window.__ModuleLoader__.load({
1305
1315
  className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
1306
1316
  disabled: effectiveMembers.length === 0 || !canEditPool,
1307
1317
  onClick: () => selectAll(false)
1308
- }, t("row.poolSelectNone")))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-table" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), (0, react.createElement)("span", null, t("row.poolColumnCheckin"))), ...pool.accounts.map((account) => renderAccountRow({
1318
+ }, t("row.poolSelectNone")))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-table" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), pool.checkinSupported ? (0, react.createElement)("span", null, t("row.poolColumnCheckin")) : null), ...pool.accounts.map((account) => renderAccountRow({
1309
1319
  t,
1310
1320
  account,
1311
1321
  checked: active.memberAccountIds.includes(account.accountId),
1312
1322
  canEdit: canEditPool,
1323
+ checkinSupported: pool.checkinSupported,
1313
1324
  onToggle: (next) => toggleMember(account.accountId, next)
1314
1325
  }))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
1315
1326
  className: "dsm-workbuddy-pool-warn",
@@ -1340,25 +1351,15 @@ window.__ModuleLoader__.load({
1340
1351
  dirty,
1341
1352
  saving,
1342
1353
  siblingBusy: siblingBusy === true,
1343
- rotationLocked,
1344
- rotationUnlockPending,
1345
1354
  saveError,
1346
1355
  settingsScope,
1347
- intervalText,
1348
- onIntervalInput,
1349
- onIntervalCommit,
1350
1356
  onEdit: editDraft,
1351
1357
  onSave: () => {
1352
1358
  save();
1353
1359
  },
1354
1360
  onDiscard: discard,
1355
- onUnlock: () => {
1356
- editDraft((current) => ({
1357
- ...current,
1358
- rotateByCredits: false
1359
- }));
1360
- },
1361
- appendLog
1361
+ appendLog,
1362
+ targetProvenance
1362
1363
  }), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolRegionNote")), renderLog({
1363
1364
  t,
1364
1365
  log,
@@ -1369,18 +1370,17 @@ window.__ModuleLoader__.load({
1369
1370
  }
1370
1371
  }));
1371
1372
  }
1372
- /** An account's display name by id, or undefined when it is not listed. */
1373
- function nameOf(accountId, pool) {
1374
- const account = pool?.accounts.find((entry) => entry.accountId === accountId);
1375
- return account === void 0 ? void 0 : account.accountName;
1376
- }
1377
1373
  /** One account's row. */
1378
1374
  function renderAccountRow(input) {
1379
- const { t, account, checked, canEdit, onToggle } = input;
1375
+ const { t, account, checked, canEdit, checkinSupported, onToggle } = input;
1380
1376
  const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
1381
1377
  const excluded = exclusionText(t, account);
1382
1378
  const probe = account.probe;
1383
- const probeLine = probe === void 0 ? t("row.poolNeverTested") : probe.retryAtMs !== void 0 ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryAt", { at: formatShort(probe.retryAtMs) })}` : probe.outcome === "rate-limited" || probe.outcome === "out-of-credit" ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}` : outcomeText(t, probe.outcome);
1379
+ const probeLine = [
1380
+ probe === void 0 ? t("row.poolNeverTested") : probe.retryAtMs !== void 0 ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryAt", { at: formatShort(probe.retryAtMs) })}` : probe.outcome === "rate-limited" || probe.outcome === "out-of-credit" ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}` : outcomeText(t, probe.outcome),
1381
+ probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now()),
1382
+ probe === void 0 ? void 0 : inlineProbeReason(probe.outcome, probe.message)
1383
+ ].filter((part) => part !== void 0 && part !== "").join(" · ");
1384
1384
  return (0, react.createElement)("div", {
1385
1385
  className: `dsm-workbuddy-pool-row${account.current ? " dsm-workbuddy-pool-row-current" : ""}`,
1386
1386
  key: account.accountId
@@ -1393,14 +1393,17 @@ window.__ModuleLoader__.load({
1393
1393
  })), (0, react.createElement)("span", {
1394
1394
  className: "dsm-workbuddy-usage-dot",
1395
1395
  style: { background: stateColor(account) }
1396
- }), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name), (0, react.createElement)("span", { className: checked ? "dsm-workbuddy-pool-member" : "dsm-workbuddy-pool-not-member" }, checked ? t("row.poolMember") : t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShort(probe.atMs) })) : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-excluded" }, excluded))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-credits" }, account.credits === void 0 ? (0, react.createElement)("span", null, "—") : (0, react.createElement)("b", null, formatNumber$1(account.credits)), account.expiringSoon !== void 0 && account.expiringSoon > 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon" }, t("row.poolCreditSoon", { count: formatNumber$1(account.expiringSoon) })) : account.nearestExpiryMs === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon-plain" }, t("row.poolCreditNearest", { at: formatShort(account.nearestExpiryMs) }))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe" }, probeLine), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
1396
+ }), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-account-name" }, (0, react.createElement)("b", null, name), account.current ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-tag" }, t("row.poolCurrentBadge")) : null, (0, react.createElement)("span", { className: checked ? "dsm-workbuddy-pool-member" : "dsm-workbuddy-pool-not-member" }, checked ? t("row.poolMember") : t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShort(probe.atMs) })) : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-excluded" }, excluded))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-credits" }, account.credits === void 0 ? (0, react.createElement)("span", null, "—") : (0, react.createElement)("b", null, formatNumber$1(account.credits)), account.expiringSoon !== void 0 && account.expiringSoon > 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon" }, t("row.poolCreditSoon", { count: formatNumber$1(account.expiringSoon) })) : account.nearestExpiryMs === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-soon-plain" }, t("row.poolCreditNearest", { at: formatShort(account.nearestExpiryMs) }))), (0, react.createElement)("span", {
1397
+ className: "dsm-workbuddy-pool-probe",
1398
+ ...probe?.message === void 0 ? {} : { title: probe.message }
1399
+ }, probeLine), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
1397
1400
  className: "dsm-workbuddy-pool-unknown",
1398
1401
  title: t("row.poolNeverTested")
1399
- }, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")));
1402
+ }, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null);
1400
1403
  }
1401
1404
  /** The preference editor plus its save/discard actions. */
1402
1405
  function renderSettings(input) {
1403
- const { t, pool, active, dirty, saving, siblingBusy, rotationLocked, rotationUnlockPending, saveError, settingsScope, intervalText, onEdit, onSave, onDiscard, onUnlock, onIntervalInput, onIntervalCommit } = input;
1406
+ const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance } = input;
1404
1407
  const canEdit = settingsScope !== void 0;
1405
1408
  const toggle = (label, hint, checked, onChange, disabled = false) => (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, label), (0, react.createElement)("span", null, hint)), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("label", { className: "sw" }, (0, react.createElement)("input", {
1406
1409
  type: "checkbox",
@@ -1411,28 +1414,10 @@ window.__ModuleLoader__.load({
1411
1414
  return (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings-wrap" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolSettingsTitle")), dirty ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-dirty" }, t("row.poolDirty")) : null), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolSettingsHint")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings" }, toggle(t("row.poolEnabled"), t("row.poolEnabledHint"), active.enabled, (next) => onEdit((current) => ({
1412
1415
  ...current,
1413
1416
  enabled: next
1414
- }))), toggle(t("row.poolRotate"), t("row.poolRotateHint"), active.rotateByCredits, (next) => onEdit((current) => ({
1415
- ...current,
1416
- rotateByCredits: next
1417
- })), !active.enabled), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolInterval")), (0, react.createElement)("span", null, t("row.poolIntervalHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("input", {
1418
- className: "dsm-workbuddy-pool-num",
1419
- type: "number",
1420
- min: 5,
1421
- max: POOL_INTERVAL_MAX,
1422
- step: 5,
1423
- value: intervalText ?? String(active.autoTestIntervalMinutes),
1424
- disabled: !active.enabled || !canEdit,
1425
- onChange: (event) => {
1426
- onIntervalInput(event.currentTarget.value);
1427
- },
1428
- onBlur: () => onIntervalCommit(),
1429
- onKeyDown: (event) => {
1430
- if (event.key === "Enter") onIntervalCommit();
1431
- }
1432
- }), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolIntervalUnit")))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolTargetLabel")), (0, react.createElement)("span", null, t("row.poolTargetHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
1417
+ }))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolTargetLabel")), (0, react.createElement)("span", null, targetProvenance === null ? null : `${targetProvenance} `, t("row.poolTargetHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
1433
1418
  className: "dsm-workbuddy-pool-select",
1434
1419
  value: active.targetModelId,
1435
- disabled: !active.enabled || !canEdit,
1420
+ disabled: !canEdit,
1436
1421
  onChange: (event) => {
1437
1422
  const value = event.currentTarget.value;
1438
1423
  onEdit((current) => ({
@@ -1446,25 +1431,28 @@ window.__ModuleLoader__.load({
1446
1431
  }, t("row.poolTargetStaleOption", { model: active.targetModelId })) : null, ...(pool.catalog ?? []).map((model) => (0, react.createElement)("option", {
1447
1432
  value: model.id,
1448
1433
  key: model.id
1449
- }, model.creditMultiplier === void 0 ? model.name : `${model.name} (x${model.creditMultiplier.toFixed(2)})`)))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-save-bar" }, (0, react.createElement)("button", {
1450
- type: "button",
1451
- className: "dsm-btn dsm-btn-primary",
1452
- disabled: !dirty || saving || siblingBusy || !canEdit,
1453
- onClick: onSave
1454
- }, saving ? t("row.poolSaving") : t("row.poolSaved")), (0, react.createElement)("button", {
1434
+ }, model.creditMultiplier === void 0 ? model.name : `${model.name} (x${model.creditMultiplier.toFixed(2)})`)))))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-save-bar" }, (0, react.createElement)("a", {
1435
+ className: "dsm-workbuddy-usage-cheer",
1436
+ href: WORKBUDDY_GITHUB_URL,
1437
+ target: "_blank",
1438
+ rel: "noopener noreferrer"
1439
+ }, t("row.cheer"), (0, react.createElement)("span", {
1440
+ className: "dsm-workbuddy-usage-cheer-star",
1441
+ "aria-hidden": "true"
1442
+ }, "★")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-save-buttons" }, (0, react.createElement)("button", {
1455
1443
  type: "button",
1456
1444
  className: "dsm-btn dsm-btn-outline",
1457
1445
  disabled: !dirty || saving || siblingBusy,
1458
1446
  onClick: onDiscard
1459
- }, t("row.poolDiscard")), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, dirty ? t("row.poolSaveDirty") : t("row.poolSaveIdle"))), saveError === void 0 ? null : (0, react.createElement)("p", {
1447
+ }, t("row.poolDiscard")), (0, react.createElement)("button", {
1448
+ type: "button",
1449
+ className: "dsm-btn dsm-btn-primary",
1450
+ disabled: !dirty || saving || siblingBusy || !canEdit,
1451
+ onClick: onSave
1452
+ }, saving ? t("row.poolSaving") : t("row.poolSaved")))), saveError === void 0 ? null : (0, react.createElement)("p", {
1460
1453
  className: "dsm-workbuddy-pool-error",
1461
1454
  role: "alert"
1462
- }, t("row.poolSaveFailed", { message: saveError })), rotationLocked ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-conflict" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-conflict-main" }, (0, react.createElement)("b", null, t("row.poolManualLocked")), (0, react.createElement)("span", null, t("row.poolManualLockedHint"))), rotationUnlockPending ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-pending" }, t("row.poolUnlockPending")) : null, (0, react.createElement)("button", {
1463
- type: "button",
1464
- className: "dsm-btn dsm-btn-outline",
1465
- disabled: !canEdit || rotationUnlockPending,
1466
- onClick: onUnlock
1467
- }, rotationUnlockPending ? t("row.poolUnlockSaved") : t("row.poolManualUnlock"))) : null);
1455
+ }, t("row.poolSaveFailed", { message: saveError })));
1468
1456
  }
1469
1457
  /** The activity log. */
1470
1458
  function renderLog(input) {
@@ -1594,14 +1582,12 @@ window.__ModuleLoader__.load({
1594
1582
  .dsm-workbuddy-tab-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
1595
1583
  .dsm-workbuddy-tab-switch input:disabled{opacity:.4;cursor:default}
1596
1584
  .dsm-workbuddy-tab-off{opacity:.55}
1585
+ /* Re-detection keeps a copy in the signed-out branch (that branch is where
1586
+ "I just signed in over there" applies), so it needs its own placement: the
1587
+ pool block's rule cannot reach it. */
1588
+ .dsm-workbuddy-usage-account-actions{display:flex;gap:8px;margin:10px 0 0}
1597
1589
  .dsm-workbuddy-tab-off-notice{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px;margin:0 0 4px;padding:8px 10px;background:var(--dsw-alias-bg-layer-3,#2a2c33);border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:8px}
1598
- .dsm-workbuddy-usage-account{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:14px;background:var(--dsw-alias-bg-layer-2,#24262c)}
1599
- .dsm-workbuddy-usage-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
1600
- .dsm-workbuddy-usage-expiry{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
1601
1590
  .dsm-workbuddy-usage-hint{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
1602
- .dsm-workbuddy-account-picker{display:flex;flex-direction:column;gap:8px}
1603
- .dsm-workbuddy-account-state{color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
1604
- .dsm-workbuddy-account-state:empty{display:none}
1605
1591
  .dsm-workbuddy-account-error{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:12px;line-height:18px;white-space:pre-line}
1606
1592
  .dsm-workbuddy-usage-select-wrap{position:relative}
1607
1593
  .dsm-workbuddy-usage-select{appearance:none;width:100%;font:inherit;padding:10px 34px 10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-3,#2a2c33);cursor:pointer;transition:border-color .15s,box-shadow .15s}
@@ -1622,58 +1608,17 @@ window.__ModuleLoader__.load({
1622
1608
  .dsm-workbuddy-searched-more:hover{text-decoration:underline}
1623
1609
  .dsm-workbuddy-usage-error{margin:0;font-size:14px;line-height:22px;color:var(--dsw-alias-state-error-primary,#ef4444)}
1624
1610
  .dsm-workbuddy-usage-dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto}
1625
- .dsm-workbuddy-usage-status{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6)}
1626
- .dsm-workbuddy-credits-panels{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(150px,.8fr);gap:10px}
1627
- .dsm-workbuddy-credit-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#24262c)}
1628
- .dsm-workbuddy-credit-panel-title{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
1629
- .dsm-workbuddy-credit-panel-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;line-height:21px}
1630
- .dsm-workbuddy-credit-panel-meta,.dsm-workbuddy-credit-panel-empty{color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
1631
- .dsm-workbuddy-credit-monthly-row{position:relative;display:flex;flex-direction:column;gap:3px;margin:-14px -14px 0;padding:11px 14px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:14px 14px 0 0;background:var(--dsw-alias-bg-layer-2,#24262c);overflow:hidden}
1632
- .dsm-workbuddy-credit-monthly-row::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:#9ca2aa}
1633
- .dsm-workbuddy-credit-monthly-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:14px;font-weight:600;line-height:20px}
1634
- .dsm-workbuddy-credit-monthly-meta{color:var(--dsw-alias-label-tertiary,#999);font-size:11.5px;line-height:17px;font-variant-numeric:tabular-nums}
1635
- .dsm-workbuddy-credit-packages{display:flex;flex-direction:column;gap:5px;margin:0;padding:0;list-style:none}
1636
- .dsm-workbuddy-credit-packages li{display:flex;align-items:baseline;justify-content:space-between;gap:10px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
1637
- .dsm-workbuddy-credit-packages li span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1638
- .dsm-workbuddy-credit-packages li span:last-child{flex:none;color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
1639
- .dsm-workbuddy-credit-soon{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-top:9px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
1640
- .dsm-workbuddy-credit-soon strong{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;font-variant-numeric:tabular-nums}
1641
- .dsm-workbuddy-credit-panel-total{position:relative;align-items:center;text-align:center;overflow:hidden}
1642
- .dsm-workbuddy-credit-panel-total::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:#4d9b6d}
1643
- .dsm-workbuddy-credit-total-body{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;width:100%}
1644
- .dsm-workbuddy-credit-total-value{color:#3f8d60;font-size:32px;line-height:36px;font-weight:700;letter-spacing:-.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
1645
- .dsm-workbuddy-checkin{display:flex;flex-direction:column;align-items:center;width:100%;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2,#36373b)}
1646
- .dsm-workbuddy-checkin-button{width:100%;padding:4px 10px;font-size:12px}
1647
- .dsm-workbuddy-checkin-error{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:11px;line-height:16px;text-align:center}
1648
- @media (max-width:760px){.dsm-workbuddy-credits-panels{grid-template-columns:1fr}.dsm-workbuddy-credit-panel-total{align-items:flex-start;text-align:left}.dsm-workbuddy-credit-total-body{align-items:flex-start}}
1649
1611
  .dsm-workbuddy-models{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:14px}
1650
1612
  .dsm-workbuddy-models-fold{display:flex;flex-direction:column;gap:10px}
1651
- /* The head doubles as the <details> summary. list-style:none plus the marker
1652
- rule removes the NATIVE triangle, because the chevron below is drawn instead;
1653
- without this the header would show two indicators in opposite corners. */
1654
- .dsm-workbuddy-models-head{display:flex;align-items:center;justify-content:space-between;gap:12px;cursor:pointer;list-style:none;user-select:none}
1655
- .dsm-workbuddy-models-head::-webkit-details-marker{display:none}
1656
- .dsm-workbuddy-models-head::marker{content:""}
1657
- .dsm-workbuddy-models-head:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:3px;border-radius:6px}
1613
+ /* A plain header now: the section does not fold, so it is neither clickable nor
1614
+ focusable, and the list-marker rules that hid the native <details> triangle are
1615
+ gone with it. */
1616
+ .dsm-workbuddy-models-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
1658
1617
  .dsm-workbuddy-models-title-row{display:flex;align-items:center;gap:6px;min-width:0}
1659
- /* The chevron points RIGHT when collapsed and rotates to DOWN when expanded.
1660
- The rotation keys off the details element's own open attribute, so it needs
1661
- no state and cannot disagree with what the browser actually shows. */
1662
- .dsm-workbuddy-models-chevron{display:inline-flex;align-items:center;justify-content:center;flex:none;width:14px;height:14px;color:var(--dsw-alias-label-tertiary,#999);transition:transform .16s}
1663
- /* The faint affordance hint, centred in the space between the section title and
1664
- the refresh button. Same faint token as the summary line under the title, so
1665
- it reads as a hint rather than as a second heading. flex:1 is what makes it
1666
- land in the actual middle instead of wherever space-between drops it. */
1667
- .dsm-workbuddy-models-fold-hint{flex:1 1 auto;min-width:0;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
1668
- /* Too narrow to hold title + hint + button on one line: the chevron already
1669
- marks the header as a disclosure, so the words are the part that can go. */
1670
- @media (max-width:760px){.dsm-workbuddy-models-fold-hint{display:none}}
1671
- .dsm-workbuddy-models-fold[open] .dsm-workbuddy-models-chevron{transform:rotate(90deg)}
1672
- .dsm-workbuddy-models-head:hover .dsm-workbuddy-models-chevron{color:var(--dsw-alias-label-secondary,#c6c9d0)}
1673
1618
  .dsm-workbuddy-models-title{margin:0;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:14px;font-weight:600;line-height:20px}
1674
- /* Unsaved-changes marker. It sits in the HEADER, outside the fold, because the
1675
- save/discard buttons are inside it: without this the user could collapse the
1676
- list and hide their own pending edits with nothing on screen saying so. */
1619
+ /* Unsaved-changes marker. It sits in the HEADER rather than beside the save
1620
+ buttons at the foot of the list, so a pending edit is visible without
1621
+ scrolling to the end of a long model list. */
1677
1622
  .dsm-workbuddy-models-dirty{margin-left:8px;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-state-warn-primary,#f59e0b);color:#1b1d22;font-size:11px;font-weight:600;line-height:16px;vertical-align:1px;white-space:nowrap}
1678
1623
  .dsm-workbuddy-models-summary{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
1679
1624
  .dsm-workbuddy-model-list{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:10px;overflow:hidden}
@@ -1717,11 +1662,23 @@ window.__ModuleLoader__.load({
1717
1662
 
1718
1663
  /* ---- Account pool ---- */
1719
1664
  .dsm-workbuddy-pool{display:flex;flex-direction:column;gap:12px;margin:0;padding:16px 0 4px;border-top:1px solid var(--dsw-alias-border-l2,#36373b)}
1720
- .dsm-workbuddy-pool-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
1665
+ .dsm-workbuddy-pool-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
1666
+ .dsm-workbuddy-pool-current-badge{padding:2px 10px;border-radius:999px;background:rgba(86,134,254,.16);color:var(--dsw-alias-brand-primary,#5686fe);font-size:12px;font-weight:600;line-height:18px;white-space:nowrap}
1667
+ /* Sits under the header block, before the batch buttons: account discovery is
1668
+ the first step of using the pool, so it reads above the actions it enables. */
1669
+ /* Manual-mode account picker: shares the settings row layout (.pool-set), and
1670
+ only the select itself needs a hint that it is a PICKER of accounts rather
1671
+ than of models — the two sit near each other and used to be indistinguishable
1672
+ to a selector query as well as to the eye. */
1673
+ .dsm-workbuddy-pool-account-select{min-width:200px}
1721
1674
  .dsm-workbuddy-pool-title{margin:0;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
1722
- .dsm-workbuddy-pool-summary{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
1723
1675
  .dsm-workbuddy-pool-badge{flex:none;padding:2px 9px;border-radius:999px;font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-brand-primary,#5686fe);background:rgba(86,134,254,.12);border:1px solid rgba(86,134,254,.32)}
1724
- .dsm-workbuddy-pool-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
1676
+ /* One row, two clusters: LEFT acts on the accounts you checked, RIGHT re-reads
1677
+ which accounts exist. Before this the rescan button was on its own row pushed
1678
+ right while the batch buttons were on the next row pushed left — two
1679
+ alignments two lines apart, with nothing explaining the split. */
1680
+ .dsm-workbuddy-pool-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:12px}
1681
+ .dsm-workbuddy-pool-actions-group{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
1725
1682
  .dsm-workbuddy-pool-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
1726
1683
  .dsm-workbuddy-pool-note{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#999)}
1727
1684
  .dsm-workbuddy-pool-warn{margin:0;padding:9px 11px;border-radius:9px;font-size:12px;line-height:17px;color:var(--dsw-alias-state-warn-primary,#f59e0b);background:rgba(245,158,11,.09);border:1px solid rgba(245,158,11,.3)}
@@ -1731,10 +1688,17 @@ window.__ModuleLoader__.load({
1731
1688
  .dsm-workbuddy-pool-row:first-child{border-top:0}
1732
1689
  .dsm-workbuddy-pool-row-head{padding:8px 14px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}
1733
1690
  .dsm-workbuddy-pool-row-current{background:rgba(86,134,254,.07)}
1691
+ /* The serving row, made findable at a glance: a left accent bar plus the tint.
1692
+ The bar is what distinguishes it in a long table, where a 7%-opacity wash
1693
+ alone reads as an alternating-row stripe. */
1694
+ .dsm-workbuddy-pool-row-current{box-shadow:inset 3px 0 0 var(--dsw-alias-brand-primary,#5686fe)}
1734
1695
  .dsm-workbuddy-pool-account{display:flex;align-items:center;gap:8px;min-width:0}
1735
1696
  .dsm-workbuddy-pool-account-name{display:flex;flex-direction:column;gap:1px;min-width:0}
1736
1697
  .dsm-workbuddy-pool-account-name b{font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1737
1698
  .dsm-workbuddy-pool-account-name span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#999)}
1699
+ /* Declared AFTER the blanket span rule so it wins: the badge must not inherit
1700
+ the muted 11px tertiary treatment the membership line uses. */
1701
+ .dsm-workbuddy-pool-account-name .dsm-workbuddy-pool-current-tag{align-self:flex-start;margin-top:2px;padding:1px 6px;border-radius:999px;background:rgba(86,134,254,.16);color:var(--dsw-alias-brand-primary,#5686fe);font-size:11px;font-weight:600;line-height:16px;white-space:nowrap}
1738
1702
  .dsm-workbuddy-pool-excluded{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
1739
1703
  .dsm-workbuddy-pool-credits{display:flex;flex-direction:column;gap:1px;min-width:0;font-variant-numeric:tabular-nums}
1740
1704
  .dsm-workbuddy-pool-credits b{color:var(--dsw-alias-label-primary,#e6e6e6);font-weight:600}
@@ -1765,7 +1729,8 @@ window.__ModuleLoader__.load({
1765
1729
  .dsm-workbuddy-pool-num{width:74px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:13px;text-align:center;font-variant-numeric:tabular-nums}
1766
1730
  .dsm-workbuddy-pool-select{max-width:260px;padding:6px 10px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-primary,#e6e6e6);font:inherit;font-size:13px;cursor:pointer}
1767
1731
  .dsm-workbuddy-pool-select:disabled,.dsm-workbuddy-pool-num:disabled{opacity:.5;cursor:default}
1768
- .dsm-workbuddy-pool-save-bar{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
1732
+ .dsm-workbuddy-pool-save-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:12px}
1733
+ .dsm-workbuddy-pool-save-buttons{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex:none}
1769
1734
  .dsm-workbuddy-pool-conflict{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:11px 13px;border-radius:10px;background:rgba(245,158,11,.08);border:1px solid var(--dsw-alias-state-warn-primary,#f59e0b)}
1770
1735
  .dsm-workbuddy-pool-pending{flex:none;align-self:center;font-size:12px;line-height:17px;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
1771
1736
  .dsm-workbuddy-pool-conflict-main{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
@@ -1942,7 +1907,6 @@ window.__ModuleLoader__.load({
1942
1907
  * @module dsh-connect-workbuddy/client/WorkBuddyCard
1943
1908
  */
1944
1909
  const POLL_INTERVAL_MS = 6e4;
1945
- const WORKBUDDY_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-workbuddy";
1946
1910
  /**
1947
1911
  * Tooltip for one model's image checkbox. The box is pre-checked only for
1948
1912
  * models the vendored vendor table documents as natively multimodal, so the
@@ -2036,16 +2000,6 @@ window.__ModuleLoader__.load({
2036
2000
  maximumFractionDigits: 2
2037
2001
  }).format(value);
2038
2002
  }
2039
- function formatDateTime(value) {
2040
- return new Intl.DateTimeFormat(void 0, {
2041
- year: "numeric",
2042
- month: "2-digit",
2043
- day: "2-digit",
2044
- hour: "2-digit",
2045
- minute: "2-digit",
2046
- hourCycle: "h23"
2047
- }).format(new Date(value));
2048
- }
2049
2003
  /**
2050
2004
  * Compact package-date rendering with time, e.g. `08/25 14:44`.
2051
2005
  *
@@ -2085,7 +2039,20 @@ window.__ModuleLoader__.load({
2085
2039
  * would look like an upstream answer while being a guess, and the user would
2086
2040
  * wait for a moment that means nothing.
2087
2041
  */
2088
- function probeResultView(result, t) {
2042
+ /**
2043
+ * ` · 约 1 小时后` for a future instant, or `''` when there is none.
2044
+ *
2045
+ * Kept beside {@link probeResultView} rather than inlined three times so the
2046
+ * "append only when non-empty" rule cannot drift between the two limited
2047
+ * outcomes. The absolute instant stays the primary text: it is the upstream's
2048
+ * own answer, and the relative form is a convenience derived from it.
2049
+ */
2050
+ function retrySuffix(t, untilMs, nowMs) {
2051
+ if (untilMs === void 0) return "";
2052
+ const remaining = remainingText(t, untilMs, nowMs);
2053
+ return remaining === "" ? "" : ` · ${remaining}`;
2054
+ }
2055
+ function probeResultView(result, t, nowMs = Date.now()) {
2089
2056
  switch (result.outcome) {
2090
2057
  case "ok": {
2091
2058
  const elapsed = result.elapsedMs === void 0 ? void 0 : `${String(result.elapsedMs / 1e3)}s`;
@@ -2095,11 +2062,11 @@ window.__ModuleLoader__.load({
2095
2062
  };
2096
2063
  }
2097
2064
  case "rate-limited": return {
2098
- text: result.retryAtMs === void 0 ? t("row.probeRateLimitedUnknown") : t("row.probeRateLimitedAt", { at: formatDate(result.retryAtMs) }),
2065
+ text: result.retryAtMs === void 0 ? t("row.probeRateLimitedUnknown") : t("row.probeRateLimitedAt", { at: formatDate(result.retryAtMs) }) + retrySuffix(t, result.retryAtMs, nowMs),
2099
2066
  tone: "warn"
2100
2067
  };
2101
2068
  case "out-of-credit": return {
2102
- text: result.retryAtMs === void 0 ? t("row.probeOutOfCreditUnknown") : t("row.probeOutOfCreditAt", { at: formatDate(result.retryAtMs) }),
2069
+ text: result.retryAtMs === void 0 ? t("row.probeOutOfCreditUnknown") : t("row.probeOutOfCreditAt", { at: formatDate(result.retryAtMs) }) + retrySuffix(t, result.retryAtMs, nowMs),
2103
2070
  tone: "bad"
2104
2071
  };
2105
2072
  case "credential-rejected": return {
@@ -2127,16 +2094,6 @@ window.__ModuleLoader__.load({
2127
2094
  /** The region whose tab is on screen; each tab is its own provider stack. */
2128
2095
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
2129
2096
  /**
2130
- * Whether the model list is unfolded.
2131
- *
2132
- * Mirrored in state purely to supply the INITIAL value: a bare `<details>` is
2133
- * COLLAPSED by default, and the list is what users already see, so unfolding
2134
- * has to be the starting point. Deliberately NOT persisted — only the active
2135
- * region is mounted, so remembering it would need a settings field for a
2136
- * cosmetic preference; it resets when the card is closed or the tab changes.
2137
- */
2138
- const [modelsOpen, setModelsOpen] = (0, react.useState)(true);
2139
- /**
2140
2097
  * Last-known usage per region, so tab dots survive tab switches.
2141
2098
  *
2142
2099
  * The placeholder carries `selectionExplicit: false` because that is the
@@ -2166,15 +2123,6 @@ window.__ModuleLoader__.load({
2166
2123
  const [saveError, setSaveError] = (0, react.useState)(void 0);
2167
2124
  const [switchingAccount, setSwitchingAccount] = (0, react.useState)(false);
2168
2125
  /**
2169
- * Whether the account pool's rotation currently owns this region's choice.
2170
- *
2171
- * Reported up by the pool section. While true, the manual account dropdown is
2172
- * disabled: both would write the same slot, and letting them disagree is how
2173
- * the card ends up showing one account while another is billed. The pool
2174
- * shows the reason and an inline way to switch rotation off.
2175
- */
2176
- const [poolRotationLocked, setPoolRotationLocked] = (0, react.useState)(false);
2177
- /**
2178
2126
  * Whether the account pool's save is in flight. Both sections write into one
2179
2127
  * region slot and the Host merges per-region, so two concurrent saves can
2180
2128
  * interleave on a stale base and revert each other while both report success
@@ -2183,8 +2131,6 @@ window.__ModuleLoader__.load({
2183
2131
  const [poolBusy, setPoolBusy] = (0, react.useState)(false);
2184
2132
  /** A refused account write (silently unpersisted settings on a locked file). */
2185
2133
  const [accountError, setAccountError] = (0, react.useState)(void 0);
2186
- const [checkingIn, setCheckingIn] = (0, react.useState)(false);
2187
- const [checkinActionError, setCheckinActionError] = (0, react.useState)(void 0);
2188
2134
  /** Region whose on/off checkbox write is in flight, so its box can't race. */
2189
2135
  const [togglingRegion, setTogglingRegion] = (0, react.useState)(void 0);
2190
2136
  /**
@@ -2405,28 +2351,6 @@ window.__ModuleLoader__.load({
2405
2351
  if (mounted.current) setTogglingRegion(void 0);
2406
2352
  }
2407
2353
  };
2408
- /**
2409
- * Claim the daily check-in reward for the active region. The action endpoint
2410
- * is region-scoped; the response only refreshes this tab's check-in state.
2411
- */
2412
- const claimDailyCheckin = async () => {
2413
- setCheckingIn(true);
2414
- setCheckinActionError(void 0);
2415
- try {
2416
- const response = await fetch(withWorkBuddyRegion(WORKBUDDY_CHECKIN_PATH, activeRegion), {
2417
- method: "POST",
2418
- headers: { accept: "application/json" },
2419
- credentials: "same-origin"
2420
- });
2421
- const body = await response.json().catch(() => void 0);
2422
- if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
2423
- await refreshUsage(activeRegion);
2424
- } catch (error) {
2425
- if (mounted.current) setCheckinActionError(error instanceof Error ? error.message : t("row.requestFailed"));
2426
- } finally {
2427
- if (mounted.current) setCheckingIn(false);
2428
- }
2429
- };
2430
2354
  const refreshModels = async () => {
2431
2355
  setBusy(true);
2432
2356
  try {
@@ -2623,7 +2547,6 @@ window.__ModuleLoader__.load({
2623
2547
  * placeholder says the honest thing instead.
2624
2548
  */
2625
2549
  const nameOf = (value) => value === "" ? t("row.accountUnnamed") : value;
2626
- const label = status.status === "signed-in" ? t("row.signedIn", { accountName: nameOf(status.accountName) }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
2627
2550
  /** The saved choice no longer matches a local sign-in (tokens are fine). */
2628
2551
  const selectionLost = status.status === "signed-out" && status.selectionLost === true;
2629
2552
  /**
@@ -2643,11 +2566,7 @@ window.__ModuleLoader__.load({
2643
2566
  message: status.status === "signed-out" ? status.message : void 0,
2644
2567
  searched
2645
2568
  });
2646
- /**
2647
- * Whether this region runs a saved choice, per the Host. `undefined` only on
2648
- * the error branch, which renders no picker and therefore no state line.
2649
- */
2650
- const selectionExplicit = status.status === "error" ? void 0 : status.selectionExplicit;
2569
+ status.status === "error" || status.selectionExplicit;
2651
2570
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2652
2571
  className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
2653
2572
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -2732,151 +2651,15 @@ window.__ModuleLoader__.load({
2732
2651
  className: "dsm-workbuddy-tab-off-notice",
2733
2652
  children: t("row.tabOffNotice")
2734
2653
  }) : null,
2735
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2736
- className: "dsm-workbuddy-usage-account",
2737
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2738
- className: "dsm-workbuddy-usage-account-copy",
2739
- role: "status",
2740
- children: [
2741
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2742
- className: "dsm-workbuddy-usage-status",
2743
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2744
- "aria-hidden": "true",
2745
- className: "dsm-workbuddy-usage-dot",
2746
- style: dotStyle(status.status)
2747
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
2748
- }),
2749
- status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2750
- className: "dsm-workbuddy-usage-expiry",
2751
- children: t("row.tokenExpiry", { expiresAt: formatDateTime(status.tokenExpiresAtMs) })
2752
- }) : null,
2753
- status.status === "error" || status.status === "signed-in" && status.creditsError !== void 0 ? status.status === "signed-in" && status.credentialRejected === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2754
- className: "dsm-workbuddy-usage-hint",
2755
- children: t("row.reloginHint")
2756
- }) : null,
2757
- selectionLost ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2758
- className: "dsm-workbuddy-usage-hint",
2759
- children: t("row.selectionLostHint")
2760
- }) : null
2761
- ]
2762
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2763
- type: "button",
2764
- className: "dsm-btn dsm-btn-outline",
2765
- disabled: busy,
2766
- onClick: () => {
2767
- rescanAccounts();
2768
- },
2769
- children: busy ? t("row.accountsScanning") : t("row.accountsRescan")
2770
- })]
2771
- }),
2772
- status.status !== "error" && status.accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2773
- className: "dsm-workbuddy-account-picker",
2774
- "aria-label": t("row.accountsTitle"),
2775
- children: [
2776
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2777
- className: "dsm-workbuddy-usage-select-wrap",
2778
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2779
- className: "dsm-workbuddy-usage-select",
2780
- value: status.accounts.find((account) => account.selected)?.id ?? "",
2781
- disabled: switchingAccount || !canWrite || poolRotationLocked,
2782
- onChange: (event) => {
2783
- switchAccount(event.currentTarget.value);
2784
- },
2785
- children: [status.accounts.some((account) => account.selected) ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2786
- value: "",
2787
- disabled: true,
2788
- children: t("row.accountNoneInEffect")
2789
- }), status.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2790
- value: account.id,
2791
- children: [nameOf(account.accountName), account.domain === "" ? "" : ` · ${account.domain}`]
2792
- }, account.id))]
2793
- })
2794
- }),
2795
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2796
- className: "dsm-workbuddy-account-state",
2797
- role: "status",
2798
- children: selectionExplicit === true ? t("row.accountsSavedChoice") : ""
2799
- }),
2800
- accountError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2801
- className: "dsm-workbuddy-account-error",
2802
- role: "alert",
2803
- children: t("row.accountsWriteFailed", { message: accountError })
2804
- })
2805
- ]
2654
+ status.status === "error" || status.status === "signed-in" && status.creditsError !== void 0 ? status.status === "signed-in" && status.credentialRejected === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2655
+ className: "dsm-workbuddy-usage-hint",
2656
+ children: t("row.reloginHint")
2657
+ }) : null,
2658
+ selectionLost ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2659
+ className: "dsm-workbuddy-usage-hint",
2660
+ children: t("row.selectionLostHint")
2806
2661
  }) : null,
2807
2662
  status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2808
- status.credits === void 0 ? null : (() => {
2809
- const monthly = [...status.credits.packages].filter((pack) => pack.monthly).sort((left, right) => right.remain - left.remain);
2810
- const SOON_MS = 2592e5;
2811
- const now = Date.now();
2812
- const expiring = [...status.credits.packages].filter((pack) => !pack.monthly && pack.remain > 0 && (pack.expiresAtMs ?? Number.MAX_SAFE_INTEGER) - now <= SOON_MS).sort((left, right) => (left.expiresAtMs ?? Number.MAX_SAFE_INTEGER) - (right.expiresAtMs ?? Number.MAX_SAFE_INTEGER));
2813
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2814
- className: "dsm-workbuddy-credits-panels",
2815
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2816
- className: "dsm-workbuddy-credit-panel dsm-workbuddy-credit-panel-activities",
2817
- children: [
2818
- monthly.map((pack, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2819
- className: "dsm-workbuddy-credit-monthly-row",
2820
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2821
- className: "dsm-workbuddy-credit-monthly-name",
2822
- children: pack.packageName
2823
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2824
- className: "dsm-workbuddy-credit-monthly-meta",
2825
- children: t("row.creditsMonthlyRemain", {
2826
- remain: formatNumber(pack.remain),
2827
- size: formatNumber(pack.size),
2828
- at: pack.cycleRefreshMs === void 0 ? "" : formatDate(pack.cycleRefreshMs)
2829
- })
2830
- })]
2831
- }, `monthly-${pack.packageName}-${String(index)}`)),
2832
- expiring.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2833
- className: "dsm-workbuddy-credit-panel-empty",
2834
- children: t("row.creditsNoSoon")
2835
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2836
- className: "dsm-workbuddy-credit-packages",
2837
- children: expiring.map((pack, index) => {
2838
- const at = pack.expiresAtMs;
2839
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: pack.packageName }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [formatNumber(pack.remain), at === void 0 ? "" : ` · ${formatDate(at)}`] })] }, `${pack.packageName}-${String(index)}`);
2840
- })
2841
- }),
2842
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2843
- className: "dsm-workbuddy-credit-soon",
2844
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.creditsExpiringSoon") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatNumber(status.credits.expiringSoon) })]
2845
- })
2846
- ]
2847
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2848
- className: "dsm-workbuddy-credit-panel dsm-workbuddy-credit-panel-total",
2849
- children: [
2850
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2851
- className: "dsm-workbuddy-credit-total-body",
2852
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2853
- className: "dsm-workbuddy-credit-panel-title",
2854
- children: t("row.creditsTotalLabel")
2855
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
2856
- className: "dsm-workbuddy-credit-total-value",
2857
- children: formatNumber(status.credits.total)
2858
- })]
2859
- }),
2860
- status.checkin === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2861
- className: "dsm-workbuddy-checkin",
2862
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2863
- type: "button",
2864
- className: "dsm-btn dsm-btn-primary dsm-workbuddy-checkin-button",
2865
- disabled: !status.checkin.active || status.checkin.todayCheckedIn || checkingIn,
2866
- onClick: () => {
2867
- claimDailyCheckin();
2868
- },
2869
- children: checkingIn ? t("row.checkinClaiming") : status.checkin.todayCheckedIn ? t("row.checkinClaimed") : status.checkin.claimButtonText ?? t("row.checkinClaim")
2870
- })
2871
- }),
2872
- status.checkinError === void 0 && checkinActionError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2873
- className: "dsm-workbuddy-checkin-error",
2874
- children: t("row.checkinError", { message: checkinActionError ?? status.checkinError ?? "" })
2875
- })
2876
- ]
2877
- })]
2878
- });
2879
- })(),
2880
2663
  status.credentialRejected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2881
2664
  className: "dsm-workbuddy-usage-error",
2882
2665
  role: "alert",
@@ -2905,71 +2688,58 @@ window.__ModuleLoader__.load({
2905
2688
  role: "alert",
2906
2689
  children: t("row.requestFailedHint", { message: status.refreshError })
2907
2690
  }),
2691
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPool, {
2692
+ t,
2693
+ region: activeRegion,
2694
+ ...status.pool === void 0 ? {} : { pool: status.pool },
2695
+ ...settingsScope === void 0 ? {} : { settingsScope },
2696
+ onRescan: () => {
2697
+ rescanAccounts();
2698
+ },
2699
+ rescanning: busy,
2700
+ onSelectAccount: (accountId) => {
2701
+ switchAccount(accountId);
2702
+ },
2703
+ selectingAccount: switchingAccount,
2704
+ siblingBusy: saving || togglingRegion !== void 0,
2705
+ onBusyChange: setPoolBusy,
2706
+ onSaved: async () => await refreshUsage(activeRegion) !== void 0,
2707
+ onRefresh: () => {
2708
+ refreshUsage(activeRegion);
2709
+ }
2710
+ }),
2908
2711
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
2909
2712
  className: "dsm-workbuddy-models",
2910
2713
  "aria-label": t("row.modelsTitle"),
2911
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
2714
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2912
2715
  className: "dsm-workbuddy-models-fold",
2913
- open: modelsOpen,
2914
- onToggle: (event) => {
2915
- setModelsOpen(event.currentTarget.open);
2916
- },
2917
2716
  children: [
2918
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", {
2717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2919
2718
  className: "dsm-workbuddy-models-head",
2920
- title: t("row.modelsFoldHint"),
2921
- onClick: (event) => {
2922
- if (event.target instanceof Element && event.target.closest(".dsm-workbuddy-models-head-actions") !== null) event.preventDefault();
2923
- },
2924
- children: [
2925
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2926
- className: "dsm-workbuddy-models-title-row",
2927
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2928
- className: "dsm-workbuddy-models-chevron",
2929
- "aria-hidden": "true",
2930
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2931
- viewBox: "0 0 12 12",
2932
- width: "12",
2933
- height: "12",
2934
- focusable: "false",
2935
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2936
- d: "M4.5 2.5 L8 6 L4.5 9.5",
2937
- fill: "none",
2938
- stroke: "currentColor",
2939
- strokeWidth: "1.6",
2940
- strokeLinecap: "round",
2941
- strokeLinejoin: "round"
2942
- })
2943
- })
2944
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
2945
- className: "dsm-workbuddy-models-title",
2946
- children: [t("row.modelsTitle"), dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2947
- className: "dsm-workbuddy-models-dirty",
2948
- children: t("row.modelsDirty")
2949
- }) : null]
2950
- })]
2951
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2952
- className: "dsm-workbuddy-models-summary",
2953
- children: t("row.modelsSummary", { count: activeEnabledIds.size })
2954
- })] }),
2955
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2956
- className: "dsm-workbuddy-models-fold-hint",
2957
- "aria-hidden": "true",
2958
- children: modelsOpen ? t("row.modelsFoldHide") : t("row.modelsFoldShow")
2959
- }),
2960
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2961
- className: "dsm-workbuddy-models-head-actions",
2962
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2963
- type: "button",
2964
- className: "dsm-btn dsm-btn-outline",
2965
- disabled: busy,
2966
- onClick: () => {
2967
- refreshModels();
2968
- },
2969
- children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
2970
- })
2719
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2720
+ className: "dsm-workbuddy-models-title-row",
2721
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
2722
+ className: "dsm-workbuddy-models-title",
2723
+ children: [t("row.modelsTitle"), dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2724
+ className: "dsm-workbuddy-models-dirty",
2725
+ children: t("row.modelsDirty")
2726
+ }) : null]
2971
2727
  })
2972
- ]
2728
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2729
+ className: "dsm-workbuddy-models-summary",
2730
+ children: t("row.modelsSummary", { count: activeEnabledIds.size })
2731
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2732
+ className: "dsm-workbuddy-models-head-actions",
2733
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2734
+ type: "button",
2735
+ className: "dsm-btn dsm-btn-outline",
2736
+ disabled: busy,
2737
+ onClick: () => {
2738
+ refreshModels();
2739
+ },
2740
+ children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
2741
+ })
2742
+ })]
2973
2743
  }),
2974
2744
  probeError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2975
2745
  className: "dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-bad",
@@ -3071,10 +2841,12 @@ window.__ModuleLoader__.load({
3071
2841
  const result = probes[activeRegion]?.[model.id];
3072
2842
  if (result === void 0) return null;
3073
2843
  const view = probeResultView(result, t);
2844
+ const reason = inlineProbeReason(result.outcome, result.message);
3074
2845
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3075
2846
  className: `dsm-workbuddy-model-probe-result dsm-workbuddy-model-probe-result-${view.tone}`,
3076
2847
  role: "status",
3077
- children: [view.text, result.message === void 0 ? null : " · " + result.message]
2848
+ ...result.message === void 0 ? {} : { title: result.message },
2849
+ children: [view.text, reason === void 0 ? null : " · " + reason]
3078
2850
  });
3079
2851
  })()
3080
2852
  ]
@@ -3124,28 +2896,30 @@ window.__ModuleLoader__.load({
3124
2896
  })
3125
2897
  ]
3126
2898
  })
3127
- }),
3128
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPool, {
3129
- t,
3130
- region: activeRegion,
3131
- ...status.pool === void 0 ? {} : { pool: status.pool },
3132
- ...settingsScope === void 0 ? {} : { settingsScope },
3133
- siblingBusy: saving || togglingRegion !== void 0,
3134
- onBusyChange: setPoolBusy,
3135
- onSaved: async () => await refreshUsage(activeRegion) !== void 0,
3136
- onRefresh: () => {
3137
- refreshUsage(activeRegion);
3138
- },
3139
- onRotationChange: setPoolRotationLocked
3140
2899
  })
3141
2900
  ] }) : null,
3142
- status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3143
- className: "dsm-workbuddy-usage-text",
3144
- children: signedOutText(notice, t)
3145
- }), searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchedPaths, {
3146
- items: searched,
3147
- t
3148
- }) : null] }) : null,
2901
+ status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2902
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2903
+ className: "dsm-workbuddy-usage-text",
2904
+ children: signedOutText(notice, t)
2905
+ }),
2906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2907
+ className: "dsm-workbuddy-usage-account-actions",
2908
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2909
+ type: "button",
2910
+ className: "dsm-btn dsm-btn-outline",
2911
+ disabled: busy,
2912
+ onClick: () => {
2913
+ rescanAccounts();
2914
+ },
2915
+ children: busy ? t("row.accountsScanning") : t("row.accountsRescan")
2916
+ })
2917
+ }),
2918
+ searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchedPaths, {
2919
+ items: searched,
2920
+ t
2921
+ }) : null
2922
+ ] }) : null,
3149
2923
  status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3150
2924
  className: "dsm-workbuddy-usage-error",
3151
2925
  children: status.message
@@ -3179,7 +2953,6 @@ window.__ModuleLoader__.load({
3179
2953
  "row.tabSwitchHint": "Uncheck to switch this provider off. Its models disappear from the DSH model picker; your account, credits, and model settings are kept and return when you check it again.",
3180
2954
  "row.tabSwitchAria": "Offer the {region} provider to DSH",
3181
2955
  "row.tabOffNotice": "This provider is switched off: none of its models are offered to DSH. Your account, credits, and model settings are kept — check the box above the tab to bring it back.",
3182
- "row.signedOut": "Not signed in",
3183
2956
  "row.signedOutHint": "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
3184
2957
  "row.signedOutWrongRegion": "Your WorkBuddy sign-in was found, but it belongs to the other region tab. Switch tabs above to use it — there is nothing to sign in again.",
3185
2958
  "row.signedOutEncrypted": "Your WorkBuddy sign-in is present, but its token fields are encrypted and the plugin could not read the key. Install the WorkBuddy desktop app, or set WORKBUDDY_APP_EXECUTABLE to its executable — signing in again will not change this.",
@@ -3193,8 +2966,6 @@ window.__ModuleLoader__.load({
3193
2966
  "row.sourceDesktop": "desktop app",
3194
2967
  "row.sourceDsh": "plugin copy",
3195
2968
  "row.searchedMore": "Show the {count} path(s) that were not found",
3196
- "row.signedIn": "Signed in: {accountName}",
3197
- "row.tokenExpiry": "Access token expires {expiresAt} (auto-renewal)",
3198
2969
  "row.reloginHint": "If errors occur, sign back in to the WorkBuddy app.",
3199
2970
  "row.credentialRejectedTitle": "WorkBuddy refused this account's token",
3200
2971
  "row.credentialRejectedIntro": "The token for \"{accountName}\" is no longer accepted by the upstream, so credits and check-in are unavailable. Signing in again only helps if the problem is the token itself — here is what actually applies:",
@@ -3207,38 +2978,24 @@ window.__ModuleLoader__.load({
3207
2978
  "row.poolErrUnavailable": "The account pool is not available in this build (or its batch route is missing). Restart the app; if it persists, the plugin needs reinstalling.",
3208
2979
  "row.poolErrFailed": "The batch failed on the host side: {message}",
3209
2980
  "row.poolErrHttp": "The batch was refused (HTTP {status}). Check the plugin is up to date and try again.",
3210
- "row.poolErrDisabled": "The account pool is switched off for this region. Turn it on and save before running a batch.",
3211
2981
  "row.poolErrNoMembers": "No account is checked into this pool — the batch had nothing to run. Check at least one account and save.",
3212
2982
  "row.poolErrNoLiveMembers": "Every account checked into this pool has lost its local sign-in, so the batch had nothing to run. Sign in again in the WorkBuddy desktop app, or save to drop them.",
3213
2983
  "row.poolErrNoFreeModel": "This region has no zero-multiplier model to test with. Refresh the model catalog, or pick a target model yourself.",
2984
+ "row.poolErrNoCheckin": "This region has no daily check-in — the action is not offered here.",
3214
2985
  "row.poolErrStaleModel": "The target model is no longer offered by this region. Pick another, or switch back to automatic.",
3215
- "row.creditsTotalLabel": "Total remaining credits",
3216
- "row.creditsMonthlyRemain": "Remaining {remain} / {size} · Refreshes {at}",
3217
- "row.checkinClaim": "Check in",
3218
- "row.checkinClaiming": "Checking in…",
3219
- "row.checkinClaimed": "Checked in",
3220
- "row.checkinError": "Check-in unavailable: {message}",
3221
2986
  "row.creditsError": "Credit query unavailable: {message}",
3222
- "row.creditsNoSoon": "No packages expire within 3 days.",
3223
- "row.creditsExpiringSoon": "Expiring in 3 days",
3224
2987
  "row.refresh": "Refresh",
3225
2988
  "row.refreshing": "Refreshing…",
3226
- "row.accountsTitle": "WorkBuddy account",
3227
2989
  "row.accountUnnamed": "Unnamed account",
3228
2990
  "row.accountsHint": "Choose from locally detected sign-ins. Tokens are never shown or saved in DSH settings.",
3229
2991
  "row.accountsRescan": "Detect accounts again",
3230
2992
  "row.accountsScanning": "Detecting…",
3231
- "row.accountNoneInEffect": "No account in effect",
3232
- "row.accountsSavedChoice": "Using the account saved for this region.",
3233
2993
  "row.accountsWriteFailed": "The account change was not saved: {message} — the active profile's configuration file could not be updated (on Windows it may be locked by an antivirus scanner or a sync client). Your previous choice is still in effect; close whatever holds that file and try again (see docs/WINDOWS.md in the plugin repo for the exact path).",
3234
2994
  "row.selectionLostHint": "The saved account no longer exists locally.",
3235
2995
  "row.selectionLostMessage": "The account saved for this region is no longer among the local sign-ins (WorkBuddy replaced its login or cleaned up backups). Your other sign-ins are fine — pick a current one from the list above. Signing in again in the desktop app will not fix this.",
3236
2996
  "row.modelsTitle": "Models",
3237
2997
  "row.modelsSummary": "{count} enabled",
3238
2998
  "row.modelsDirty": "unsaved changes",
3239
- "row.modelsFoldHint": "Expand or collapse the model list",
3240
- "row.modelsFoldShow": "Click to expand",
3241
- "row.modelsFoldHide": "Click to collapse",
3242
2999
  "row.modelsRefresh": "Refresh from WorkBuddy",
3243
3000
  "row.modelsRefreshing": "Refreshing models…",
3244
3001
  "row.discard": "Discard changes",
@@ -3268,16 +3025,14 @@ window.__ModuleLoader__.load({
3268
3025
  "row.probeOutOfCreditAt": "✗ Not enough credits · quota refreshes {at}",
3269
3026
  "row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
3270
3027
  "row.probeNotFound": "✗ The upstream does not know this model",
3271
- "row.probeUnavailable": "✗ The upstream could not be reached — this is a network problem, not the model",
3028
+ "row.probeUnavailable": "✗ The upstream could not be reached — no usable answer came back",
3272
3029
  "row.probeFailed": "✗ Test failed",
3273
3030
  "row.probeFailedStatus": "✗ Test failed (HTTP {status})",
3274
3031
  "row.probeError": "Could not run the test: {message}",
3275
3032
  "row.cheer": "Star on GitHub",
3276
3033
  "row.poolTitle": "Account pool",
3277
- "row.poolSummary": "{count} account(s) in this region · target model {model}",
3278
3034
  "row.poolNoCandidate": "No account in this pool can be used right now.",
3279
- "row.poolNoCandidateHint": "Every account is rate-limited, out of credits, rejected, unreachable, or in a state this build cannot use. Re-check the accounts, or sign in again in the desktop app.",
3280
- "row.poolRotating": "Auto-rotating",
3035
+ "row.poolNoCandidateHint": "Members are rate-limited, out of credits, rejected, or unreachable. Rate limits, drained quotas and transient failures return to the pool on their own — at the time the upstream states, or after a 30-minute fallback. Only a rejected credential needs a new sign-in.",
3281
3036
  "row.poolCheckinAll": "Check in all accounts",
3282
3037
  "row.poolCheckingIn": "Checking in…",
3283
3038
  "row.poolTestAll": "Test all accounts",
@@ -3288,6 +3043,8 @@ window.__ModuleLoader__.load({
3288
3043
  "row.poolTargetNoneShort": "no free model",
3289
3044
  "row.poolTargetFree": "Auto-picked free model: {model}",
3290
3045
  "row.poolTargetPreferred": "Your chosen model: {model}",
3046
+ "row.poolTargetCurrentPreferred": "Current: {model} (your choice).",
3047
+ "row.poolTargetCurrentFree": "Current: {model} (auto-picked free model).",
3291
3048
  "row.poolTargetNone": "This region has no zero-multiplier model yet.",
3292
3049
  "row.poolTargetStale": "The target model you saved is no longer offered: {model}",
3293
3050
  "row.poolTargetStaleHint": "The region's catalog changed and no longer lists this model, so testing is off rather than running against a model the upstream may not accept. Pick another target, or switch back to automatic.",
@@ -3311,15 +3068,17 @@ window.__ModuleLoader__.load({
3311
3068
  "row.poolExcludedUnusable": "Unusable",
3312
3069
  "row.poolRetryAt": "Back after {at}",
3313
3070
  "row.poolRetryUnknown": "The upstream gave no time for when this frees up",
3071
+ "row.remainingImminent": "any moment now",
3072
+ "row.remainingMinutes": "in about {count} min",
3073
+ "row.remainingHours": "in about {count} h",
3074
+ "row.remainingDays": "in about {count} d",
3314
3075
  "row.poolSettingsTitle": "Account pool settings",
3315
- "row.poolSettingsHint": "These four are preferences: they take effect when you save (same as the model list). Test results and rotation are written automatically and are not affected by this button.",
3076
+ "row.poolSettingsHint": "These are preferences: they take effect when you save (same as the model list). Test results are written automatically and are not affected by this button.",
3316
3077
  "row.poolEnabled": "Enable the account pool",
3317
- "row.poolEnabledHint": "While off, the plugin behaves exactly as before: it follows the account you selected and never reorders accounts.",
3318
- "row.poolRotate": "Rotate by credits",
3319
- "row.poolRotateHint": "Let the plugin pick the billing account by availability, then credits, then expiry (ties fall back to credential freshness and account id). Turning this on disables manual selection until you save; every switch is recorded.",
3320
- "row.poolInterval": "Automatic test interval",
3321
- "row.poolIntervalHint": "Minutes between automatic tests of this region's accounts. Runs only while BOTH the pool and this region's provider are enabled; switching either off stops the timer.",
3322
- "row.poolIntervalUnit": "minutes",
3078
+ "row.poolEnabledHint": "While on, this region's pool decides who serves every request (usable first, then credits, soonest-expiring, freshest credential) and retries on another account when one fails. While off, the plugin does not pick or switch: the account you selected serves every request and a failure is reported as-is. Check-in and testing are MANUAL actions and stay available either way — the switch only controls whether the plugin decides by itself.",
3079
+ "row.poolManualAccountLabel": "Account in use",
3080
+ "row.poolManualAccountHint": "Which account serves this region while the pool is off. Turn the pool on and the ranking picks instead.",
3081
+ "row.poolManualAccountNone": "No account in effect",
3323
3082
  "row.poolTargetLabel": "Target test model",
3324
3083
  "row.poolTargetHint": "Auto (default) picks a zero-multiplier model from this region's catalog, preferring the largest context window. The two regions can pick different models. A manual choice always wins.",
3325
3084
  "row.poolTargetAutoOption": "Auto — pick a free model",
@@ -3329,13 +3088,7 @@ window.__ModuleLoader__.load({
3329
3088
  "row.poolSavedStaleRefresh": "Saved, but the panel could not re-read it. The value is stored; the next refresh will show it.",
3330
3089
  "row.poolDiscard": "Discard changes",
3331
3090
  "row.poolDirty": "● Unsaved changes",
3332
- "row.poolSaveIdle": "Change any setting above to save it.",
3333
- "row.poolSaveDirty": "You have unsaved changes — saving writes them to this region.",
3334
- "row.poolManualLocked": "Manual selection is disabled: this region is rotating automatically",
3335
- "row.poolManualLockedHint": "The plugin picks the account by availability, then credits, then expiry, so you cannot pin one manually. The dropdown above is disabled to avoid showing one account while another is billed.",
3336
3091
  "row.poolManualUnlock": "Turn off rotation",
3337
- "row.poolUnlockPending": "Save to apply — until then the plugin is still rotating.",
3338
- "row.poolUnlockSaved": "Rotation off — save to unlock",
3339
3092
  "row.poolLogTitle": "Activity",
3340
3093
  "row.poolLogClear": "Clear",
3341
3094
  "row.poolLogEmpty": "Nothing yet. Check-ins, tests, and account switches are recorded here.",
@@ -3349,8 +3102,6 @@ window.__ModuleLoader__.load({
3349
3102
  "row.poolLogTestRow": "{accountName}: {outcome}",
3350
3103
  "row.poolLogTestDone": "Test finished; the pool was re-ranked",
3351
3104
  "row.poolLogTestRowMalformed": "{accountName}: the host returned no result for this account",
3352
- "row.poolLogRotated": "Switched billing from {from} to {to}",
3353
- "row.poolLogRotationStopped": "Rotation stopped — back to {from}",
3354
3105
  "row.poolRegionNote": "The domestic and international pools are fully independent: accounts, switches, intervals, and target models never affect each other.",
3355
3106
  "row.poolMembersTitle": "Accounts in this pool",
3356
3107
  "row.poolMembersHint": "Only checked accounts are checked in, tested, and used for rotation. Nothing is selected by default — check the ones you want, then save.",
@@ -3362,6 +3113,8 @@ window.__ModuleLoader__.load({
3362
3113
  "row.poolGhostMembers": "{count} saved account(s) are no longer signed in on this machine, so they are not counted and not used. Save again to drop them.",
3363
3114
  "row.poolSelectAria": "Include {accountName} in this region's account pool",
3364
3115
  "row.poolMember": "In pool",
3116
+ "row.poolCurrentBadge": "In use",
3117
+ "row.poolCurrentHeader": "In use: {account}",
3365
3118
  "row.poolNotMember": "Not in pool",
3366
3119
  "row.poolUnsavedMembers": "Selection changed — save to apply"
3367
3120
  };
@@ -3376,7 +3129,6 @@ window.__ModuleLoader__.load({
3376
3129
  "row.tabSwitchHint": "取消勾选即关闭该供应商:它的模型不再出现在 DSH 模型选择器里;账号、积分与模型设置都会保留,重新勾选即恢复。",
3377
3130
  "row.tabSwitchAria": "是否向 DSH 提供{region}供应商",
3378
3131
  "row.tabOffNotice": "该供应商已关闭:它的模型不会提供给 DSH。账号、积分与模型设置均已保留——勾选上方复选框即可恢复。",
3379
- "row.signedOut": "未登录",
3380
3132
  "row.signedOutHint": "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
3381
3133
  "row.signedOutWrongRegion": "找到了你的 WorkBuddy 登录信息,但它属于另一个地区的标签页。切换到上方对应标签页即可使用——不需要重新登录。",
3382
3134
  "row.signedOutEncrypted": "你的 WorkBuddy 登录信息在,但其中的 token 字段是加密的,插件拿不到密钥。请安装 WorkBuddy 桌面 App,或用环境变量 WORKBUDDY_APP_EXECUTABLE 指定它的可执行文件——重新登录无法解决这个问题。",
@@ -3390,8 +3142,6 @@ window.__ModuleLoader__.load({
3390
3142
  "row.sourceDesktop": "桌面 App",
3391
3143
  "row.sourceDsh": "插件副本",
3392
3144
  "row.searchedMore": "显示另外 {count} 条「不存在」的路径",
3393
- "row.signedIn": "已登录:{accountName}",
3394
- "row.tokenExpiry": "访问令牌 {expiresAt} 过期(自动续期)",
3395
3145
  "row.reloginHint": "出现错误,重新登录 WorkBuddy APP 即可。",
3396
3146
  "row.credentialRejectedTitle": "WorkBuddy 拒绝了这个账号的 token",
3397
3147
  "row.credentialRejectedIntro": "「{accountName}」的 token 已不被上游接受,所以积分与签到都用不了。重新登录只在「确实是 token 的问题」时才有用——当前实际适用的是下面这一条:",
@@ -3404,38 +3154,24 @@ window.__ModuleLoader__.load({
3404
3154
  "row.poolErrUnavailable": "本版本没有可用的账号池(或其批量接口缺失)。请重启应用;若仍不行,插件可能需要重新安装。",
3405
3155
  "row.poolErrFailed": "批量操作在宿主侧失败:{message}",
3406
3156
  "row.poolErrHttp": "批量操作被拒绝(HTTP {status})。请确认插件是最新版本后重试。",
3407
- "row.poolErrDisabled": "本区域的账号池是关闭的。请先启用并保存,再执行批量操作。",
3408
3157
  "row.poolErrNoMembers": "本池没有勾选任何账号,批量操作没有可执行对象。请至少勾选一个账号并保存。",
3409
3158
  "row.poolErrNoLiveMembers": "本池勾选的账号在本机都已不再登录,批量操作没有可执行对象。请在 WorkBuddy 桌面端重新登录,或保存以移除它们。",
3410
3159
  "row.poolErrNoFreeModel": "本区域没有可用的免费(倍率为 0)模型。请先刷新模型目录,或自己指定一个目标模型。",
3160
+ "row.poolErrNoCheckin": "本区域没有每日签到 —— 这里不提供该操作。",
3411
3161
  "row.poolErrStaleModel": "目标模型已不在本区域目录中。请改选一个,或切回自动。",
3412
- "row.creditsTotalLabel": "总剩余积分",
3413
- "row.creditsMonthlyRemain": "剩余 {remain} / {size} · {at} 刷新",
3414
- "row.checkinClaim": "立即签到",
3415
- "row.checkinClaiming": "签到中…",
3416
- "row.checkinClaimed": "今日已签到",
3417
- "row.checkinError": "签到状态获取失败:{message}",
3418
3162
  "row.creditsError": "积分查询失败:{message}",
3419
- "row.creditsNoSoon": "3 天内没有到期礼包。",
3420
- "row.creditsExpiringSoon": "最近 3 天到期",
3421
3163
  "row.refresh": "刷新",
3422
3164
  "row.refreshing": "正在刷新…",
3423
- "row.accountsTitle": "WorkBuddy 账号",
3424
3165
  "row.accountUnnamed": "未命名账号",
3425
3166
  "row.accountsHint": "选择本机检测到的登录账号;Token 不会显示,也不会保存到 DSH 设置。",
3426
3167
  "row.accountsRescan": "重新检测账号",
3427
3168
  "row.accountsScanning": "正在检测…",
3428
- "row.accountNoneInEffect": "当前没有生效的账号",
3429
- "row.accountsSavedChoice": "正在使用该区域保存的账号。",
3430
3169
  "row.accountsWriteFailed": "账号改动未保存:{message} —— 当前 profile 的配置文件写入失败(Windows 上可能被杀毒软件或同步盘占用)。当前仍是原来保存的选择,请先关闭占用该文件的程序再重试(文件的确切路径见插件仓库的 docs/WINDOWS.md)。",
3431
3170
  "row.selectionLostHint": "已保存的账号在本机已不存在。",
3432
3171
  "row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方列表中重新选择一个当前账号。重新登录桌面端 App 无法修复此问题。",
3433
3172
  "row.modelsTitle": "模型",
3434
3173
  "row.modelsSummary": "已启用 {count} 个",
3435
3174
  "row.modelsDirty": "有未保存的修改",
3436
- "row.modelsFoldHint": "展开或收起模型列表",
3437
- "row.modelsFoldShow": "点击展开",
3438
- "row.modelsFoldHide": "点击收起",
3439
3175
  "row.modelsRefresh": "从 WorkBuddy 刷新",
3440
3176
  "row.modelsRefreshing": "正在刷新模型…",
3441
3177
  "row.discard": "放弃修改",
@@ -3456,7 +3192,7 @@ window.__ModuleLoader__.load({
3456
3192
  "row.modelCapabilityPending": "仅展示 WorkBuddy 接口明确公布的模型能力。",
3457
3193
  "row.probe": "测试",
3458
3194
  "row.probing": "测试中…",
3459
- "row.probeHint": "对该模型发一条**真实体积**的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按**请求体积**触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
3195
+ "row.probeHint": "对该模型发一条真实体积的请求(约 25k 输入 token),报告它现在能不能用。上游的限流按请求体积触发——实测约 20k 能过、约 30k 被拒——所以小请求看着正常、长对话里却可能每次都被拒。这也是为什么每次测试都会消耗真实积分(约 0.7),且一次只测一个模型。",
3460
3196
  "row.probeOk": "✓ 可用",
3461
3197
  "row.probeOkMs": "✓ 可用({ms})",
3462
3198
  "row.probeRateLimitedUnknown": "✗ 被限流 · 上游未给出何时恢复",
@@ -3465,16 +3201,14 @@ window.__ModuleLoader__.load({
3465
3201
  "row.probeOutOfCreditAt": "✗ 积分不足 · 积分包 {at} 刷新",
3466
3202
  "row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
3467
3203
  "row.probeNotFound": "✗ 上游不认识这个模型",
3468
- "row.probeUnavailable": "✗ 连不上上游——这是网络问题,不是模型问题",
3204
+ "row.probeUnavailable": "✗ 连不上上游——上游没有给出可用答复",
3469
3205
  "row.probeFailed": "✗ 测试失败",
3470
3206
  "row.probeFailedStatus": "✗ 测试失败(HTTP {status})",
3471
3207
  "row.probeError": "无法执行测试:{message}",
3472
3208
  "row.cheer": "鼓励一下",
3473
3209
  "row.poolTitle": "账号池",
3474
- "row.poolSummary": "本区域 {count} 个账号 · 目标模型 {model}",
3475
3210
  "row.poolNoCandidate": "账号池里目前没有可用的账号。",
3476
- "row.poolNoCandidateHint": "每个账号要么被限流、要么积分耗尽、要么被上游拒绝、要么连不上上游,或处于本版本无法使用的状态。请重新检测账号,或在桌面端重新登录。",
3477
- "row.poolRotating": "自动轮换中",
3211
+ "row.poolNoCandidateHint": "成员要么在限流冷却中、要么积分耗尽、要么凭据被拒、要么上游无响应。限流、积分耗尽与瞬时故障都会自动回池——按上游给出的恢复时间,没给时间就按 30 分钟兜底,不需要你操作;只有「凭据被拒」需要重新登录。",
3478
3212
  "row.poolCheckinAll": "一键签到所有账号",
3479
3213
  "row.poolCheckingIn": "签到中…",
3480
3214
  "row.poolTestAll": "一键测试所有账号",
@@ -3485,12 +3219,14 @@ window.__ModuleLoader__.load({
3485
3219
  "row.poolTargetNoneShort": "无免费模型",
3486
3220
  "row.poolTargetFree": "自动选中的免费模型:{model}",
3487
3221
  "row.poolTargetPreferred": "你指定的模型:{model}",
3222
+ "row.poolTargetCurrentPreferred": "当前:{model}(你指定)。",
3223
+ "row.poolTargetCurrentFree": "当前:{model}(自动挑选的免费模型)。",
3488
3224
  "row.poolTargetNone": "本区域暂时没有倍率为 0 的免费模型。",
3489
3225
  "row.poolTargetStale": "你保存的目标模型已不在目录中:{model}",
3490
3226
  "row.poolTargetStaleHint": "本区域的模型目录已变化,不再包含这个模型,因此测试被停用——而不是拿一个上游可能不接受的模型去跑。请改选一个目标模型,或切回自动。",
3491
3227
  "row.poolTargetStaleClear": "切回自动",
3492
3228
  "row.poolTargetStaleOption": "{model}(已下架)",
3493
- "row.poolTargetNoneHint": "国内版的倍率要等模型目录刷新后才会出现。为了不让你在不知情的情况下花掉积分,插件**不会**自动改用收费模型——请先刷新模型目录,或自己指定一个目标模型。",
3229
+ "row.poolTargetNoneHint": "国内版的倍率要等模型目录刷新后才会出现。为了不让你在不知情的情况下花掉积分,插件不会自动改用收费模型——请先刷新模型目录,或自己指定一个目标模型。",
3494
3230
  "row.poolColumnAccount": "账号",
3495
3231
  "row.poolColumnCredits": "积分",
3496
3232
  "row.poolColumnProbe": "目标模型",
@@ -3508,15 +3244,17 @@ window.__ModuleLoader__.load({
3508
3244
  "row.poolExcludedUnusable": "不可用",
3509
3245
  "row.poolRetryAt": "{at} 之后可再用",
3510
3246
  "row.poolRetryUnknown": "上游未给出何时恢复",
3247
+ "row.remainingImminent": "即将恢复",
3248
+ "row.remainingMinutes": "约 {count} 分钟后",
3249
+ "row.remainingHours": "约 {count} 小时后",
3250
+ "row.remainingDays": "约 {count} 天后",
3511
3251
  "row.poolSettingsTitle": "账号池设置",
3512
- "row.poolSettingsHint": "下面四项是偏好设置:点保存才生效(与「模型管理」一致)。测试结果与轮换记录由插件自动写入,不受这个保存按钮影响。",
3252
+ "row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
3513
3253
  "row.poolEnabled": "启用账号池",
3514
- "row.poolEnabledHint": "关闭时插件行为与从前完全一致:跟随你选择的账号,绝不按积分重排。",
3515
- "row.poolRotate": "按积分自动轮换",
3516
- "row.poolRotateHint": "允许插件按「可用性 → 积分 → 可用时间」自动选择计费账号(并列时依次比凭据新鲜度与账号 id)。开启后需保存才停用手动选择;每次切换都会留下记录。",
3517
- "row.poolInterval": "自动测试间隔",
3518
- "row.poolIntervalHint": "每隔多少分钟自动测试本区域账号。需要「账号池」与本区域的供应商都启用;任一关闭即停止定时器。",
3519
- "row.poolIntervalUnit": "分钟",
3254
+ "row.poolEnabledHint": "开启后由本区域的账号池自动决定每一次请求用谁(可用优先,其次积分高、快过期的积分包、凭据更新的),失败时自动换下一个可用账号。关闭后插件不替你选、也不替你换:你选的账号服务每一次请求,失败就如实报错。签到与测试是手动动作,两种状态下都可用 —— 这个开关只管「插件要不要自己决定」。",
3255
+ "row.poolManualAccountLabel": "使用哪个账号",
3256
+ "row.poolManualAccountHint": "账号池关闭时,由你在这里选定服务本区域的账号。开启账号池后改由排序自动决定。",
3257
+ "row.poolManualAccountNone": "当前没有生效的账号",
3520
3258
  "row.poolTargetLabel": "目标测试模型",
3521
3259
  "row.poolTargetHint": "「自动」(默认)会从本区域目录里挑一个倍率为 0 的免费模型,多个候选取上下文窗口最大的。国内版与国际版可能挑到不同的模型。手动指定优先于自动。",
3522
3260
  "row.poolTargetAutoOption": "自动 —— 挑一个免费模型",
@@ -3526,13 +3264,7 @@ window.__ModuleLoader__.load({
3526
3264
  "row.poolSavedStaleRefresh": "已保存,但面板未能重新读取。设置已写入;下次刷新后会显示。",
3527
3265
  "row.poolDiscard": "放弃修改",
3528
3266
  "row.poolDirty": "● 有未保存的修改",
3529
- "row.poolSaveIdle": "修改任一设置后即可保存。",
3530
- "row.poolSaveDirty": "有未保存的修改 —— 保存后写入本区域配置。",
3531
- "row.poolManualLocked": "手动选择已停用:本区域正在自动轮换",
3532
- "row.poolManualLockedHint": "插件按「可用性 → 积分 → 可用时间」自动选择账号,因此不能手动指定。上面的下拉已置灰,避免出现\"显示 A 却计费 B\"。",
3533
3267
  "row.poolManualUnlock": "关闭自动轮换",
3534
- "row.poolUnlockPending": "保存后生效 —— 在那之前插件仍在轮换。",
3535
- "row.poolUnlockSaved": "已关闭轮换 —— 保存后解锁",
3536
3268
  "row.poolLogTitle": "活动记录",
3537
3269
  "row.poolLogClear": "清空",
3538
3270
  "row.poolLogEmpty": "尚无活动。签到、测试与账号切换都会记录在这里。",
@@ -3546,8 +3278,6 @@ window.__ModuleLoader__.load({
3546
3278
  "row.poolLogTestRow": "{accountName}:{outcome}",
3547
3279
  "row.poolLogTestDone": "测试完成,已重新计算候选账号",
3548
3280
  "row.poolLogTestRowMalformed": "{accountName}:Host 未返回该账号的结果",
3549
- "row.poolLogRotated": "计费账号已从 {from} 切换到 {to}",
3550
- "row.poolLogRotationStopped": "轮换已停止 —— 回到 {from}",
3551
3281
  "row.poolRegionNote": "国内版与国际版是完全独立的两个账号池:账号、开关、间隔、目标模型互不影响。",
3552
3282
  "row.poolMembersTitle": "本池包含的账号",
3553
3283
  "row.poolMembersHint": "只有勾选的账号才会被签到、测试和轮换。默认一个都不勾 —— 请勾选你要的账号,然后保存。",
@@ -3559,6 +3289,8 @@ window.__ModuleLoader__.load({
3559
3289
  "row.poolGhostMembers": "有 {count} 个已保存的账号在本机已不再登录,因此不计入、也不会被使用。再保存一次即可移除它们。",
3560
3290
  "row.poolSelectAria": "把 {accountName} 加入本区域的账号池",
3561
3291
  "row.poolMember": "已在池中",
3292
+ "row.poolCurrentBadge": "当前使用",
3293
+ "row.poolCurrentHeader": "当前使用:{account}",
3562
3294
  "row.poolNotMember": "未入池",
3563
3295
  "row.poolUnsavedMembers": "勾选已改动 —— 保存后生效"
3564
3296
  };