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/CHANGELOG.md +81 -0
- package/README.en.md +25 -9
- package/README.md +29 -12
- package/docs/WINDOWS.md +1 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +716 -155
- package/lib/{host-heartbeat-D5FWM_8k.js → host-heartbeat-CVnH8S3J.js} +11 -1
- package/lib/index.d.ts +27 -0
- package/lib/index.js +432 -131
- package/package.json +1 -1
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
|
|
189
|
-
|
|
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 === "
|
|
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
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
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))))
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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
|
-
},
|
|
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)("
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
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:
|
|
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
|
-
})()),
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
/*
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
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
|
-
/*
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
.dsm-workbuddy-pool-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap
|
|
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-
|
|
4696
|
-
.dsm-workbuddy-pool-note{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-
|
|
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);
|
|
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
|
-
|
|
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
|
-
/*
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
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
|
-
|
|
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 —
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
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
|
|
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
|
|
8284
|
-
* shipped example is the official DeepSeek "1017" release badge — a
|
|
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
|
|
8290
|
-
*
|
|
8291
|
-
*
|
|
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
|
|
8296
|
-
*
|
|
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
|
|
8500
|
+
/** How long a fetched snapshot is trusted, in milliseconds. */
|
|
8312
8501
|
const CACHE_MS = 6e4;
|
|
8313
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
8332
|
-
|
|
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
|
-
|
|
8530
|
+
snapshot,
|
|
8335
8531
|
at: Date.now()
|
|
8336
8532
|
});
|
|
8337
|
-
return
|
|
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,
|
|
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
|
|
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 [
|
|
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
|
-
|
|
8365
|
-
if (!live ||
|
|
8366
|
-
|
|
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: [
|
|
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
|
|
9167
|
+
"row.poolCheckinAll": "Check in selected",
|
|
8666
9168
|
"row.poolCheckingIn": "Checking in…",
|
|
8667
|
-
"row.poolTestAll": "Test
|
|
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": "
|
|
9187
|
+
"row.poolColumnProbe": "Status",
|
|
8686
9188
|
"row.poolColumnCheckin": "Check-in",
|
|
8687
|
-
"row.poolNeverTested": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
8735
|
-
"row.poolTargetHint": "Auto
|
|
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
|
|
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
|
|
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": "
|
|
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);
|