dsh-connect-workbuddy 5.4.0 → 5.5.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
@@ -111,6 +111,19 @@ window.__ModuleLoader__.load({
111
111
  * arbitrary user data and export carries token material.
112
112
  */
113
113
  const WORKBUDDY_TRANSFER_PATH = "/plugins/dsh-connect-workbuddy/transfer";
114
+ /**
115
+ * The CLI route's live turns, for the composer readout.
116
+ *
117
+ * A GET with no parameters and no region: a live turn is not scoped to a
118
+ * credential region (it is one spawned process), and the chip polls this on an
119
+ * interval, so it must stay cheap — the handler reads an in-memory board and
120
+ * serializes it, touching no upstream and no disk.
121
+ *
122
+ * The document is `{ turns: LiveTurnView[], serverTime: number }`. `serverTime`
123
+ * is the host's clock at serialization so the browser can derive an age from
124
+ * `startedAt` without trusting the two clocks to agree.
125
+ */
126
+ const WORKBUDDY_CLI_LIVE_PATH = "/plugins/dsh-connect-workbuddy/cli-live";
114
127
  /** Address one region's pool endpoint with an action. */
115
128
  function withWorkBuddyRegionAndAction(path, region, action) {
116
129
  return `${withWorkBuddyRegion(path, region)}&${WORKBUDDY_POOL_ACTION_PARAM}=${action}`;
@@ -179,14 +192,27 @@ window.__ModuleLoader__.load({
179
192
  * it is then `undefined`: the enabled flag reads back as "on" forever (the very
180
193
  * symptom described above), and a merge that spreads this map silently drops
181
194
  * the region it was not editing. Unwrap before narrowing.
195
+ *
196
+ * THE HOP IS CONDITIONAL, and that is the whole point (a saved manual pool
197
+ * order "disappearing"). "Has a `regions` key" is NOT sufficient evidence that
198
+ * the caller passed the whole section: the `regions` MAP can legitimately carry
199
+ * a `regions` key of its own, because an earlier build could write a sibling
200
+ * field name into it — a live profile was found with `accounts`, `regions` and
201
+ * `cliBridge` sitting beside `cn` / `global`. Hopping there means reading a
202
+ * DIFFERENT, older copy of the same region: measured, the same document returned
203
+ * `['c','a','b']` when read as a section and `[]` when read as a map. So a value
204
+ * counts as the whole section only when it shows a key that is neither a region
205
+ * id nor a known sibling — i.e. it looks like a settings document. A plain (or
206
+ * dirty) `cn` / `global` map is returned as-is.
182
207
  */
183
208
  function regionsMapOf(value) {
184
209
  const unwrapped = unwrapVolatileDeep(value);
185
210
  if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
186
211
  const record = unwrapped;
187
212
  const nested = record["regions"];
188
- if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
189
- return record;
213
+ if (typeof nested !== "object" || nested === null || Array.isArray(nested)) return record;
214
+ const regionIds = WORKBUDDY_REGIONS;
215
+ return Object.keys(record).some((key) => regionIds.includes(key)) ? record : nested;
190
216
  }
191
217
  /** One region's stored slot as a plain object; any other shape reads as empty. */
192
218
  function regionSlotOf(value, region) {
@@ -2726,7 +2752,7 @@ window.__ModuleLoader__.load({
2726
2752
  * Returns null when there is nothing to say, so the caller can fall back to the
2727
2753
  * static explanation instead of rendering an empty "current:" prefix.
2728
2754
  */
2729
- 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;
2755
+ const targetProvenance = pool.targetModelSource === "free" ? t("row.poolTargetCurrentFree", { model: pool.targetModelId ?? "" }) : null;
2730
2756
  /**
2731
2757
  * The account the next request will start from.
2732
2758
  *
@@ -2736,15 +2762,19 @@ window.__ModuleLoader__.load({
2736
2762
  */
2737
2763
  const currentAccount = pool.accounts.find((account) => account.current);
2738
2764
  const currentName = currentAccount === void 0 ? void 0 : currentAccount.accountName === "" ? t("row.accountUnnamed") : currentAccount.accountName;
2739
- return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, rowBusyAction === "remove" ? (0, react.createElement)(BusyOverlay, {
2740
- visible: true,
2741
- message: t("row.poolRowRemoving"),
2742
- hint: t("row.veilHintList")
2743
- }) : null, saving ? (0, react.createElement)(BusyOverlay, {
2744
- visible: true,
2745
- message: t("row.poolSaving"),
2746
- hint: t("row.veilHintList")
2747
- }) : null, (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 }))), !poolRunning && 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", {
2765
+ /**
2766
+ * The manual-mode picker: shown ONLY with the pool off, and it is the whole of
2767
+ * "manual mode" — with the pool off nothing else chooses who is billed.
2768
+ *
2769
+ * It lives in the HEAD line, sharing one slot with the 「current: X」 badge,
2770
+ * instead of in a row of its own further down. That row said the same account
2771
+ * name for the third time on one screen (badge, picker, member row), and the
2772
+ * repetition is what made the page read as generated rather than designed.
2773
+ * The two states are the two halves of one question — "who serves?" — so they
2774
+ * get one place to answer it: a picker when the answer is yours, a badge when
2775
+ * the pool decides.
2776
+ */
2777
+ const manualPicker = !poolRunning && onSelectAccount !== void 0 && pool.accounts.length > 0 ? (0, react.createElement)("label", { className: "dsm-workbuddy-pool-head-pick" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-head-pick-label" }, t("row.poolManualAccountLabel")), (0, react.createElement)("select", {
2748
2778
  className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
2749
2779
  value: currentAccount?.accountId ?? "",
2750
2780
  disabled: selectingAccount === true,
@@ -2757,27 +2787,45 @@ window.__ModuleLoader__.load({
2757
2787
  }, t("row.poolManualAccountNone")), ...pool.accounts.map((account) => (0, react.createElement)("option", {
2758
2788
  key: account.accountId,
2759
2789
  value: account.accountId
2760
- }, 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", {
2761
- type: "button",
2762
- className: "dsm-btn dsm-btn-primary",
2763
- disabled: busy !== void 0 || effectiveMembers.length === 0,
2764
- onClick: () => {
2765
- runAction("checkin");
2766
- }
2767
- }, busy === "checkin" ? t("row.poolCheckingIn") : t("row.poolCheckinAll")) : null, (0, react.createElement)("button", {
2768
- type: "button",
2769
- className: "dsm-btn dsm-btn-outline",
2770
- disabled: busy !== void 0 || effectiveMembers.length === 0 || pool.targetModelSource === "none" || pool.targetModelSource === "stale",
2771
- title: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
2772
- onClick: () => {
2773
- runAction("test");
2790
+ }, account.accountName === "" ? t("row.accountUnnamed") : account.accountName)))) : null;
2791
+ /**
2792
+ * The full ordering rule, collapsed — pure reference text.
2793
+ *
2794
+ * It used to sit between 「本池包含的账号」 and the first account row, which put
2795
+ * a paragraph of explanation ahead of the accounts it explains and pushed the
2796
+ * table down. Reference text belongs BELOW the data it describes; here it is,
2797
+ * at the top of 「更多设置」, next to the controls it explains.
2798
+ *
2799
+ * A <details>/<summary> pair rather than a stateful button: it is
2800
+ * keyboard-operable and screen-reader-expanded natively, and cannot fall out
2801
+ * of sync with what is on screen. `open` is controlled so a background refresh
2802
+ * cannot collapse a section the user just opened.
2803
+ */
2804
+ const orderRules = (0, react.createElement)("details", {
2805
+ className: "dsm-workbuddy-pool-order-rules",
2806
+ open: orderRulesOpen,
2807
+ onToggle: (event) => setOrderRulesOpen(event.currentTarget.open)
2808
+ }, (0, react.createElement)("summary", { className: "dsm-workbuddy-pool-order-rules-summary" }, t("row.poolOrderRulesTitle")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-order-rules-body" }, (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolOrderRulesIntro")), (0, react.createElement)("ol", { className: "dsm-workbuddy-pool-order-rules-list" }, ...ORDER_RULE_KEYS.map((key) => (0, react.createElement)("li", { key }, t(key)))), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-order-rules-why" }, t("row.poolOrderWhy")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolOrderBadgeNote"))));
2809
+ return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, rowBusyAction === "remove" ? (0, react.createElement)(BusyOverlay, {
2810
+ visible: true,
2811
+ message: t("row.poolRowRemoving"),
2812
+ hint: t("row.veilHintList")
2813
+ }) : null, saving ? (0, react.createElement)(BusyOverlay, {
2814
+ visible: true,
2815
+ message: t("row.poolSaving"),
2816
+ hint: t("row.veilHintList")
2817
+ }) : null, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolTitle")), manualPicker !== null ? manualPicker : currentName === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-badge" }, t("row.poolCurrentHeader", { account: currentName }))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings dsm-workbuddy-pool-modebar" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-modebar-row" }, (0, react.createElement)("label", { className: "sw" }, (0, react.createElement)("input", {
2818
+ type: "checkbox",
2819
+ checked: active.enabled,
2820
+ disabled: !canEditPool,
2821
+ onChange: (event) => {
2822
+ const next = event.currentTarget.checked;
2823
+ editDraft((current) => ({
2824
+ ...current,
2825
+ enabled: next
2826
+ }));
2774
2827
  }
2775
- }, 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", {
2776
- type: "button",
2777
- className: "dsm-btn dsm-btn-outline",
2778
- disabled: rescanning === true,
2779
- onClick: onRescan
2780
- }, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), !poolRunning ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolEnabledHint")) : null, poolRunning && usable.length === 0 ? allMembersBlockedByModel(pool?.accounts ?? [], effectiveMembers, active.memberAccountIds) ? (0, react.createElement)("p", {
2828
+ }), (0, react.createElement)("span", { className: "sw-track" })), (0, react.createElement)("b", { className: "dsm-workbuddy-pool-modebar-label" }, t("row.poolEnabled")), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-modebar-state" }, poolRunning ? t("row.poolModeAuto") : t("row.poolModeManual")), 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.poolEnabledHint"))), poolRunning && currentAccount !== void 0 && !effectiveMembers.includes(currentAccount.accountId) ? (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note dsm-workbuddy-pool-serving-note" }, t("row.poolServingOutside", { account: currentName ?? "" })) : null, poolRunning && effectiveMembers.length > 0 && usable.length === 0 ? allMembersBlockedByModel(pool?.accounts ?? [], effectiveMembers, active.memberAccountIds) ? (0, react.createElement)("p", {
2781
2829
  className: "dsm-workbuddy-pool-warn",
2782
2830
  role: "status"
2783
2831
  }, `${t("row.poolModelWideBusy")} ${t("row.poolModelWideBusyHint")}`) : (0, react.createElement)("p", {
@@ -2796,11 +2844,27 @@ window.__ModuleLoader__.load({
2796
2844
  className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
2797
2845
  disabled: effectiveMembers.length === 0 || !canEditPool,
2798
2846
  onClick: () => selectAll(false)
2799
- }, t("row.poolSelectNone")))), (0, react.createElement)("details", {
2800
- className: "dsm-workbuddy-pool-order-rules",
2801
- open: orderRulesOpen,
2802
- onToggle: (event) => setOrderRulesOpen(event.currentTarget.open)
2803
- }, (0, react.createElement)("summary", { className: "dsm-workbuddy-pool-order-rules-summary" }, t("row.poolOrderRulesTitle")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-order-rules-body" }, (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolOrderRulesIntro")), (0, react.createElement)("ol", { className: "dsm-workbuddy-pool-order-rules-list" }, ...ORDER_RULE_KEYS.map((key) => (0, react.createElement)("li", { key }, t(key)))), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-order-rules-why" }, t("row.poolOrderWhy")), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolOrderBadgeNote")))), (0, react.createElement)("div", { className: `dsm-workbuddy-pool-table${pool.checkinSupported ? " dsm-workbuddy-pool-table-checkin" : ""}` }, (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, (0, react.createElement)("span", null)), ...(() => {
2847
+ }, t("row.poolSelectNone")))), (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", {
2848
+ type: "button",
2849
+ className: "dsm-btn dsm-btn-primary",
2850
+ disabled: busy !== void 0 || effectiveMembers.length === 0,
2851
+ onClick: () => {
2852
+ runAction("checkin");
2853
+ }
2854
+ }, busy === "checkin" ? t("row.poolCheckingIn") : t("row.poolCheckinAll")) : null, (0, react.createElement)("button", {
2855
+ type: "button",
2856
+ className: "dsm-btn dsm-btn-outline",
2857
+ disabled: busy !== void 0 || effectiveMembers.length === 0 || pool.targetModelSource === "none" || pool.targetModelSource === "stale",
2858
+ title: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
2859
+ onClick: () => {
2860
+ runAction("test");
2861
+ }
2862
+ }, busy === "test" ? t("row.poolTesting") : t("row.poolTestAll")), busy !== void 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolBusyHint")) : effectiveMembers.length === 0 ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-hint" }, t("row.poolNoneSelectedHint")) : null), onRescan === void 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-actions-group" }, (0, react.createElement)("button", {
2863
+ type: "button",
2864
+ className: "dsm-btn dsm-btn-outline",
2865
+ disabled: rescanning === true,
2866
+ onClick: onRescan
2867
+ }, rescanning === true ? t("row.accountsScanning") : t("row.accountsRescan")))), (0, react.createElement)("div", { className: `dsm-workbuddy-pool-table${pool.checkinSupported ? " dsm-workbuddy-pool-table-checkin" : ""}` }, (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, (0, react.createElement)("span", null)), ...(() => {
2804
2868
  const ordered = sortAccountsByOrder(pool.accounts, orderIds);
2805
2869
  let memberSeen = 0;
2806
2870
  let memberTotal = 0;
@@ -2824,6 +2888,7 @@ window.__ModuleLoader__.load({
2824
2888
  claimBusy: rowBusyAccountId === account.accountId,
2825
2889
  testDisabled: pool.targetModelSource === "none" || pool.targetModelSource === "stale",
2826
2890
  targetHint: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
2891
+ poolModelId: pool.targetModelId ?? "",
2827
2892
  onTest: () => {
2828
2893
  runRowTest(account.accountId);
2829
2894
  },
@@ -2835,7 +2900,7 @@ window.__ModuleLoader__.load({
2835
2900
  },
2836
2901
  orderIndex: isMember && orderIsManual ? left + 1 : void 0,
2837
2902
  orderIsManual,
2838
- moveEnabled: pool.enabled === true && isMember && canEditPool,
2903
+ moveEnabled: active.enabled && isMember && canEditPool,
2839
2904
  onMoveUp: () => {
2840
2905
  moveMember(account.accountId, -1);
2841
2906
  },
@@ -2846,10 +2911,7 @@ window.__ModuleLoader__.load({
2846
2911
  moveDownDisabled: right <= 0
2847
2912
  });
2848
2913
  });
2849
- })()), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
2850
- className: "dsm-workbuddy-pool-warn",
2851
- role: "status"
2852
- }, `${t("row.poolNoneSelected")} ${t("row.poolNoneSelectedHint")}`) : null, ghostMembers$1.length === 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-ghosts" }, (0, react.createElement)("p", {
2914
+ })()), ghostMembers$1.length === 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-ghosts" }, (0, react.createElement)("p", {
2853
2915
  className: "dsm-workbuddy-pool-warn",
2854
2916
  role: "status"
2855
2917
  }, t("row.poolGhostMembers", {
@@ -2899,6 +2961,7 @@ window.__ModuleLoader__.load({
2899
2961
  onDiscard: discard,
2900
2962
  appendLog,
2901
2963
  targetProvenance,
2964
+ orderRules,
2902
2965
  ...accountTools === void 0 ? {} : { accountTools }
2903
2966
  }), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolRegionNote")), renderLog({
2904
2967
  t,
@@ -2912,7 +2975,7 @@ window.__ModuleLoader__.load({
2912
2975
  }
2913
2976
  /** One account's row. */
2914
2977
  function renderAccountRow(input) {
2915
- const { t, account, checked, canEdit, checkinSupported, onToggle, busy, rowBusy, testDisabled, targetHint, onTest, onRemove, onClaim, rowBusyAction, claimBusy, orderIndex, orderIsManual, moveEnabled, onMoveUp, onMoveDown, moveUpDisabled, moveDownDisabled } = input;
2978
+ const { t, account, checked, canEdit, checkinSupported, onToggle, busy, rowBusy, testDisabled, targetHint, onTest, onRemove, onClaim, rowBusyAction, claimBusy, orderIndex, orderIsManual, moveEnabled, onMoveUp, onMoveDown, moveUpDisabled, moveDownDisabled, poolModelId } = input;
2916
2979
  const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
2917
2980
  const excluded = exclusionText(t, account.excludedBy);
2918
2981
  const probe = account.probe;
@@ -2962,7 +3025,7 @@ window.__ModuleLoader__.load({
2962
3025
  "aria-label": t("row.poolMoveDownAria", { accountName: name }),
2963
3026
  disabled: moveDownDisabled,
2964
3027
  onClick: onMoveDown
2965
- }, "↓")) : null) : null, 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: formatShortClock(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: formatShortClock(account.nearestExpiryMs) })), account.benefits === void 0 || account.benefits.length === 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-benefits" }, account.benefits.map((benefit, index) => (0, react.createElement)("span", {
3028
+ }, "↓")) : null) : null, account.current ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-current-tag" }, t("row.poolCurrentBadge")) : null, checked ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-not-member" }, t("row.poolNotMember")), excluded === void 0 ? probe === void 0 ? null : (0, react.createElement)("span", null, t("row.poolTestedAt", { at: formatShortClock(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: formatShortClock(account.nearestExpiryMs) })), account.benefits === void 0 || account.benefits.length === 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-benefits" }, account.benefits.map((benefit, index) => (0, react.createElement)("span", {
2966
3029
  key: benefit.kind,
2967
3030
  className: benefit.urgent ? "dsm-workbuddy-pool-benefit dsm-workbuddy-pool-benefit-urgent" : "dsm-workbuddy-pool-benefit"
2968
3031
  }, index === 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-benefit-tag" }, benefit.urgent ? benefit.daysLeft === void 0 || benefit.daysLeft === 0 ? t("row.poolBenefitDueToday", {
@@ -2988,7 +3051,7 @@ window.__ModuleLoader__.load({
2988
3051
  }, (0, react.createElement)("span", null, claimBusy && rowBusyAction === "claim" ? t("row.poolBenefitClaiming") : t("row.poolBenefitClaim"))))))), (0, react.createElement)("span", {
2989
3052
  className: "dsm-workbuddy-pool-probe",
2990
3053
  ...probe?.message === void 0 ? {} : { title: probe.message }
2991
- }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age), probe === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-model" }, probe.modelId === void 0 ? t("row.poolProbeModelUnknown") : t("row.poolProbeModel", { model: probe.modelId })))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
3054
+ }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age), probe !== void 0 && probe.modelId !== poolModelId ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-model" }, probe.modelId === void 0 ? t("row.poolProbeModelUnknown") : t("row.poolProbeModel", { model: probe.modelId })) : null)), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
2992
3055
  className: "dsm-workbuddy-pool-unknown",
2993
3056
  title: t("row.poolNeverTested")
2994
3057
  }, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("button", {
@@ -3008,18 +3071,9 @@ window.__ModuleLoader__.load({
3008
3071
  }
3009
3072
  /** The preference editor plus its save/discard actions. */
3010
3073
  function renderSettings(input) {
3011
- const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance, accountTools } = input;
3074
+ const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance, accountTools, orderRules } = input;
3012
3075
  const canEdit = settingsScope !== void 0;
3013
- 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", {
3014
- type: "checkbox",
3015
- checked,
3016
- disabled: disabled || !canEdit,
3017
- onChange: (event) => onChange(event.currentTarget.checked)
3018
- }), (0, react.createElement)("span", { className: "sw-track" }))));
3019
- 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), accountTools === void 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-tools" }, accountTools), (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) => ({
3020
- ...current,
3021
- enabled: next
3022
- }))), (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", {
3076
+ return (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings-wrap" }, accountTools === void 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-tools" }, accountTools), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-card-head" }, (0, react.createElement)("h4", { className: "dsm-workbuddy-pool-card-title" }, t("row.poolSettingsTitle"))), orderRules, (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolSettingsHint")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings" }, (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", {
3023
3077
  className: "dsm-workbuddy-pool-select",
3024
3078
  value: active.targetModelId,
3025
3079
  disabled: !canEdit,
@@ -3504,10 +3558,6 @@ window.__ModuleLoader__.load({
3504
3558
  message: t("row.oauthSyncing"),
3505
3559
  hint: t("row.veilHintList")
3506
3560
  }),
3507
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3508
- className: "dsm-workbuddy-bar-label",
3509
- children: t(titleKey)
3510
- }),
3511
3561
  phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3512
3562
  className: `dsm-workbuddy-bar-status ${phase.message === "added" ? "dsm-workbuddy-bar-ok" : "dsm-workbuddy-bar-error"}`,
3513
3563
  children: finishText
@@ -3810,10 +3860,6 @@ window.__ModuleLoader__.load({
3810
3860
  message: veilMessage,
3811
3861
  ...busy ? { hint: t("row.veilHintList") } : {}
3812
3862
  }),
3813
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3814
- className: "dsm-workbuddy-bar-label",
3815
- children: t("row.transferTitle")
3816
- }),
3817
3863
  phase.kind === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3818
3864
  className: "dsm-workbuddy-bar-status dsm-workbuddy-bar-ok",
3819
3865
  children: phase.message
@@ -4200,6 +4246,20 @@ window.__ModuleLoader__.load({
4200
4246
  return badge.label;
4201
4247
  }
4202
4248
  /**
4249
+ * The badge's wording where the chip has to fit a narrow row.
4250
+ *
4251
+ * The model picker's rows are one line wide and already carry the model name,
4252
+ * the credit rate and the load icon, so a four-character label ("夜间折扣") can
4253
+ * push the check mark out of the row. The upstream publishes a compact wording
4254
+ * for exactly this case (`shortLabel`), and when it does not, the full label is
4255
+ * still better than a cropped one: an ellipsis in a menu row reads as a
4256
+ * truncated word, while the full label is at worst tight.
4257
+ */
4258
+ function promoBadgeShortLabel(promo, language) {
4259
+ const badge = promo.badge;
4260
+ return (isEnglish(language) ? badge.shortLabelEn ?? badge.shortLabel : badge.shortLabel) ?? promoBadgeLabel(promo, language);
4261
+ }
4262
+ /**
4203
4263
  * The chip's colour, when the upstream published a usable one.
4204
4264
  *
4205
4265
  * Only a hex colour is accepted: the value reaches an inline style, and the
@@ -4560,6 +4620,11 @@ window.__ModuleLoader__.load({
4560
4620
  scrolling to the end of a long model list. */
4561
4621
  .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}
4562
4622
  .dsm-workbuddy-models-summary{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
4623
+ /* The one grey line that states what a section is FOR, at the top of a panel.
4624
+ Kept as its own class instead of reusing the models-summary class: the same
4625
+ rule now heads the capability section, whose vocabulary has nothing to do with
4626
+ models, and a shared name would send the next reader to the wrong panel. */
4627
+ .dsm-workbuddy-panel-note{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
4563
4628
  /* The free-model quota warning (see the tabFreeQuotaWarn locale key). Bold on
4564
4629
  purpose, and it keeps the summary's own grey rather than the primary label
4565
4630
  colour: it has to stand out from the three lines around it WITHOUT reading as
@@ -4602,7 +4667,15 @@ window.__ModuleLoader__.load({
4602
4667
  one the brand colour stands in. "flex:none" keeps the badge whole while the
4603
4668
  model name beside it truncates with an ellipsis. The plain background comes
4604
4669
  first so an engine without color-mix still gets a readable chip. */
4605
- .dsm-workbuddy-model-label{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;vertical-align:baseline}
4670
+ .dsm-workbuddy-model-label{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;vertical-align:baseline}
4671
+ /* The same chip, inside the HOST's picker. A model row there is one line wide
4672
+ and already carries the name, the check mark and the load icon, so the chip
4673
+ gets a hard ceiling and the TEXT inside it truncates — the chip itself must
4674
+ stay whole, because a half-drawn pill reads as a rendering bug. "6em" at the
4675
+ chip's 10px is about six characters: enough for "夜间折扣", not enough for a
4676
+ label to push the row's own controls out of view. */
4677
+ .dsm-workbuddy-model-label-promo{max-width:6em;overflow:hidden}
4678
+ .dsm-workbuddy-model-label-promo-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4606
4679
  .dsm-workbuddy-busy{flex:none;display:inline-flex;align-items:center;block-size:12px;color:var(--dsm-workbuddy-busy-color);vertical-align:-2px}
4607
4680
  .dsm-workbuddy-busy svg{display:block;block-size:12px;inline-size:auto}
4608
4681
  .dsm-workbuddy-model-promo{flex:none;display:inline-flex;align-items:center;padding:0 7px;border-radius:999px;font-size:10px;line-height:16px;font-weight:600;white-space:nowrap;color:var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe));background:rgba(255,255,255,.08);background:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 16%,transparent);border:1px solid transparent;border-color:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 40%,transparent)}
@@ -4679,21 +4752,26 @@ window.__ModuleLoader__.load({
4679
4752
  .dsm-workbuddy-pool-current-badge{margin-left:auto;padding:2px 10px;border-radius:999px;background:var(--dsw-alias-state-success-primary,#22c55e);border:1px solid rgba(34,197,94,.55);color:#fff;font-size:12px;font-weight:600;line-height:18px;white-space:nowrap}
4680
4753
  /* Sits under the header block, before the batch buttons: account discovery is
4681
4754
  the first step of using the pool, so it reads above the actions it enables. */
4682
- /* Manual-mode account picker: shares the settings row layout (.pool-set), and
4683
- only the select itself needs a hint that it is a PICKER of accounts rather
4684
- than of models — the two sit near each other and used to be indistinguishable
4685
- to a selector query as well as to the eye. */
4755
+ /* THE HEAD SLOT — one place that answers "who is serving?".
4756
+ *
4757
+ * With the pool off it holds the account picker, with the pool on the badge.
4758
+ * Both are pushed right by the same rule so the header does not jump between
4759
+ * the two states. The picker used to live in a settings row of its own further
4760
+ * down the page, which meant the same account name appeared in the title row,
4761
+ * in that row, and again in the member table. */
4762
+ .dsm-workbuddy-pool-head-pick{margin-left:auto;display:flex;align-items:center;gap:8px;font-size:12px}
4763
+ .dsm-workbuddy-pool-head-pick-label{color:var(--dsw-alias-label-secondary,#c6c9d0);white-space:nowrap}
4686
4764
  .dsm-workbuddy-pool-account-select{min-width:200px}
4687
4765
  .dsm-workbuddy-pool-title{margin:0;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
4688
4766
  .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)}
4689
- /* One row, two clusters: LEFT acts on the accounts you checked, RIGHT re-reads
4690
- which accounts exist. Before this the rescan button was on its own row pushed
4691
- right while the batch buttons were on the next row pushed left — two
4692
- alignments two lines apart, with nothing explaining the split. */
4693
- .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}
4767
+ /* No top rule: this bar now hangs directly under 「本池包含的账号 / 已选 N / M /
4768
+ 全选 / 全不选」 and acts on exactly that set, so a separator here would cut the
4769
+ buttons away from the count that explains them. The divider that matters is
4770
+ the one the member block has against 「更多设置」 below. */
4771
+ .dsm-workbuddy-pool-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
4694
4772
  .dsm-workbuddy-pool-actions-group{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
4695
- .dsm-workbuddy-pool-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
4696
- .dsm-workbuddy-pool-note{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#999)}
4773
+ .dsm-workbuddy-pool-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
4774
+ .dsm-workbuddy-pool-note{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
4697
4775
  .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)}
4698
4776
  /* Ghost members (issue #60): the warning plus one removable chip per id. The
4699
4777
  chips are what make the warning actionable — a ghost has no row in the table
@@ -4753,7 +4831,7 @@ window.__ModuleLoader__.load({
4753
4831
  its built-in keyboard and screen-reader behaviour. The summary is a full-width
4754
4832
  row because a 12px grey label on its own is a small click target, and this is
4755
4833
  the one place a confused user is meant to click. */
4756
- .dsm-workbuddy-pool-order-rules{border:1px solid var(--dsw-alias-border-l1,#2c2d31);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#232529);margin:0 0 10px;overflow:hidden}
4834
+ .dsm-workbuddy-pool-order-rules{border:1px solid var(--dsw-alias-border-l1,#2c2d31);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#232529);overflow:hidden}
4757
4835
  .dsm-workbuddy-pool-order-rules-summary{padding:8px 12px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;font-weight:600;line-height:18px}
4758
4836
  .dsm-workbuddy-pool-order-rules-summary:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}
4759
4837
  .dsm-workbuddy-pool-order-rules-summary:focus-visible{outline:2px solid rgba(86,134,254,.55);outline-offset:-2px}
@@ -4907,17 +4985,57 @@ window.__ModuleLoader__.load({
4907
4985
  .dsm-workbuddy-pool-check input:disabled{cursor:default;opacity:.5}
4908
4986
  .dsm-workbuddy-pool-checked{color:var(--dsw-alias-state-success-primary,#22a06b)}
4909
4987
  .dsm-workbuddy-pool-unknown{color:var(--dsw-alias-label-dimmed,#9aa0a6)}
4910
- .dsm-workbuddy-pool-member{color:var(--dsw-alias-brand-primary,#5686fe)}
4911
4988
  .dsm-workbuddy-pool-not-member{color:var(--dsw-alias-label-dimmed,#9aa0a6)}
4912
- .dsm-workbuddy-pool-settings-wrap{display:flex;flex-direction:column;gap:10px}
4989
+ /* A section break, not just a gap: below this line the page stops showing what
4990
+ the pool IS and starts showing the knobs that change it. Without the rule the
4991
+ heading floated under the table with nothing marking the change of subject. */
4992
+ .dsm-workbuddy-pool-settings-wrap{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:14px}
4913
4993
  .dsm-workbuddy-pool-dirty{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
4914
4994
  .dsm-workbuddy-pool-settings{display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
4915
- /* QR sign-in + export/import, hosted inside the pool settings block.
4916
- The block's own bordered rows belong to the SAVED preferences, so these
4917
- get their own container: same rhythm (12px 14px) so the two read as one
4918
- group, but a dashed edge marking them as immediate actions rather than
4919
- drafts the Save button below would commit. */
4920
- .dsm-workbuddy-pool-tools{display:flex;flex-direction:column;gap:2px;border:1px dashed var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;padding:4px 10px}
4995
+ /* THE MODE BAR — the page's master control, directly under the title.
4996
+ *
4997
+ * It reuses the .dsm-workbuddy-pool-settings class for the bordered, stacked
4998
+ * look, and on purpose: the component tests locate 「启用账号池」 as the first
4999
+ * checkbox inside that class, and a user who has learned "settings live in a
5000
+ * bordered block" should not have to learn a second shape for the most
5001
+ * important one.
5002
+ *
5003
+ * What it adds is a brand-tinted edge and background, so it reads as "this
5004
+ * decides what the rest of the page means" instead of as one more settings
5005
+ * card — the switch used to sit at the BOTTOM, 400px below this point.
5006
+ */
5007
+ .dsm-workbuddy-pool-modebar{padding:12px 14px;gap:8px;border-color:rgba(86,134,254,.36);background:linear-gradient(180deg,rgba(86,134,254,.10),rgba(86,134,254,.02)),var(--dsw-alias-bg-layer-2,#24262c)}
5008
+ .dsm-workbuddy-pool-modebar-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
5009
+ .dsm-workbuddy-pool-modebar-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
5010
+ /* The mode in words. A switch's position is a 20px shape to decode; this says
5011
+ which of the two modes is in force, and therefore how to read the table. */
5012
+ .dsm-workbuddy-pool-modebar-state{flex:none;padding:1px 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,.14);border:1px solid rgba(86,134,254,.3)}
5013
+ .dsm-workbuddy-pool-modebar .dsm-workbuddy-pool-dirty{margin-left:auto}
5014
+ .dsm-workbuddy-pool-modebar .dsm-workbuddy-pool-note{line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
5015
+ /* Orthogonal concepts in one sentence when they collide ("in use" vs "in the
5016
+ pool"). A left rule marks it as an aside about the row above, not a warning. */
5017
+ .dsm-workbuddy-pool-serving-note{padding-left:10px;border-left:2px solid rgba(86,134,254,.5);color:var(--dsw-alias-label-secondary,#c6c9d0)}
5018
+ /* Subheading for "the rest of the settings": one step DOWN from the page title,
5019
+ so the panel has one title instead of two equally loud ones. */
5020
+ .dsm-workbuddy-pool-card-head{display:flex;align-items:center;gap:8px}
5021
+ .dsm-workbuddy-pool-card-title{margin:0;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#c6c9d0)}
5022
+ /* QR sign-in + export/import, ABOVE the settings block.
5023
+ One toolbar row, not a column of two: with the captions gone (the buttons
5024
+ name themselves) there is nothing left to stack, and a row is where a user
5025
+ looks for instant actions. The dashed edge still separates them from the
5026
+ saved preferences below — that contrast is now the whole point, since the
5027
+ heading under this row is the one that says 「点保存才生效」.
5028
+ Left-aligned, not right: 扫码登录 is the primary action and it should read
5029
+ first, in the same left-hand column as 一键签到 above and 「更多设置」 below.
5030
+ Pushed to the right edge the three buttons looked like the tail of the row
5031
+ above rather than the start of this one. */
5032
+ .dsm-workbuddy-pool-tools{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;justify-content:flex-start;gap:8px;border:1px dashed var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;padding:6px 10px}
5033
+ /* A bar inside the toolbar is a flex item, not a full-width row: it keeps only
5034
+ its own content width, and the toolbar decides where the group sits. */
5035
+ .dsm-workbuddy-pool-tools .dsm-workbuddy-bar{flex:0 1 auto;min-width:0;margin:0}
5036
+ /* Status text may wrap here instead of truncating: a toolbar row is short, and
5037
+ 「已导出」「等待扫码」 are the whole message — an ellipsis would eat it. */
5038
+ .dsm-workbuddy-pool-tools .dsm-workbuddy-bar-status{white-space:normal}
4921
5039
  .dsm-workbuddy-pool-set{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:12px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31)}
4922
5040
  .dsm-workbuddy-pool-set:first-child{border-top:0}
4923
5041
  .dsm-workbuddy-pool-set-copy{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
@@ -4927,7 +5045,12 @@ window.__ModuleLoader__.load({
4927
5045
  .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}
4928
5046
  .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}
4929
5047
  .dsm-workbuddy-pool-select:disabled,.dsm-workbuddy-pool-num:disabled{opacity:.5;cursor:default}
4930
- .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}
5048
+ /* The commit row: pinned to the bottom of the viewport while the panel is on
5049
+ * screen. The panel is taller than a screen, and the two buttons that make any
5050
+ * edit real used to sit at its very bottom — off-screen at the moment the user
5051
+ * finished editing, and invisible entirely in a screenshot. Opaque, because
5052
+ * content scrolls under it. */
5053
+ .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;position:sticky;bottom:0;z-index:2;padding-bottom:8px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
4931
5054
  .dsm-workbuddy-pool-save-buttons{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex:none}
4932
5055
  .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)}
4933
5056
  .dsm-workbuddy-pool-pending{flex:none;align-self:center;font-size:12px;line-height:17px;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
@@ -4969,13 +5092,16 @@ window.__ModuleLoader__.load({
4969
5092
  .dsm-workbuddy-pool .sw input:disabled+.sw-track{opacity:.4}
4970
5093
  .dsm-workbuddy-pool .sw input:disabled{cursor:default}
4971
5094
  /* Account action bars: QR sign-in and export/import.
4972
- One row each — label, live status, actions — instead of a padded block
4973
- per feature. The bar is the resting height; only an open dialog (below)
4974
- grows vertically, so the card does not scroll past a wall of chrome to
4975
- reach the buttons. Status text shares the row rather than claiming its own,
4976
- and is what truncates first when the card is narrow. */
5095
+ One row each — live status, actions — instead of a padded block per feature.
5096
+ There is no caption: each bar holds buttons that name themselves, so a
5097
+ heading above them would repeat the button in a smaller font (「扫码添加账号」
5098
+ over 「扫码登录」). The section keeps an aria-label for the same reason a
5099
+ caption existed, so the name is still available to a screen reader.
5100
+ The bar is the resting height; only an open dialog (below) grows vertically,
5101
+ so the card does not scroll past a wall of chrome to reach the buttons.
5102
+ Status text shares the row rather than claiming its own, and is what
5103
+ truncates first when the card is narrow. */
4977
5104
  .dsm-workbuddy-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0;padding:0}
4978
- .dsm-workbuddy-bar-label{flex:none;margin:0;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#c6c9d0)}
4979
5105
  .dsm-workbuddy-bar-status{flex:1 1 auto;min-width:0;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e9ed);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4980
5106
  .dsm-workbuddy-bar-hint{color:var(--dsw-alias-label-tertiary,#999)}
4981
5107
  .dsm-workbuddy-bar-actions{display:flex;gap:8px;flex:none;margin-left:auto}
@@ -5225,6 +5351,32 @@ window.__ModuleLoader__.load({
5225
5351
  }
5226
5352
  .dsm-workbuddy-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
5227
5353
  .dsm-workbuddy-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
5354
+
5355
+ /* The CLI live pill shares the composer dock row with the credit readout above
5356
+ and the shell's own ContextMeter, so it takes ITS metrics from the same place
5357
+ the credit trigger does: 12px/18px, label-tertiary, tabular numerals. Rendered
5358
+ with inline styles it inherited the dock's body font and read as stray text
5359
+ sitting next to two native-looking controls. */
5360
+ .dsm-workbuddy-cli-live{
5361
+ display:inline-flex;align-items:center;gap:6px;
5362
+ padding:2px 6px;border-radius:6px;
5363
+ font-size:12px;line-height:18px;
5364
+ color:var(--dsw-alias-label-tertiary,#999);
5365
+ white-space:nowrap;font-variant-numeric:tabular-nums;
5366
+ }
5367
+ /* A quiet pulse is the whole signal that the turn is still moving: the label's
5368
+ second counter also advances, but at one tick a second it is easy to miss. */
5369
+ .dsm-workbuddy-cli-live-dot{
5370
+ width:6px;height:6px;border-radius:50%;flex:0 0 auto;
5371
+ background:var(--dsw-alias-brand-primary,#5686fe);
5372
+ animation:dsm-workbuddy-cli-live-pulse 1.6s ease-in-out infinite;
5373
+ }
5374
+ @keyframes dsm-workbuddy-cli-live-pulse{0%,100%{opacity:.3}50%{opacity:1}}
5375
+ /* Respect the OS setting: a blinking dot is decoration, the numbers are the
5376
+ information, and they keep updating with the animation off. */
5377
+ @media (prefers-reduced-motion:reduce){
5378
+ .dsm-workbuddy-cli-live-dot{animation:none;opacity:.8}
5379
+ }
5228
5380
  `;
5229
5381
  //#endregion
5230
5382
  //#region src/client/searched-paths.ts
@@ -6871,6 +7023,18 @@ window.__ModuleLoader__.load({
6871
7023
  },
6872
7024
  children: t("row.panelModels")
6873
7025
  }),
7026
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7027
+ type: "button",
7028
+ role: "tab",
7029
+ id: "dsm-workbuddy-tab-capabilities",
7030
+ "aria-controls": "dsm-workbuddy-panel-capabilities",
7031
+ "aria-selected": panel === "capabilities",
7032
+ className: `dsm-workbuddy-panel-tab${panel === "capabilities" ? " dsm-workbuddy-panel-tab-active" : ""}`,
7033
+ onClick: () => {
7034
+ setPanel("capabilities");
7035
+ },
7036
+ children: t("row.panelCapabilities")
7037
+ }),
6874
7038
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6875
7039
  type: "button",
6876
7040
  role: "tab",
@@ -6904,7 +7068,6 @@ window.__ModuleLoader__.load({
6904
7068
  className: "dsm-workbuddy-tab-off-notice",
6905
7069
  children: t("row.tabOffNotice")
6906
7070
  }) : null,
6907
- creditPreferences,
6908
7071
  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", {
6909
7072
  className: "dsm-workbuddy-usage-hint",
6910
7073
  children: t("row.reloginHint")
@@ -7077,11 +7240,7 @@ window.__ModuleLoader__.load({
7077
7240
  className: "dsm-workbuddy-model-name",
7078
7241
  children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7079
7242
  className: "dsm-workbuddy-model-name-rate",
7080
- children: [
7081
- "(",
7082
- model.creditMultiplier.toFixed(2),
7083
- "x)"
7084
- ]
7243
+ children: ["x", model.creditMultiplier.toFixed(2)]
7085
7244
  })]
7086
7245
  }),
7087
7246
  promoChip(model),
@@ -7259,6 +7418,17 @@ window.__ModuleLoader__.load({
7259
7418
  })
7260
7419
  })
7261
7420
  }) : null,
7421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7422
+ className: "dsm-workbuddy-panel",
7423
+ role: "tabpanel",
7424
+ id: "dsm-workbuddy-panel-capabilities",
7425
+ "aria-labelledby": "dsm-workbuddy-tab-capabilities",
7426
+ hidden: signedIn && panel !== "capabilities",
7427
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7428
+ className: "dsm-workbuddy-panel-note",
7429
+ children: t("row.panelCapabilitiesHint")
7430
+ }), creditPreferences]
7431
+ }),
7262
7432
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7263
7433
  className: "dsm-workbuddy-panel",
7264
7434
  role: "tabpanel",
@@ -8273,27 +8443,46 @@ window.__ModuleLoader__.load({
8273
8443
  //#endregion
8274
8444
  //#region src/client/WorkBuddyModelLabel.tsx
8275
8445
  /**
8276
- * The load-level badge in the HOST's model picker.
8446
+ * The HOST's model picker label: the catalog name, the upstream's promotion
8447
+ * badge, and the load-level icon.
8277
8448
  *
8278
8449
  * WHY A SLOT. The host renders the model name as a plain string, so the only
8279
8450
  * thing a provider can put there is a character — and a character's colour is
8280
8451
  * the emoji font's business, not ours. The host therefore offers exactly one
8281
8452
  * seam for this: the `conversation.input.model.label` slot, whose contract is
8282
8453
  * "render non-interactive inline content such as a coloured name or badge"
8283
- * while the picker keeps owning clicks, focus, check marks and the rest. The
8284
- * shipped example is the official DeepSeek "1017" release badge — a coloured
8285
- * marker next to a model name — which is the same job.
8454
+ * while the picker keeps owning clicks, focus, check marks and the accessibility
8455
+ * text. The shipped example is the official DeepSeek "1017" release badge — a
8456
+ * coloured marker next to a model name — which is the same job.
8457
+ *
8458
+ * WHY THE BADGE BELONGS HERE AT ALL. The picker is where the choice is made,
8459
+ * and a time-boxed promotion is a choice criterion: "free until the 25th" and
8460
+ * "half price at night" both change which row you click. The card already drew
8461
+ * these badges, but the card is the settings surface — by the time you read
8462
+ * them there you have already picked. The badge is not decoration added here;
8463
+ * it is the same upstream fact, moved to the place where it can still change a
8464
+ * decision.
8286
8465
  *
8287
8466
  * WHY A SEPARATE FETCH. The slot is rendered by the host, outside the plugin's
8288
8467
  * card, so it cannot read the card's React state. It therefore asks the Host
8289
- * for the load levels itself. The levels are a fact about this minute, so the
8290
- * answer is cached briefly: a picker that re-fetches on every re-render would
8291
- * be wasteful, and a level that is a few seconds old is still true.
8468
+ * for the snapshot itself — and asks for the SAME route the card polls
8469
+ * (`WORKBUDDY_USAGE_PATH`), which carries both the load levels and each model's
8470
+ * unresolved promotions. One request, both facts. The answer is cached briefly:
8471
+ * a picker that re-fetched on every re-render would be wasteful, and a level
8472
+ * that is a few seconds old is still true.
8473
+ *
8474
+ * WHY THE CLOCK IS RE-READ ON EVERY RENDER. A promotion's badge is a function
8475
+ * of the clock (a night window opens and closes; a campaign expires). Resolving
8476
+ * it once at fetch time and holding it would leave "夜间折扣" on the composer
8477
+ * button the next morning — the exact overstatement `src/promo.ts` refuses to
8478
+ * make. The snapshot is therefore fetched once and the BADGE is resolved at
8479
+ * render time; a shared one-minute ticker re-renders the mounted labels so a
8480
+ * window edge is picked up without one timer per model row.
8292
8481
  *
8293
8482
  * WHY THE HOST STILL OWNS THE NAME. The contract says to keep `model.name` as
8294
8483
  * the text and render the addition beside it, so this component returns the
8295
- * name followed by the badge — it never replaces the name, and it renders
8296
- * nothing at all for a provider that is not ours.
8484
+ * name first — it never replaces it, and it renders nothing at all for a
8485
+ * provider that is not ours.
8297
8486
  */
8298
8487
  /** The provider ids this plugin registers, and the region each one serves. */
8299
8488
  const PROVIDER_REGIONS = /* @__PURE__ */ new Map([["workbuddy", "cn"], ["workbuddy-global", "global"]]);
@@ -8308,70 +8497,381 @@ window.__ModuleLoader__.load({
8308
8497
  function regionOfProvider(providerId) {
8309
8498
  return PROVIDER_REGIONS.get(providerId);
8310
8499
  }
8311
- /** How long a fetched level set is trusted, in milliseconds. */
8500
+ /** How long a fetched snapshot is trusted, in milliseconds. */
8312
8501
  const CACHE_MS = 6e4;
8313
- /** The levels per region, by model id, plus when each set was read. */
8502
+ /** How often a mounted label re-reads the clock, in milliseconds. */
8503
+ const TICK_MS = 6e4;
8504
+ /** The snapshot per region, plus when it was read. */
8314
8505
  const caches = /* @__PURE__ */ new Map();
8315
8506
  /**
8316
- * The levels, read from the Host once and reused briefly.
8507
+ * The snapshot, read from the Host once and reused briefly.
8317
8508
  *
8318
8509
  * `undefined` means "not known" — the caller renders nothing rather than
8319
- * guessing, because a badge that claims a level the upstream never stated is
8320
- * worse than no badge.
8510
+ * guessing, because a badge that claims a level or a promotion the upstream
8511
+ * never stated is worse than no badge.
8321
8512
  */
8322
- async function levelsNow(region) {
8513
+ async function statusNow(region) {
8323
8514
  const cached = caches.get(region);
8324
- if (cached !== void 0 && Date.now() - cached.at < CACHE_MS) return cached.levels;
8515
+ if (cached !== void 0 && Date.now() - cached.at < CACHE_MS) return cached.snapshot;
8325
8516
  try {
8326
8517
  const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, region), {
8327
8518
  headers: { accept: "application/json" },
8328
8519
  credentials: "same-origin"
8329
8520
  });
8330
8521
  if (!response.ok) return void 0;
8331
- const levels = (await response.json()).busyLevels;
8332
- if (levels === void 0 || levels === null) return void 0;
8522
+ const body = await response.json();
8523
+ const promos = {};
8524
+ for (const model of body.models ?? []) if (model.promos !== void 0) promos[model.id] = model.promos;
8525
+ const snapshot = {
8526
+ levels: body.busyLevels ?? {},
8527
+ promos
8528
+ };
8333
8529
  caches.set(region, {
8334
- levels,
8530
+ snapshot,
8335
8531
  at: Date.now()
8336
8532
  });
8337
- return levels;
8533
+ return snapshot;
8338
8534
  } catch {
8339
8535
  return;
8340
8536
  }
8341
8537
  }
8538
+ /** One shared minute ticker, so N mounted labels cost one timer, not N. */
8539
+ const minuteTicker = {
8540
+ timer: void 0,
8541
+ listeners: /* @__PURE__ */ new Set()
8542
+ };
8543
+ /**
8544
+ * Re-render the caller once a minute while it is mounted.
8545
+ *
8546
+ * @param listener - The callback to run at each tick.
8547
+ * @returns The unsubscribe function, which stops the shared timer when the last
8548
+ * listener leaves.
8549
+ */
8550
+ function subscribeMinute(listener) {
8551
+ minuteTicker.listeners.add(listener);
8552
+ if (minuteTicker.timer === void 0) {
8553
+ minuteTicker.timer = setInterval(() => {
8554
+ for (const each of [...minuteTicker.listeners]) each();
8555
+ }, TICK_MS);
8556
+ minuteTicker.timer.unref?.();
8557
+ }
8558
+ return () => {
8559
+ minuteTicker.listeners.delete(listener);
8560
+ if (minuteTicker.listeners.size === 0 && minuteTicker.timer !== void 0) {
8561
+ clearInterval(minuteTicker.timer);
8562
+ minuteTicker.timer = void 0;
8563
+ }
8564
+ };
8565
+ }
8566
+ /**
8567
+ * The upstream's promotion badge, as the picker draws it.
8568
+ *
8569
+ * Reuses the card's chip class so the two surfaces cannot drift apart, and adds
8570
+ * one modifier for the two things a menu row changes: the chip must fit a row
8571
+ * that is one line wide (see `.dsm-workbuddy-model-label-promo` in the card's
8572
+ * stylesheet), and it is the only place the full tooltip is available.
8573
+ *
8574
+ * @param props - The resolved promotion, and whether space is tight.
8575
+ * @returns The chip.
8576
+ */
8577
+ function PromoBadge(props) {
8578
+ const { promo, compact } = props;
8579
+ const language = uiLanguage();
8580
+ const label = compact ? promoBadgeShortLabel(promo, language) : promoBadgeLabel(promo, language);
8581
+ const color = promoBadgeColor(promo);
8582
+ const tooltip = promoBadgeTooltip(promo, language);
8583
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8584
+ className: "dsm-workbuddy-model-promo dsm-workbuddy-model-label-promo",
8585
+ title: tooltip === "" ? void 0 : tooltip,
8586
+ style: color === void 0 ? void 0 : { "--dsm-workbuddy-promo-color": color },
8587
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8588
+ className: "dsm-workbuddy-model-label-promo-text",
8589
+ children: label
8590
+ })
8591
+ });
8592
+ }
8342
8593
  /**
8343
8594
  * The label for one model row in the host's picker.
8344
8595
  *
8345
8596
  * @param props - The host's model identity and the placement of this label.
8346
- * @returns The catalog name, followed by the load badge when there is one.
8597
+ * @returns The catalog name, the promotion chip when one holds, and the load
8598
+ * badge when the probe reported one.
8347
8599
  */
8348
8600
  function WorkBuddyModelLabel(props) {
8349
- const { provider, model } = props;
8601
+ const { provider, model, placement } = props;
8350
8602
  const region = regionOfProvider(provider.id);
8351
8603
  if (region === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
8352
8604
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkBuddyModelLabelInner, {
8353
8605
  modelId: model.id,
8354
8606
  name: model.name,
8355
- region
8607
+ region,
8608
+ compact: placement !== "trigger"
8356
8609
  });
8357
8610
  }
8358
- /** The stateful half: fetches the levels, then renders the badge. */
8611
+ /** The stateful half: fetches the snapshot, then renders what holds right now. */
8359
8612
  function WorkBuddyModelLabelInner(props) {
8360
- const { modelId, name, region } = props;
8361
- const [level, setLevel] = (0, react.useState)(void 0);
8613
+ const { modelId, name, region, compact } = props;
8614
+ const [snapshot, setSnapshot] = (0, react.useState)(void 0);
8615
+ const [, setTick] = (0, react.useState)(0);
8362
8616
  (0, react.useEffect)(() => {
8363
8617
  let live = true;
8364
- levelsNow(region).then((levels) => {
8365
- if (!live || levels === void 0) return;
8366
- setLevel(levels[modelId]);
8618
+ statusNow(region).then((next) => {
8619
+ if (!live || next === void 0) return;
8620
+ setSnapshot(next);
8367
8621
  });
8368
8622
  return () => {
8369
8623
  live = false;
8370
8624
  };
8371
8625
  }, [modelId, region]);
8626
+ (0, react.useEffect)(() => subscribeMinute(() => setTick((count) => count + 1)), []);
8627
+ const level = snapshot?.levels[modelId];
8628
+ const promo = snapshot === void 0 ? void 0 : selectPromoBadge(snapshot.promos[modelId], Date.now());
8372
8629
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8373
8630
  className: "dsm-workbuddy-model-label",
8374
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), level === void 0 ? null : busyIcon(level, "zh")]
8631
+ children: [
8632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }),
8633
+ promo === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PromoBadge, {
8634
+ promo,
8635
+ compact
8636
+ }),
8637
+ level === void 0 ? null : busyIcon(level, "zh")
8638
+ ]
8639
+ });
8640
+ }
8641
+ //#endregion
8642
+ //#region src/client/CodebuddyLive.tsx
8643
+ /**
8644
+ * The composer pill that says what the CodeBuddy CLI route is doing RIGHT NOW.
8645
+ *
8646
+ * WHY THIS EXISTS. A bridged turn is a spawned agent: minutes of reading files
8647
+ * and running commands, then one answer. The Host tool contract has no progress
8648
+ * channel — `execute()` returns when it returns — so without this pill the user
8649
+ * watches a spinner with no way to tell "still working" from "hung". The pill is
8650
+ * fed by the plugin's own read-only route (`WORKBUDDY_CLI_LIVE_PATH`), which
8651
+ * serializes the host-side live board (see `../cli-bridge/live.ts`).
8652
+ *
8653
+ * WHY IT RENDERS NOTHING WHEN IDLE. This is the ambient dock under the composer,
8654
+ * a row the shell already fills with round/token pills. A permanent "no turn
8655
+ * running" line would be a claim about a feature most users never switch on
8656
+ * (`cliBridge.enabled` defaults to false), so the empty state is `null`: the row
8657
+ * is untouched until there is something true to say.
8658
+ *
8659
+ * WHY IT ONLY DESCRIBES THIS SESSION'S TURN. The board is host-wide: one route
8660
+ * answers for every session in the process, so an unfiltered pill would show the
8661
+ * turn another tab started — a line about a directory the user is not looking at,
8662
+ * sitting under the composer they are typing in. The seat is declared
8663
+ * `scope: 'session'` (`conversation.composer.dock`), so the framework hands the
8664
+ * registration its `sessionId`; the pill keeps the rows that belong to it.
8665
+ *
8666
+ * WHY NOT THE PROVIDER GATE THE CREDIT READOUT USES. `ComposerCredits` renders
8667
+ * only while the session's model is a WorkBuddy provider, because it reports that
8668
+ * provider's quota. This pill reports a spawned CLI process, and the CLI route is
8669
+ * deliberately model-independent: the whole point is that it works from any
8670
+ * session, including one running DeepSeek. Gating it on the provider would hide
8671
+ * the readout exactly when the feature is being used the way it was designed.
8672
+ * `cliBridge.enabled` needs no gate either — with the bridge off no turn can
8673
+ * begin, so the board is empty and the pill renders nothing.
8674
+ *
8675
+ * WHY POLLING, AND WHY THE INTERVAL IS 5s. The Host has no push channel to a
8676
+ * session component, so a poll is the only option. Five seconds is the slowest
8677
+ * cadence at which a turn that just started still feels immediate; the elapsed
8678
+ * counter between polls is advanced locally from `startedAt` (a 1s tick that
8679
+ * costs no request), so the number moves every second without the network
8680
+ * traffic moving with it. Concurrent sessions share ONE in-flight request per
8681
+ * interval (see {@link readLive}), which is what keeps N open tabs from becoming
8682
+ * N polls per tick.
8683
+ *
8684
+ * WHY THE CLOCK OFFSET IS APPLIED. `startedAt` is the HOST's clock. A browser
8685
+ * whose clock is a minute behind would compute a negative age and the pill would
8686
+ * print nonsense. The document ships `serverTime`, so the offset is measured at
8687
+ * each successful read and applied to the local clock instead of trusting the
8688
+ * two machines to agree.
8689
+ *
8690
+ * @module dsh-connect-workbuddy/client/CodebuddyLive
8691
+ */
8692
+ /**
8693
+ * Inject the dock stylesheet on module load, once.
8694
+ *
8695
+ * `ComposerCredits` injects the same sheet for its own classes, and in the
8696
+ * shipped bundle both modules are static imports of the client entry, so in
8697
+ * practice the sheet is already there. This block is not redundant: it makes the
8698
+ * pill independent of whether the credit readout is mounted at all (the gate can
8699
+ * hide it for a non-WorkBuddy model), and the `data-plugin-css` guard means the
8700
+ * two never produce a second tag.
8701
+ */
8702
+ if (typeof document !== "undefined") {
8703
+ const cssId = "dsh-connect-workbuddy/composer-credits.css";
8704
+ if (!document.querySelector(`style[data-plugin-css="${cssId}"]`)) {
8705
+ const styleTag = document.createElement("style");
8706
+ styleTag.dataset.plugin = "dsh-connect-workbuddy";
8707
+ styleTag.dataset.pluginCss = cssId;
8708
+ styleTag.textContent = WORKBUDDY_COMPOSER_CSS;
8709
+ document.head.appendChild(styleTag);
8710
+ }
8711
+ }
8712
+ /** How often the route is asked. See the module comment for why 5s. */
8713
+ const CLI_LIVE_POLL_MS = 5e3;
8714
+ /**
8715
+ * The last document any component in this page saw, shared across sessions.
8716
+ *
8717
+ * A module-level cache is safe here because the document is host state, not
8718
+ * component state: every session polls the same board, so one request serves
8719
+ * them all. `inflight` collapses the case where several components mount in the
8720
+ * same tick — without it, opening three sessions at once would fire three
8721
+ * requests and each would overwrite the others' result.
8722
+ */
8723
+ let cached;
8724
+ let inflight;
8725
+ /**
8726
+ * Accept a document only if every number the pill does arithmetic on is a real
8727
+ * number.
8728
+ *
8729
+ * The route is our own, so this is not defence against an attacker — it is
8730
+ * defence against a HALF-AGREED wire: a field renamed on the host side, an
8731
+ * `undefined` that `JSON.stringify` drops, or a future route that answers a
8732
+ * different shape. Without the check `serverTime` becomes `NaN`, the clock
8733
+ * offset becomes `NaN`, and the pill renders `NaNs` — a number that is wrong in
8734
+ * the one place a user is looking for reassurance. Dropping the whole document
8735
+ * degrades to "no turn", which is the honest answer when the wire is unreadable.
8736
+ */
8737
+ function acceptDocument(value) {
8738
+ if (typeof value !== "object" || value === null) return void 0;
8739
+ const candidate = value;
8740
+ if (!Array.isArray(candidate.turns)) return void 0;
8741
+ if (typeof candidate.serverTime !== "number" || !Number.isFinite(candidate.serverTime)) return void 0;
8742
+ return {
8743
+ turns: candidate.turns.filter((turn) => {
8744
+ if (typeof turn !== "object" || turn === null) return false;
8745
+ const row = turn;
8746
+ return typeof row.startedAt === "number" && Number.isFinite(row.startedAt) && typeof row.toolCalls === "number" && Number.isFinite(row.toolCalls);
8747
+ }),
8748
+ serverTime: candidate.serverTime
8749
+ };
8750
+ }
8751
+ /** Read the board, reusing a recent document or a request already in flight. */
8752
+ async function readLive(fetchImpl, now = Date.now(), cacheMs = CLI_LIVE_POLL_MS) {
8753
+ if (cached !== void 0 && now - cached.at < cacheMs) return cached.document;
8754
+ if (inflight !== void 0) return inflight;
8755
+ inflight = (async () => {
8756
+ try {
8757
+ const response = await fetchImpl(WORKBUDDY_CLI_LIVE_PATH, { headers: { accept: "application/json" } });
8758
+ if (!response.ok) return void 0;
8759
+ const document = acceptDocument(await response.json());
8760
+ if (document === void 0) return void 0;
8761
+ cached = {
8762
+ at: Date.now(),
8763
+ document
8764
+ };
8765
+ return document;
8766
+ } catch {
8767
+ return;
8768
+ } finally {
8769
+ inflight = void 0;
8770
+ }
8771
+ })();
8772
+ return inflight;
8773
+ }
8774
+ /**
8775
+ * The text of one pill.
8776
+ *
8777
+ * Split out so the format is testable without a DOM, and so the same function
8778
+ * produces the visible label and the accessible one.
8779
+ */
8780
+ function liveLabel(turn, elapsedMs, t) {
8781
+ const seconds = Math.max(0, Math.round(elapsedMs / 1e3));
8782
+ const parts = [t("cliLive.running"), t("cliLive.toolCalls", { count: turn.toolCalls })];
8783
+ if (turn.phase === "starting") parts.push(t("cliLive.starting"));
8784
+ else if (turn.phase === "finishing") parts.push(t("cliLive.finishing"));
8785
+ else parts.push(t("cliLive.elapsed", { seconds }));
8786
+ if (turn.lastTool !== "") parts.push(t("cliLive.lastTool", { tool: turn.lastTool }));
8787
+ return parts.join(" · ");
8788
+ }
8789
+ /**
8790
+ * Keep the rows this pill is allowed to describe.
8791
+ *
8792
+ * A row with an empty `sessionId` is one the host could not attribute (the tool
8793
+ * ran without an agent on the execution object). Hiding those would make the pill
8794
+ * silently useless on such a host, so they are kept: an unattributed row is
8795
+ * evidence of real work, and the alternative is a readout that never appears.
8796
+ */
8797
+ function turnsForSession(turns, sessionId) {
8798
+ if (sessionId === void 0 || sessionId === "") return turns;
8799
+ return turns.filter((turn) => turn.sessionId === "" || turn.sessionId === sessionId);
8800
+ }
8801
+ /**
8802
+ * The pill itself.
8803
+ *
8804
+ * Shows the NEWEST turn (the one the last click started) and appends `+N` when
8805
+ * several are live, because the dock is a single line and a queue listing would
8806
+ * wrap. The count is the whole point of the `+N`: it tells the user there is
8807
+ * more work running than the line describes.
8808
+ */
8809
+ function CodebuddyLive(props) {
8810
+ const { t } = props;
8811
+ const fetchImpl = props.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
8812
+ const pollMs = props.pollMs ?? 5e3;
8813
+ const tickMs = props.tickMs ?? 1e3;
8814
+ const [document, setDocument] = (0, react.useState)(void 0);
8815
+ const [, setTick] = (0, react.useState)(0);
8816
+ const offsetRef = (0, react.useRef)(0);
8817
+ const offsetForRef = (0, react.useRef)(void 0);
8818
+ const failuresRef = (0, react.useRef)(0);
8819
+ (0, react.useEffect)(() => {
8820
+ if (fetchImpl === void 0) return void 0;
8821
+ let live = true;
8822
+ const read = () => {
8823
+ readLive(fetchImpl, Date.now(), pollMs).then((next) => {
8824
+ if (!live) return;
8825
+ if (next === void 0) {
8826
+ failuresRef.current += 1;
8827
+ if (failuresRef.current >= 3) setDocument(void 0);
8828
+ return;
8829
+ }
8830
+ failuresRef.current = 0;
8831
+ if (next.serverTime !== offsetForRef.current) {
8832
+ offsetForRef.current = next.serverTime;
8833
+ offsetRef.current = next.serverTime - Date.now();
8834
+ }
8835
+ setDocument(next);
8836
+ });
8837
+ };
8838
+ read();
8839
+ const timer = setInterval(read, pollMs);
8840
+ return () => {
8841
+ live = false;
8842
+ clearInterval(timer);
8843
+ };
8844
+ }, [fetchImpl, pollMs]);
8845
+ const turns = turnsForSession(document?.turns ?? [], props.sessionId);
8846
+ const active = turns.length > 0;
8847
+ (0, react.useEffect)(() => {
8848
+ if (!active) return void 0;
8849
+ const timer = setInterval(() => {
8850
+ setTick((value) => value + 1);
8851
+ }, tickMs);
8852
+ return () => {
8853
+ clearInterval(timer);
8854
+ };
8855
+ }, [active, tickMs]);
8856
+ if (!active) return null;
8857
+ const turn = turns[turns.length - 1];
8858
+ const label = liveLabel(turn, Math.max(0, Date.now() + offsetRef.current - turn.startedAt), t);
8859
+ const extra = turns.length > 1 ? ` +${String(turns.length - 1)}` : "";
8860
+ const dirTail = turn.workingDir.replace(/[\\/]+$/u, "").split(/[\\/]/u).pop() ?? "";
8861
+ const title = dirTail === "" ? `${label}${extra}` : `${label}${extra} · ${dirTail}`;
8862
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8863
+ role: "status",
8864
+ "aria-label": t("cliLive.aria", { detail: label }),
8865
+ title,
8866
+ className: "dsm-workbuddy-cli-live",
8867
+ children: [
8868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8869
+ "aria-hidden": "true",
8870
+ className: "dsm-workbuddy-cli-live-dot"
8871
+ }),
8872
+ label,
8873
+ extra
8874
+ ]
8375
8875
  });
8376
8876
  }
8377
8877
  //#endregion
@@ -8538,6 +9038,8 @@ window.__ModuleLoader__.load({
8538
9038
  "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.",
8539
9039
  "row.panelAccounts": "Accounts",
8540
9040
  "row.panelModels": "Models",
9041
+ "row.panelCapabilities": "Capabilities",
9042
+ "row.panelCapabilitiesHint": "What the plugin may do on your behalf. Each switch takes effect on the click — there is no Save button in this section.",
8541
9043
  "row.panelDiag": "Diagnostics",
8542
9044
  "row.panelAria": "Card sections",
8543
9045
  "row.diagHint": "What this card actually read, so a report can quote it. Nothing here changes a setting.",
@@ -8662,9 +9164,9 @@ window.__ModuleLoader__.load({
8662
9164
  "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.",
8663
9165
  "row.poolModelWideBusy": "Every member is blocked on the same model — this is the model, not your accounts.",
8664
9166
  "row.poolModelWideBusyHint": "Switching accounts cannot help: the limit follows the model, so a different account hits the same wall. Wait about a minute, or switch to a less busy model in this session.",
8665
- "row.poolCheckinAll": "Check in all accounts",
9167
+ "row.poolCheckinAll": "Check in selected",
8666
9168
  "row.poolCheckingIn": "Checking in…",
8667
- "row.poolTestAll": "Test all accounts",
9169
+ "row.poolTestAll": "Test selected",
8668
9170
  "row.poolTesting": "Testing…",
8669
9171
  "row.poolBusyHint": "Running one account at a time…",
8670
9172
  "row.poolTargetAuto": "Auto",
@@ -8682,9 +9184,9 @@ window.__ModuleLoader__.load({
8682
9184
  "row.poolTargetNoneHint": "The domestic catalog only reveals its multipliers after a model refresh. Rather than quietly spending credits on a paid model, testing stays off until one appears — refresh the models, or pick a target model yourself.",
8683
9185
  "row.poolColumnAccount": "Account",
8684
9186
  "row.poolColumnCredits": "Credits",
8685
- "row.poolColumnProbe": "Target model",
9187
+ "row.poolColumnProbe": "Status",
8686
9188
  "row.poolColumnCheckin": "Check-in",
8687
- "row.poolNeverTested": "Not tested yet",
9189
+ "row.poolNeverTested": "Untested",
8688
9190
  "row.poolTestedAt": "Tested {at}",
8689
9191
  "row.poolCheckedIn": "Checked in",
8690
9192
  "row.poolNotCheckedIn": "Not checked in",
@@ -8724,15 +9226,16 @@ window.__ModuleLoader__.load({
8724
9226
  "row.poolProbeSourceUnknown": "source unknown",
8725
9227
  "row.poolProbeModel": "model {model}",
8726
9228
  "row.poolProbeModelUnknown": "model not recorded",
8727
- "row.poolSettingsTitle": "Account pool settings",
9229
+ "row.poolSettingsTitle": "More settings",
8728
9230
  "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.",
8729
9231
  "row.poolEnabled": "Enable the account pool",
8730
- "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.",
9232
+ "row.poolEnabledHint": "On: the pool picks the account for every request and moves to the next one when it fails. Off: the account you select serves every request, and a failure is reported as it is.",
9233
+ "row.poolModeAuto": "picking automatically",
9234
+ "row.poolModeManual": "you pick",
8731
9235
  "row.poolManualAccountLabel": "Account in use",
8732
- "row.poolManualAccountHint": "Which account serves this region while the pool is off. Turn the pool on and the ranking picks instead.",
8733
9236
  "row.poolManualAccountNone": "No account in effect",
8734
- "row.poolTargetLabel": "Target test model",
8735
- "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.",
9237
+ "row.poolTargetLabel": "Test model (whole pool)",
9238
+ "row.poolTargetHint": "One model for every account here — the test measures each row with it. Auto picks a zero-multiplier model from this region's catalog, preferring the largest context window; a manual choice wins.",
8736
9239
  "row.poolTargetAutoOption": "Auto — pick a free model",
8737
9240
  "row.poolProbeSizeLabel": "Test request size",
8738
9241
  "row.poolProbeSizeHint": "How much text a test sends. The upstream refuses a request once it grows past roughly 20K–30K tokens, so a test smaller than your real conversations answers \"usable\" for an account that would refuse them. Bigger tests are more truthful but cost more credit each.",
@@ -8760,7 +9263,7 @@ window.__ModuleLoader__.load({
8760
9263
  "row.poolLogTestRowMalformed": "{accountName}: the host returned no result for this account",
8761
9264
  "row.poolRegionNote": "The domestic and international pools are fully independent: accounts, switches, intervals, and target models never affect each other.",
8762
9265
  "row.poolMembersTitle": "Accounts in this pool",
8763
- "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.",
9266
+ "row.poolMembersHint": "Only checked accounts rotate, get checked in, and get tested.",
8764
9267
  "row.poolSelectAll": "Select all",
8765
9268
  "row.poolSelectNone": "Clear all",
8766
9269
  "row.poolSelectedCount": "{count} of {total} selected",
@@ -8778,16 +9281,17 @@ window.__ModuleLoader__.load({
8778
9281
  "row.poolOrderBadgeNote": "The badge shows the position you SAVED, not the live ranking. An account can read \"No. 2\" while being benched for a rate limit — the number is your saved preference, and the pool skips accounts it cannot bill right now.",
8779
9282
  "row.poolOrderRulesToggle": "Show the full ordering rule",
8780
9283
  "row.poolNoneSelected": "No account is checked into this pool.",
8781
- "row.poolNoneSelectedHint": "Check at least one account above and save. Until then both buttons stay disabled, because a pool with no members has nothing to run on.",
9284
+ "row.poolNoneSelectedHint": "Check at least one account to enable check-in and test.",
8782
9285
  "row.poolGhostMembers": "{count} pool member(s) are no longer signed in on this machine, so they are not counted and not used: {names}. Remove any you no longer need below.",
8783
9286
  "row.poolGhostRemove": "Remove {account} from this pool",
8784
9287
  "row.poolGhostRemoveNamed": "Remove {account} from this pool. This account is no longer signed in on this machine.",
8785
9288
  "row.poolGhostRemoveUnnamed": "Remove {account} from this pool. No name was ever recorded for this account, so what you see is the start of its id.",
8786
9289
  "row.poolSelectAria": "Include {accountName} in this region's account pool",
8787
- "row.poolMember": "In pool",
8788
9290
  "row.poolCurrentBadge": "In use",
8789
9291
  "row.poolCurrentHeader": "In use: {account}",
8790
9292
  "row.poolNotMember": "Not in pool",
9293
+ "row.poolNotMemberHint": "Not in the pool: rotation skips it, and batch check-in and test leave it alone.",
9294
+ "row.poolServingOutside": "“{account}” is serving right now but is not in this pool: rotation will not pick it, and batch check-in and test will not touch it.",
8791
9295
  "row.poolUnsavedMembers": "Selection changed — save to apply",
8792
9296
  "row.poolRankLabel": "No. {rank}",
8793
9297
  "row.poolRankManualTitle": "You placed this account at position {rank} in the pool manually.",
@@ -8845,7 +9349,15 @@ window.__ModuleLoader__.load({
8845
9349
  "sidebar.refreshing": "Refreshing…",
8846
9350
  "sidebar.lastRefresh": "Last refreshed {time}",
8847
9351
  "sidebar.refreshFailed": "Refresh failed",
8848
- "sidebar.signedOut": "Not signed in"
9352
+ "sidebar.signedOut": "Not signed in",
9353
+ "cliLive.running": "CodeBuddy",
9354
+ "cliLive.toolCalls": "{count} tools",
9355
+ "cliLive.elapsed": "{seconds}s",
9356
+ "cliLive.starting": "starting…",
9357
+ "cliLive.finishing": "finishing…",
9358
+ "cliLive.lastTool": "last: {tool}",
9359
+ "cliLive.chars": "{count} chars",
9360
+ "cliLive.aria": "A CodeBuddy turn is running: {detail}"
8849
9361
  };
8850
9362
  const zh = {
8851
9363
  "row.title": "接入使用 WorkBuddy 积分与模型(dsh-connect-workbuddy)",
@@ -8904,6 +9416,8 @@ window.__ModuleLoader__.load({
8904
9416
  "row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方列表中重新选择一个当前账号。重新登录桌面端 App 无法修复此问题。",
8905
9417
  "row.panelAccounts": "账号",
8906
9418
  "row.panelModels": "模型",
9419
+ "row.panelCapabilities": "能力",
9420
+ "row.panelCapabilitiesHint": "插件可以替你做什么。这些开关点一下立即生效 —— 这一区里没有保存按钮。",
8907
9421
  "row.panelDiag": "诊断",
8908
9422
  "row.panelAria": "卡片分区",
8909
9423
  "row.diagHint": "这张卡片实际读到了什么,方便贴进问题反馈。这里没有任何会改动设置的控件。",
@@ -9028,9 +9542,9 @@ window.__ModuleLoader__.load({
9028
9542
  "row.poolNoCandidateHint": "成员要么在限流冷却中、要么积分耗尽、要么凭据被拒、要么上游无响应。限流、积分耗尽与瞬时故障都会自动回池——按上游给出的恢复时间,没给时间就按 30 分钟兜底,不需要你操作;只有「凭据被拒」需要重新登录。",
9029
9543
  "row.poolModelWideBusy": "所有成员都卡在同一个模型上 —— 这是模型的问题,不是你的账号。",
9030
9544
  "row.poolModelWideBusyHint": "换账号解决不了:限制跟随模型,换一个账号撞的是同一堵墙。等大约 1 分钟,或把这个会话切到负载较低的模型。",
9031
- "row.poolCheckinAll": "一键签到所有账号",
9545
+ "row.poolCheckinAll": "签到勾选的账号",
9032
9546
  "row.poolCheckingIn": "签到中…",
9033
- "row.poolTestAll": "一键测试所有账号",
9547
+ "row.poolTestAll": "测试勾选的账号",
9034
9548
  "row.poolTesting": "测试中…",
9035
9549
  "row.poolBusyHint": "正在逐个账号执行…",
9036
9550
  "row.poolTargetAuto": "自动",
@@ -9048,9 +9562,9 @@ window.__ModuleLoader__.load({
9048
9562
  "row.poolTargetNoneHint": "国内版的倍率要等模型目录刷新后才会出现。为了不让你在不知情的情况下花掉积分,插件不会自动改用收费模型——请先刷新模型目录,或自己指定一个目标模型。",
9049
9563
  "row.poolColumnAccount": "账号",
9050
9564
  "row.poolColumnCredits": "积分",
9051
- "row.poolColumnProbe": "目标模型",
9565
+ "row.poolColumnProbe": "状态",
9052
9566
  "row.poolColumnCheckin": "签到",
9053
- "row.poolNeverTested": "尚未测试",
9567
+ "row.poolNeverTested": "未测试",
9054
9568
  "row.poolTestedAt": "测试于 {at}",
9055
9569
  "row.poolCheckedIn": "已签到",
9056
9570
  "row.poolNotCheckedIn": "未签到",
@@ -9090,15 +9604,16 @@ window.__ModuleLoader__.load({
9090
9604
  "row.poolProbeSourceUnknown": "来源未知",
9091
9605
  "row.poolProbeModel": "模型 {model}",
9092
9606
  "row.poolProbeModelUnknown": "未记录模型",
9093
- "row.poolSettingsTitle": "账号池设置",
9607
+ "row.poolSettingsTitle": "更多设置",
9094
9608
  "row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
9095
9609
  "row.poolEnabled": "启用账号池",
9096
- "row.poolEnabledHint": "开启后由本区域的账号池自动决定每一次请求用谁(可用优先,其次积分高、快过期的积分包、凭据更新的),失败时自动换下一个可用账号。关闭后插件不替你选、也不替你换:你选的账号服务每一次请求,失败就如实报错。签到与测试是手动动作,两种状态下都可用 —— 这个开关只管「插件要不要自己决定」。",
9610
+ "row.poolEnabledHint": "开启:由账号池为每次请求挑号,失败就换下一个。关闭:你选的账号服务每一次请求,失败如实报错。",
9611
+ "row.poolModeAuto": "自动挑号",
9612
+ "row.poolModeManual": "手动选号",
9097
9613
  "row.poolManualAccountLabel": "使用哪个账号",
9098
- "row.poolManualAccountHint": "账号池关闭时,由你在这里选定服务本区域的账号。开启账号池后改由排序自动决定。",
9099
9614
  "row.poolManualAccountNone": "当前没有生效的账号",
9100
- "row.poolTargetLabel": "目标测试模型",
9101
- "row.poolTargetHint": "「自动」(默认)会从本区域目录里挑一个倍率为 0 的免费模型,多个候选取上下文窗口最大的。国内版与国际版可能挑到不同的模型。手动指定优先于自动。",
9615
+ "row.poolTargetLabel": "测试用模型(全池统一)",
9616
+ "row.poolTargetHint": "全池共用这一个模型,测试就是拿它逐个去测。「自动」会从本区域目录里挑一个倍率为 0 的免费模型,多个候选取上下文窗口最大的;手动指定优先于自动。",
9102
9617
  "row.poolTargetAutoOption": "自动 —— 挑一个免费模型",
9103
9618
  "row.poolProbeSizeLabel": "测试请求体积",
9104
9619
  "row.poolProbeSizeHint": "测试时发送多少文本。上游在请求大约超过 2 万~3 万 token 时会开始拒绝,所以测试体积若小于你真实对话的体积,就会出现「测出可用、实际一发就被限」。体积越大测得越准,但每次测试消耗的积分也越多。",
@@ -9126,7 +9641,7 @@ window.__ModuleLoader__.load({
9126
9641
  "row.poolLogTestRowMalformed": "{accountName}:Host 未返回该账号的结果",
9127
9642
  "row.poolRegionNote": "国内版与国际版是完全独立的两个账号池:账号、开关、间隔、目标模型互不影响。",
9128
9643
  "row.poolMembersTitle": "本池包含的账号",
9129
- "row.poolMembersHint": "只有勾选的账号才会被签到、测试和轮换。默认一个都不勾 —— 请勾选你要的账号,然后保存。",
9644
+ "row.poolMembersHint": "只有勾选的账号参与轮换、签到与测试。",
9130
9645
  "row.poolSelectAll": "全选",
9131
9646
  "row.poolSelectNone": "全不选",
9132
9647
  "row.poolSelectedCount": "已选 {count} / {total}",
@@ -9143,17 +9658,18 @@ window.__ModuleLoader__.load({
9143
9658
  "row.poolOrderWhy": "为什么临期积分压过手动顺序:临期积分是马上要过期的钱,而手动顺序可能已经过时、也可能是手滑拖错了。如果让手动顺序赢,一次误拖就足以让积分白白过期。所以有 3 天内到期积分的账号,永远排在没有的账号前面 —— 哪怕你把它拖到了最后一位。",
9144
9659
  "row.poolOrderBadgeNote": "徽标显示的是你「保存」的位置,不是此刻的实际排序。一个账号可能写着「第 2 位」却因为被限流而暂时不被选中 —— 那个数字代表你的偏好,池子会跳过当下无法扣费的账号。",
9145
9660
  "row.poolOrderRulesToggle": "查看完整排序规则",
9146
- "row.poolNoneSelected": "这个池里还没有勾选任何账号。",
9147
- "row.poolNoneSelectedHint": "请在上方勾选至少一个账号并保存。在此之前两个按钮都不可用 —— 空池没有可执行的对象。",
9661
+ "row.poolNoneSelected": "还没有勾选任何账号。",
9662
+ "row.poolNoneSelectedHint": "勾选至少一个账号,才能签到与测试。",
9148
9663
  "row.poolGhostMembers": "有 {count} 个已选入本池的账号在本机已不再登录,因此不计入、也不会被使用:{names}。不需要的可在下方逐个移除。",
9149
9664
  "row.poolGhostRemove": "把 {account} 移出本池",
9150
9665
  "row.poolGhostRemoveNamed": "把 {account} 移出本池。该账号在本机已不再登录。",
9151
9666
  "row.poolGhostRemoveUnnamed": "把 {account} 移出本池。这个账号从未记录过名字,显示的是它 id 的开头几位。",
9152
9667
  "row.poolSelectAria": "把 {accountName} 加入本区域的账号池",
9153
- "row.poolMember": "已在池中",
9154
9668
  "row.poolCurrentBadge": "当前使用",
9155
9669
  "row.poolCurrentHeader": "当前使用:{account}",
9156
9670
  "row.poolNotMember": "未入池",
9671
+ "row.poolNotMemberHint": "不在池里:不参与轮换,也不进批量签到与测试。",
9672
+ "row.poolServingOutside": "「{account}」正在使用,但不在这个池里:轮换不会挑它,批量签到与测试也不会动它。",
9157
9673
  "row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
9158
9674
  "row.poolRankLabel": "第 {rank} 位",
9159
9675
  "row.poolRankManualTitle": "你在本池中手动把该账号排在第 {rank} 位。",
@@ -9211,7 +9727,15 @@ window.__ModuleLoader__.load({
9211
9727
  "sidebar.refreshing": "刷新中…",
9212
9728
  "sidebar.lastRefresh": "最近刷新 {time}",
9213
9729
  "sidebar.refreshFailed": "刷新失败",
9214
- "sidebar.signedOut": "未登录"
9730
+ "sidebar.signedOut": "未登录",
9731
+ "cliLive.running": "CodeBuddy",
9732
+ "cliLive.toolCalls": "{count} 次工具",
9733
+ "cliLive.elapsed": "{seconds} 秒",
9734
+ "cliLive.starting": "启动中…",
9735
+ "cliLive.finishing": "收尾中…",
9736
+ "cliLive.lastTool": "最近:{tool}",
9737
+ "cliLive.chars": "{count} 字",
9738
+ "cliLive.aria": "有一个 CodeBuddy 回合正在跑:{detail}"
9215
9739
  };
9216
9740
  //#endregion
9217
9741
  //#region src/client/index.tsx
@@ -9391,6 +9915,42 @@ window.__ModuleLoader__.load({
9391
9915
  }
9392
9916
  };
9393
9917
  /**
9918
+ * The live CodeBuddy turn pill, in the same ambient dock as the readout.
9919
+ *
9920
+ * `order` 11 puts it immediately after the credit readout (10) and still
9921
+ * right of the shell's own pills (0): the two numbers a user glances at while
9922
+ * a turn runs sit together. The component renders `null` whenever no turn is
9923
+ * live, which is the normal case — the bridge is off by default — so an
9924
+ * unused seat costs one poll interval and nothing on screen.
9925
+ *
9926
+ * THE SESSION ID IS THE PILL'S DISPLAY CONDITION. `conversation.composer.dock`
9927
+ * is declared `scope: 'session'`, so the registration's `inject` receives the
9928
+ * session id and the pill can keep only the rows that session started. The
9929
+ * board itself is host-wide (one route for every session in the process), so
9930
+ * without this the pill would describe whatever turn is newest anywhere —
9931
+ * including one from another tab, about a directory the reader is not in.
9932
+ * Unlike the credit readout there is deliberately no provider gate: the CLI
9933
+ * route is model-independent, and a turn started from a DeepSeek session is
9934
+ * exactly a case the readout has to cover.
9935
+ */
9936
+ const registerCliLive = () => {
9937
+ try {
9938
+ return ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
9939
+ name: "conversation.composer.dock",
9940
+ id: "dsh-connect-workbuddy-cli-live",
9941
+ order: 11,
9942
+ locale: namespace,
9943
+ inject: (sessionId) => ({
9944
+ t,
9945
+ sessionId
9946
+ })
9947
+ }, CodebuddyLive));
9948
+ } catch (error) {
9949
+ console.error("[dsh-connect-workbuddy] cli live slot failed to register (host provider unaffected):", error);
9950
+ return;
9951
+ }
9952
+ };
9953
+ /**
9394
9954
  * The frame-wide quota notice, DECLINED so the host keeps its own Toast
9395
9955
  * (issue #63 follow-up, the maintainer's call on a screenshot).
9396
9956
  *
@@ -9454,6 +10014,7 @@ window.__ModuleLoader__.load({
9454
10014
  ...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.row.config", `${key}#${key}`)),
9455
10015
  registerComposerCredits(),
9456
10016
  registerStatsCredits(),
10017
+ registerCliLive(),
9457
10018
  registerQuotaNotice(),
9458
10019
  registerModelLabel()
9459
10020
  ].filter((disposer) => disposer !== void 0);