dsh-connect-workbuddy 5.4.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -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 +879 -159
- package/lib/{host-heartbeat-D5FWM_8k.js → host-heartbeat-BhgyeZWV.js} +11 -1
- package/lib/index.d.ts +79 -1
- package/lib/index.js +691 -150
- 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,178 @@ 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.
|
|
207
|
+
*
|
|
208
|
+
* THE RESULT IS HEALED, not merely narrowed — see {@link healedRegionsMap}.
|
|
209
|
+
* Returning the map "as-is" is what let a corrupted document keep reading as
|
|
210
|
+
* half-empty: the field held `cn` at level 0 and `global` only one level down,
|
|
211
|
+
* so every `global` question answered "never configured" even though the file
|
|
212
|
+
* still had the answer. Reads and writes both come through here, so healing
|
|
213
|
+
* here fixes both halves at once (the Host's `regionStateOf`, the card's
|
|
214
|
+
* `nextRegion*` write bases, and the catalog repair's write).
|
|
182
215
|
*/
|
|
183
216
|
function regionsMapOf(value) {
|
|
217
|
+
const peeled = peelLiveReference(value);
|
|
218
|
+
if (typeof peeled === "object" && peeled !== null && !Array.isArray(peeled)) {
|
|
219
|
+
const candidate = peeled;
|
|
220
|
+
if (!isRecord(candidate["regions"]) && !carriesForeignKey(candidate)) return candidate;
|
|
221
|
+
}
|
|
184
222
|
const unwrapped = unwrapVolatileDeep(value);
|
|
185
223
|
if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
|
|
186
224
|
const record = unwrapped;
|
|
187
225
|
const nested = record["regions"];
|
|
188
|
-
if (typeof nested
|
|
189
|
-
return record;
|
|
226
|
+
if (typeof nested !== "object" || nested === null || Array.isArray(nested)) return healedRegionsMap(record);
|
|
227
|
+
return healedRegionsMap(hasRegionKey(record) ? record : nested);
|
|
228
|
+
}
|
|
229
|
+
/** `value` with ONE `{get(): T}` live reference peeled, exactly like a read. */
|
|
230
|
+
function peelLiveReference(value) {
|
|
231
|
+
if (value === null || typeof value !== "object") return value;
|
|
232
|
+
const get = value.get;
|
|
233
|
+
return typeof get === "function" ? get() : value;
|
|
234
|
+
}
|
|
235
|
+
/** Whether the object carries a sibling-field name where a region id belongs. */
|
|
236
|
+
function carriesForeignKey(record) {
|
|
237
|
+
return Object.keys(record).some((key) => REGION_MAP_FOREIGN_KEYS.has(key));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Names that belong to the settings SECTION (`Config` in `src/index.ts`) or to
|
|
241
|
+
* one region SLOT (`regionStateConfig`) — never to the `regions` map.
|
|
242
|
+
*
|
|
243
|
+
* They are the signature of a data-loss bug and the list that undoes it. From
|
|
244
|
+
* 5.4.0 to 5.5.0 `mergeRegionCatalog` was handed the WHOLE settings section but
|
|
245
|
+
* read it as the `regions` field, so every boot's directory repair wrote
|
|
246
|
+
* `{ accounts, regions, cliBridge }` INTO the field — replacing the region map
|
|
247
|
+
* and burying the real one under a `regions` key. A live profile ended up five
|
|
248
|
+
* levels deep (one per startup) and 258 KB of duplicated slots.
|
|
249
|
+
*
|
|
250
|
+
* Neither a region id (`cn` / `global`) nor any future one can collide with
|
|
251
|
+
* these names, so removing them from the map is safe, idempotent, and does not
|
|
252
|
+
* discard a region that a newer build might add.
|
|
253
|
+
*/
|
|
254
|
+
const REGION_MAP_FOREIGN_KEYS = /* @__PURE__ */ new Set([
|
|
255
|
+
"authFile",
|
|
256
|
+
"accountId",
|
|
257
|
+
"accounts",
|
|
258
|
+
"regions",
|
|
259
|
+
"cliBridge",
|
|
260
|
+
"enabled",
|
|
261
|
+
"showCreditsInMainUi",
|
|
262
|
+
"creditPosition",
|
|
263
|
+
"lastCatalog",
|
|
264
|
+
"enabledModelIds",
|
|
265
|
+
"imageModelIds",
|
|
266
|
+
"imageToolEnabled",
|
|
267
|
+
"offModelIds",
|
|
268
|
+
"contextBudgets",
|
|
269
|
+
"pool"
|
|
270
|
+
]);
|
|
271
|
+
/** A non-null, non-array object: the only shape a slot or a level can have. */
|
|
272
|
+
function isRecord(value) {
|
|
273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
274
|
+
}
|
|
275
|
+
/** Whether a map carries at least one real region id. */
|
|
276
|
+
function hasRegionKey(map) {
|
|
277
|
+
return Object.keys(map).some((key) => WORKBUDDY_REGIONS.includes(key));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* How much USER CONFIG one candidate slot actually carries.
|
|
281
|
+
*
|
|
282
|
+
* Used only when the map lost a region and the copies left at different depths
|
|
283
|
+
* disagree — and they do: the bug's own directory repair wrote a slot that held
|
|
284
|
+
* nothing but `lastCatalog` (it started from a map where the region was already
|
|
285
|
+
* missing), while the copy further down still holds the pool the user had
|
|
286
|
+
* checked in. "Shallowest wins" would therefore resurrect the EMPTY one and
|
|
287
|
+
* throw away the real configuration. Depth is not age in this wreckage; CONTENT
|
|
288
|
+
* is the only usable signal.
|
|
289
|
+
*
|
|
290
|
+
* Deliberately counts only non-default, user-authored values: resolved slots
|
|
291
|
+
* carry every schema default, so a slot's key count says nothing.
|
|
292
|
+
*/
|
|
293
|
+
function slotConfigWeight(slot) {
|
|
294
|
+
let weight = 0;
|
|
295
|
+
const pool = slot["pool"];
|
|
296
|
+
if (isRecord(pool)) {
|
|
297
|
+
if (pool["enabled"] === true) weight += 4;
|
|
298
|
+
if (Array.isArray(pool["memberAccountIds"]) && pool["memberAccountIds"].length > 0) weight += 4;
|
|
299
|
+
if (Array.isArray(pool["orderAccountIds"]) && pool["orderAccountIds"].length > 0) weight += 2;
|
|
300
|
+
if (typeof pool["targetModelId"] === "string" && pool["targetModelId"] !== "") weight += 2;
|
|
301
|
+
if (typeof pool["probeInputTokens"] === "number" && pool["probeInputTokens"] > 0) weight += 1;
|
|
302
|
+
}
|
|
303
|
+
for (const key of [
|
|
304
|
+
"enabledModelIds",
|
|
305
|
+
"imageModelIds",
|
|
306
|
+
"offModelIds"
|
|
307
|
+
]) {
|
|
308
|
+
const list = slot[key];
|
|
309
|
+
if (Array.isArray(list) && list.length > 0) weight += 1;
|
|
310
|
+
}
|
|
311
|
+
const budgets = slot["contextBudgets"];
|
|
312
|
+
if (isRecord(budgets) && Object.keys(budgets).length > 0) weight += 1;
|
|
313
|
+
if (slot["enabled"] === false) weight += 1;
|
|
314
|
+
if (slot["showCreditsInMainUi"] === false) weight += 1;
|
|
315
|
+
if (slot["imageToolEnabled"] === true) weight += 1;
|
|
316
|
+
return weight;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The `regions` map with a corrupted document's wreckage removed.
|
|
320
|
+
*
|
|
321
|
+
* Two repairs, both keyed on the bug's signature (a foreign key or a nested
|
|
322
|
+
* `regions` copy) so a HEALTHY map passes through untouched:
|
|
323
|
+
*
|
|
324
|
+
* 1. DROP every key that names a section field or a slot field. Those are the
|
|
325
|
+
* copies the bug wrote; keeping them means the next save carries them
|
|
326
|
+
* forward forever.
|
|
327
|
+
* 2. RECOVER a region this level does not have from the copies buried beneath
|
|
328
|
+
* it. A region that IS here is never replaced — the outer copy is the one
|
|
329
|
+
* the surviving writers have been updating, and re-reading a stale inner
|
|
330
|
+
* copy over it is exactly the defect the shape rule above was fixed for.
|
|
331
|
+
*
|
|
332
|
+
* Recovery exists because `global` in the measured profile lived ONLY one level
|
|
333
|
+
* down: without it, the host reads that region as "never configured" and the
|
|
334
|
+
* first write after the fix would delete the user's international pool outright.
|
|
335
|
+
*
|
|
336
|
+
* Among the buried copies of a MISSING region, the one carrying the most user
|
|
337
|
+
* configuration wins rather than the shallowest — see {@link slotConfigWeight}:
|
|
338
|
+
* the bug's own directory repair left `{ lastCatalog }` slots that look newer
|
|
339
|
+
* but hold nothing, one level above the copy with the pool still in it.
|
|
340
|
+
*/
|
|
341
|
+
function healedRegionsMap(map) {
|
|
342
|
+
const healed = {};
|
|
343
|
+
for (const [key, slot] of Object.entries(map)) {
|
|
344
|
+
if (REGION_MAP_FOREIGN_KEYS.has(key)) continue;
|
|
345
|
+
healed[key] = slot;
|
|
346
|
+
}
|
|
347
|
+
const recovered = {};
|
|
348
|
+
let deeper = map;
|
|
349
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
350
|
+
const nested = deeper["regions"];
|
|
351
|
+
if (!isRecord(nested)) break;
|
|
352
|
+
for (const region of WORKBUDDY_REGIONS) {
|
|
353
|
+
if (region in map) continue;
|
|
354
|
+
const candidate = nested[region];
|
|
355
|
+
if (!isRecord(candidate)) continue;
|
|
356
|
+
const weight = slotConfigWeight(candidate);
|
|
357
|
+
const best = recovered[region];
|
|
358
|
+
if (best === void 0 || weight > best.weight) recovered[region] = {
|
|
359
|
+
slot: candidate,
|
|
360
|
+
weight
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
deeper = nested;
|
|
364
|
+
}
|
|
365
|
+
for (const [region, entry] of Object.entries(recovered)) healed[region] = entry.slot;
|
|
366
|
+
return healed;
|
|
190
367
|
}
|
|
191
368
|
/** One region's stored slot as a plain object; any other shape reads as empty. */
|
|
192
369
|
function regionSlotOf(value, region) {
|
|
@@ -511,7 +688,9 @@ window.__ModuleLoader__.load({
|
|
|
511
688
|
* value, so reading it wrong silently deleted the untouched region.
|
|
512
689
|
*/
|
|
513
690
|
function fieldSnapshotOf(scope, field) {
|
|
514
|
-
const
|
|
691
|
+
const stored = scope.getSnapshot().value?.[field];
|
|
692
|
+
if (field === "regions") return regionsMapOf(stored);
|
|
693
|
+
const value = unwrapVolatileDeep(stored);
|
|
515
694
|
return typeof value === "object" && value !== null ? value : {};
|
|
516
695
|
}
|
|
517
696
|
/**
|
|
@@ -858,12 +1037,18 @@ window.__ModuleLoader__.load({
|
|
|
858
1037
|
* `orderAccountIds` may be anything at all. Every shape that is not a list of
|
|
859
1038
|
* strings means "no manual order", which is also an older profile's spelling —
|
|
860
1039
|
* never a crash.
|
|
1040
|
+
*
|
|
1041
|
+
* The map comes from `regionsMapOf` rather than a bare `value.regions` lookup:
|
|
1042
|
+
* it accepts the section OR the field and heals a field an older build
|
|
1043
|
+
* corrupted (the "restart reset" report). That is not cosmetic here — on the
|
|
1044
|
+
* measured profile the INTERNATIONAL slot survived only one level down, so a
|
|
1045
|
+
* direct lookup reported "no manual order" for the region whose order the user
|
|
1046
|
+
* had actually saved, and the credit panel listed those accounts in the Host's
|
|
1047
|
+
* order instead.
|
|
861
1048
|
*/
|
|
862
1049
|
function readOrderAccountIds(value, region) {
|
|
863
1050
|
if (region === void 0) return [];
|
|
864
|
-
const
|
|
865
|
-
if (typeof regions !== "object" || regions === null) return [];
|
|
866
|
-
const slot = regions[region];
|
|
1051
|
+
const slot = regionsMapOf(value)[region];
|
|
867
1052
|
if (typeof slot !== "object" || slot === null) return [];
|
|
868
1053
|
const order = slot.pool;
|
|
869
1054
|
if (typeof order !== "object" || order === null) return [];
|
|
@@ -2726,7 +2911,7 @@ window.__ModuleLoader__.load({
|
|
|
2726
2911
|
* Returns null when there is nothing to say, so the caller can fall back to the
|
|
2727
2912
|
* static explanation instead of rendering an empty "current:" prefix.
|
|
2728
2913
|
*/
|
|
2729
|
-
const targetProvenance = pool.targetModelSource === "
|
|
2914
|
+
const targetProvenance = pool.targetModelSource === "free" ? t("row.poolTargetCurrentFree", { model: pool.targetModelId ?? "" }) : null;
|
|
2730
2915
|
/**
|
|
2731
2916
|
* The account the next request will start from.
|
|
2732
2917
|
*
|
|
@@ -2736,15 +2921,19 @@ window.__ModuleLoader__.load({
|
|
|
2736
2921
|
*/
|
|
2737
2922
|
const currentAccount = pool.accounts.find((account) => account.current);
|
|
2738
2923
|
const currentName = currentAccount === void 0 ? void 0 : currentAccount.accountName === "" ? t("row.accountUnnamed") : currentAccount.accountName;
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2924
|
+
/**
|
|
2925
|
+
* The manual-mode picker: shown ONLY with the pool off, and it is the whole of
|
|
2926
|
+
* "manual mode" — with the pool off nothing else chooses who is billed.
|
|
2927
|
+
*
|
|
2928
|
+
* It lives in the HEAD line, sharing one slot with the 「current: X」 badge,
|
|
2929
|
+
* instead of in a row of its own further down. That row said the same account
|
|
2930
|
+
* name for the third time on one screen (badge, picker, member row), and the
|
|
2931
|
+
* repetition is what made the page read as generated rather than designed.
|
|
2932
|
+
* The two states are the two halves of one question — "who serves?" — so they
|
|
2933
|
+
* get one place to answer it: a picker when the answer is yours, a badge when
|
|
2934
|
+
* the pool decides.
|
|
2935
|
+
*/
|
|
2936
|
+
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
2937
|
className: "dsm-workbuddy-pool-select dsm-workbuddy-pool-account-select",
|
|
2749
2938
|
value: currentAccount?.accountId ?? "",
|
|
2750
2939
|
disabled: selectingAccount === true,
|
|
@@ -2757,27 +2946,45 @@ window.__ModuleLoader__.load({
|
|
|
2757
2946
|
}, t("row.poolManualAccountNone")), ...pool.accounts.map((account) => (0, react.createElement)("option", {
|
|
2758
2947
|
key: account.accountId,
|
|
2759
2948
|
value: account.accountId
|
|
2760
|
-
}, account.accountName === "" ? t("row.accountUnnamed") : account.accountName))))
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2949
|
+
}, account.accountName === "" ? t("row.accountUnnamed") : account.accountName)))) : null;
|
|
2950
|
+
/**
|
|
2951
|
+
* The full ordering rule, collapsed — pure reference text.
|
|
2952
|
+
*
|
|
2953
|
+
* It used to sit between 「本池包含的账号」 and the first account row, which put
|
|
2954
|
+
* a paragraph of explanation ahead of the accounts it explains and pushed the
|
|
2955
|
+
* table down. Reference text belongs BELOW the data it describes; here it is,
|
|
2956
|
+
* at the top of 「更多设置」, next to the controls it explains.
|
|
2957
|
+
*
|
|
2958
|
+
* A <details>/<summary> pair rather than a stateful button: it is
|
|
2959
|
+
* keyboard-operable and screen-reader-expanded natively, and cannot fall out
|
|
2960
|
+
* of sync with what is on screen. `open` is controlled so a background refresh
|
|
2961
|
+
* cannot collapse a section the user just opened.
|
|
2962
|
+
*/
|
|
2963
|
+
const orderRules = (0, react.createElement)("details", {
|
|
2964
|
+
className: "dsm-workbuddy-pool-order-rules",
|
|
2965
|
+
open: orderRulesOpen,
|
|
2966
|
+
onToggle: (event) => setOrderRulesOpen(event.currentTarget.open)
|
|
2967
|
+
}, (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"))));
|
|
2968
|
+
return (0, react.createElement)("section", { className: "dsm-workbuddy-pool" }, rowBusyAction === "remove" ? (0, react.createElement)(BusyOverlay, {
|
|
2969
|
+
visible: true,
|
|
2970
|
+
message: t("row.poolRowRemoving"),
|
|
2971
|
+
hint: t("row.veilHintList")
|
|
2972
|
+
}) : null, saving ? (0, react.createElement)(BusyOverlay, {
|
|
2973
|
+
visible: true,
|
|
2974
|
+
message: t("row.poolSaving"),
|
|
2975
|
+
hint: t("row.veilHintList")
|
|
2976
|
+
}) : 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", {
|
|
2977
|
+
type: "checkbox",
|
|
2978
|
+
checked: active.enabled,
|
|
2979
|
+
disabled: !canEditPool,
|
|
2980
|
+
onChange: (event) => {
|
|
2981
|
+
const next = event.currentTarget.checked;
|
|
2982
|
+
editDraft((current) => ({
|
|
2983
|
+
...current,
|
|
2984
|
+
enabled: next
|
|
2985
|
+
}));
|
|
2774
2986
|
}
|
|
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", {
|
|
2987
|
+
}), (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
2988
|
className: "dsm-workbuddy-pool-warn",
|
|
2782
2989
|
role: "status"
|
|
2783
2990
|
}, `${t("row.poolModelWideBusy")} ${t("row.poolModelWideBusyHint")}`) : (0, react.createElement)("p", {
|
|
@@ -2796,11 +3003,27 @@ window.__ModuleLoader__.load({
|
|
|
2796
3003
|
className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
|
|
2797
3004
|
disabled: effectiveMembers.length === 0 || !canEditPool,
|
|
2798
3005
|
onClick: () => selectAll(false)
|
|
2799
|
-
}, t("row.poolSelectNone")))), (0, react.createElement)("
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
3006
|
+
}, 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", {
|
|
3007
|
+
type: "button",
|
|
3008
|
+
className: "dsm-btn dsm-btn-primary",
|
|
3009
|
+
disabled: busy !== void 0 || effectiveMembers.length === 0,
|
|
3010
|
+
onClick: () => {
|
|
3011
|
+
runAction("checkin");
|
|
3012
|
+
}
|
|
3013
|
+
}, busy === "checkin" ? t("row.poolCheckingIn") : t("row.poolCheckinAll")) : null, (0, react.createElement)("button", {
|
|
3014
|
+
type: "button",
|
|
3015
|
+
className: "dsm-btn dsm-btn-outline",
|
|
3016
|
+
disabled: busy !== void 0 || effectiveMembers.length === 0 || pool.targetModelSource === "none" || pool.targetModelSource === "stale",
|
|
3017
|
+
title: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
|
|
3018
|
+
onClick: () => {
|
|
3019
|
+
runAction("test");
|
|
3020
|
+
}
|
|
3021
|
+
}, 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", {
|
|
3022
|
+
type: "button",
|
|
3023
|
+
className: "dsm-btn dsm-btn-outline",
|
|
3024
|
+
disabled: rescanning === true,
|
|
3025
|
+
onClick: onRescan
|
|
3026
|
+
}, 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
3027
|
const ordered = sortAccountsByOrder(pool.accounts, orderIds);
|
|
2805
3028
|
let memberSeen = 0;
|
|
2806
3029
|
let memberTotal = 0;
|
|
@@ -2824,6 +3047,7 @@ window.__ModuleLoader__.load({
|
|
|
2824
3047
|
claimBusy: rowBusyAccountId === account.accountId,
|
|
2825
3048
|
testDisabled: pool.targetModelSource === "none" || pool.targetModelSource === "stale",
|
|
2826
3049
|
targetHint: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
|
|
3050
|
+
poolModelId: pool.targetModelId ?? "",
|
|
2827
3051
|
onTest: () => {
|
|
2828
3052
|
runRowTest(account.accountId);
|
|
2829
3053
|
},
|
|
@@ -2835,7 +3059,7 @@ window.__ModuleLoader__.load({
|
|
|
2835
3059
|
},
|
|
2836
3060
|
orderIndex: isMember && orderIsManual ? left + 1 : void 0,
|
|
2837
3061
|
orderIsManual,
|
|
2838
|
-
moveEnabled:
|
|
3062
|
+
moveEnabled: active.enabled && isMember && canEditPool,
|
|
2839
3063
|
onMoveUp: () => {
|
|
2840
3064
|
moveMember(account.accountId, -1);
|
|
2841
3065
|
},
|
|
@@ -2846,10 +3070,7 @@ window.__ModuleLoader__.load({
|
|
|
2846
3070
|
moveDownDisabled: right <= 0
|
|
2847
3071
|
});
|
|
2848
3072
|
});
|
|
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", {
|
|
3073
|
+
})()), ghostMembers$1.length === 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-ghosts" }, (0, react.createElement)("p", {
|
|
2853
3074
|
className: "dsm-workbuddy-pool-warn",
|
|
2854
3075
|
role: "status"
|
|
2855
3076
|
}, t("row.poolGhostMembers", {
|
|
@@ -2899,6 +3120,7 @@ window.__ModuleLoader__.load({
|
|
|
2899
3120
|
onDiscard: discard,
|
|
2900
3121
|
appendLog,
|
|
2901
3122
|
targetProvenance,
|
|
3123
|
+
orderRules,
|
|
2902
3124
|
...accountTools === void 0 ? {} : { accountTools }
|
|
2903
3125
|
}), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolRegionNote")), renderLog({
|
|
2904
3126
|
t,
|
|
@@ -2912,7 +3134,7 @@ window.__ModuleLoader__.load({
|
|
|
2912
3134
|
}
|
|
2913
3135
|
/** One account's row. */
|
|
2914
3136
|
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;
|
|
3137
|
+
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
3138
|
const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
|
|
2917
3139
|
const excluded = exclusionText(t, account.excludedBy);
|
|
2918
3140
|
const probe = account.probe;
|
|
@@ -2962,7 +3184,7 @@ window.__ModuleLoader__.load({
|
|
|
2962
3184
|
"aria-label": t("row.poolMoveDownAria", { accountName: name }),
|
|
2963
3185
|
disabled: moveDownDisabled,
|
|
2964
3186
|
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:
|
|
3187
|
+
}, "↓")) : 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
3188
|
key: benefit.kind,
|
|
2967
3189
|
className: benefit.urgent ? "dsm-workbuddy-pool-benefit dsm-workbuddy-pool-benefit-urgent" : "dsm-workbuddy-pool-benefit"
|
|
2968
3190
|
}, 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 +3210,7 @@ window.__ModuleLoader__.load({
|
|
|
2988
3210
|
}, (0, react.createElement)("span", null, claimBusy && rowBusyAction === "claim" ? t("row.poolBenefitClaiming") : t("row.poolBenefitClaim"))))))), (0, react.createElement)("span", {
|
|
2989
3211
|
className: "dsm-workbuddy-pool-probe",
|
|
2990
3212
|
...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
|
|
3213
|
+
}, (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
3214
|
className: "dsm-workbuddy-pool-unknown",
|
|
2993
3215
|
title: t("row.poolNeverTested")
|
|
2994
3216
|
}, 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 +3230,9 @@ window.__ModuleLoader__.load({
|
|
|
3008
3230
|
}
|
|
3009
3231
|
/** The preference editor plus its save/discard actions. */
|
|
3010
3232
|
function renderSettings(input) {
|
|
3011
|
-
const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance, accountTools } = input;
|
|
3233
|
+
const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance, accountTools, orderRules } = input;
|
|
3012
3234
|
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", {
|
|
3235
|
+
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
3236
|
className: "dsm-workbuddy-pool-select",
|
|
3024
3237
|
value: active.targetModelId,
|
|
3025
3238
|
disabled: !canEdit,
|
|
@@ -3504,10 +3717,6 @@ window.__ModuleLoader__.load({
|
|
|
3504
3717
|
message: t("row.oauthSyncing"),
|
|
3505
3718
|
hint: t("row.veilHintList")
|
|
3506
3719
|
}),
|
|
3507
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3508
|
-
className: "dsm-workbuddy-bar-label",
|
|
3509
|
-
children: t(titleKey)
|
|
3510
|
-
}),
|
|
3511
3720
|
phase.kind === "finished" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3512
3721
|
className: `dsm-workbuddy-bar-status ${phase.message === "added" ? "dsm-workbuddy-bar-ok" : "dsm-workbuddy-bar-error"}`,
|
|
3513
3722
|
children: finishText
|
|
@@ -3810,10 +4019,6 @@ window.__ModuleLoader__.load({
|
|
|
3810
4019
|
message: veilMessage,
|
|
3811
4020
|
...busy ? { hint: t("row.veilHintList") } : {}
|
|
3812
4021
|
}),
|
|
3813
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3814
|
-
className: "dsm-workbuddy-bar-label",
|
|
3815
|
-
children: t("row.transferTitle")
|
|
3816
|
-
}),
|
|
3817
4022
|
phase.kind === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3818
4023
|
className: "dsm-workbuddy-bar-status dsm-workbuddy-bar-ok",
|
|
3819
4024
|
children: phase.message
|
|
@@ -4200,6 +4405,20 @@ window.__ModuleLoader__.load({
|
|
|
4200
4405
|
return badge.label;
|
|
4201
4406
|
}
|
|
4202
4407
|
/**
|
|
4408
|
+
* The badge's wording where the chip has to fit a narrow row.
|
|
4409
|
+
*
|
|
4410
|
+
* The model picker's rows are one line wide and already carry the model name,
|
|
4411
|
+
* the credit rate and the load icon, so a four-character label ("夜间折扣") can
|
|
4412
|
+
* push the check mark out of the row. The upstream publishes a compact wording
|
|
4413
|
+
* for exactly this case (`shortLabel`), and when it does not, the full label is
|
|
4414
|
+
* still better than a cropped one: an ellipsis in a menu row reads as a
|
|
4415
|
+
* truncated word, while the full label is at worst tight.
|
|
4416
|
+
*/
|
|
4417
|
+
function promoBadgeShortLabel(promo, language) {
|
|
4418
|
+
const badge = promo.badge;
|
|
4419
|
+
return (isEnglish(language) ? badge.shortLabelEn ?? badge.shortLabel : badge.shortLabel) ?? promoBadgeLabel(promo, language);
|
|
4420
|
+
}
|
|
4421
|
+
/**
|
|
4203
4422
|
* The chip's colour, when the upstream published a usable one.
|
|
4204
4423
|
*
|
|
4205
4424
|
* Only a hex colour is accepted: the value reaches an inline style, and the
|
|
@@ -4560,6 +4779,11 @@ window.__ModuleLoader__.load({
|
|
|
4560
4779
|
scrolling to the end of a long model list. */
|
|
4561
4780
|
.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
4781
|
.dsm-workbuddy-models-summary{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
|
|
4782
|
+
/* The one grey line that states what a section is FOR, at the top of a panel.
|
|
4783
|
+
Kept as its own class instead of reusing the models-summary class: the same
|
|
4784
|
+
rule now heads the capability section, whose vocabulary has nothing to do with
|
|
4785
|
+
models, and a shared name would send the next reader to the wrong panel. */
|
|
4786
|
+
.dsm-workbuddy-panel-note{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
|
|
4563
4787
|
/* The free-model quota warning (see the tabFreeQuotaWarn locale key). Bold on
|
|
4564
4788
|
purpose, and it keeps the summary's own grey rather than the primary label
|
|
4565
4789
|
colour: it has to stand out from the three lines around it WITHOUT reading as
|
|
@@ -4602,7 +4826,15 @@ window.__ModuleLoader__.load({
|
|
|
4602
4826
|
one the brand colour stands in. "flex:none" keeps the badge whole while the
|
|
4603
4827
|
model name beside it truncates with an ellipsis. The plain background comes
|
|
4604
4828
|
first so an engine without color-mix still gets a readable chip. */
|
|
4605
|
-
.dsm-workbuddy-model-label{display:inline-flex;align-items:center;gap:
|
|
4829
|
+
.dsm-workbuddy-model-label{display:inline-flex;align-items:center;gap:5px;white-space:nowrap;vertical-align:baseline}
|
|
4830
|
+
/* The same chip, inside the HOST's picker. A model row there is one line wide
|
|
4831
|
+
and already carries the name, the check mark and the load icon, so the chip
|
|
4832
|
+
gets a hard ceiling and the TEXT inside it truncates — the chip itself must
|
|
4833
|
+
stay whole, because a half-drawn pill reads as a rendering bug. "6em" at the
|
|
4834
|
+
chip's 10px is about six characters: enough for "夜间折扣", not enough for a
|
|
4835
|
+
label to push the row's own controls out of view. */
|
|
4836
|
+
.dsm-workbuddy-model-label-promo{max-width:6em;overflow:hidden}
|
|
4837
|
+
.dsm-workbuddy-model-label-promo-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4606
4838
|
.dsm-workbuddy-busy{flex:none;display:inline-flex;align-items:center;block-size:12px;color:var(--dsm-workbuddy-busy-color);vertical-align:-2px}
|
|
4607
4839
|
.dsm-workbuddy-busy svg{display:block;block-size:12px;inline-size:auto}
|
|
4608
4840
|
.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 +4911,26 @@ window.__ModuleLoader__.load({
|
|
|
4679
4911
|
.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
4912
|
/* Sits under the header block, before the batch buttons: account discovery is
|
|
4681
4913
|
the first step of using the pool, so it reads above the actions it enables. */
|
|
4682
|
-
/*
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4914
|
+
/* THE HEAD SLOT — one place that answers "who is serving?".
|
|
4915
|
+
*
|
|
4916
|
+
* With the pool off it holds the account picker, with the pool on the badge.
|
|
4917
|
+
* Both are pushed right by the same rule so the header does not jump between
|
|
4918
|
+
* the two states. The picker used to live in a settings row of its own further
|
|
4919
|
+
* down the page, which meant the same account name appeared in the title row,
|
|
4920
|
+
* in that row, and again in the member table. */
|
|
4921
|
+
.dsm-workbuddy-pool-head-pick{margin-left:auto;display:flex;align-items:center;gap:8px;font-size:12px}
|
|
4922
|
+
.dsm-workbuddy-pool-head-pick-label{color:var(--dsw-alias-label-secondary,#c6c9d0);white-space:nowrap}
|
|
4686
4923
|
.dsm-workbuddy-pool-account-select{min-width:200px}
|
|
4687
4924
|
.dsm-workbuddy-pool-title{margin:0;font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
4688
4925
|
.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
|
|
4926
|
+
/* No top rule: this bar now hangs directly under 「本池包含的账号 / 已选 N / M /
|
|
4927
|
+
全选 / 全不选」 and acts on exactly that set, so a separator here would cut the
|
|
4928
|
+
buttons away from the count that explains them. The divider that matters is
|
|
4929
|
+
the one the member block has against 「更多设置」 below. */
|
|
4930
|
+
.dsm-workbuddy-pool-actions{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
4694
4931
|
.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-
|
|
4932
|
+
.dsm-workbuddy-pool-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
4933
|
+
.dsm-workbuddy-pool-note{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
4697
4934
|
.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
4935
|
/* Ghost members (issue #60): the warning plus one removable chip per id. The
|
|
4699
4936
|
chips are what make the warning actionable — a ghost has no row in the table
|
|
@@ -4753,7 +4990,7 @@ window.__ModuleLoader__.load({
|
|
|
4753
4990
|
its built-in keyboard and screen-reader behaviour. The summary is a full-width
|
|
4754
4991
|
row because a 12px grey label on its own is a small click target, and this is
|
|
4755
4992
|
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);
|
|
4993
|
+
.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
4994
|
.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
4995
|
.dsm-workbuddy-pool-order-rules-summary:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
4759
4996
|
.dsm-workbuddy-pool-order-rules-summary:focus-visible{outline:2px solid rgba(86,134,254,.55);outline-offset:-2px}
|
|
@@ -4907,17 +5144,57 @@ window.__ModuleLoader__.load({
|
|
|
4907
5144
|
.dsm-workbuddy-pool-check input:disabled{cursor:default;opacity:.5}
|
|
4908
5145
|
.dsm-workbuddy-pool-checked{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
4909
5146
|
.dsm-workbuddy-pool-unknown{color:var(--dsw-alias-label-dimmed,#9aa0a6)}
|
|
4910
|
-
.dsm-workbuddy-pool-member{color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
4911
5147
|
.dsm-workbuddy-pool-not-member{color:var(--dsw-alias-label-dimmed,#9aa0a6)}
|
|
4912
|
-
|
|
5148
|
+
/* A section break, not just a gap: below this line the page stops showing what
|
|
5149
|
+
the pool IS and starts showing the knobs that change it. Without the rule the
|
|
5150
|
+
heading floated under the table with nothing marking the change of subject. */
|
|
5151
|
+
.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
5152
|
.dsm-workbuddy-pool-dirty{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
4914
5153
|
.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
|
-
|
|
5154
|
+
/* THE MODE BAR — the page's master control, directly under the title.
|
|
5155
|
+
*
|
|
5156
|
+
* It reuses the .dsm-workbuddy-pool-settings class for the bordered, stacked
|
|
5157
|
+
* look, and on purpose: the component tests locate 「启用账号池」 as the first
|
|
5158
|
+
* checkbox inside that class, and a user who has learned "settings live in a
|
|
5159
|
+
* bordered block" should not have to learn a second shape for the most
|
|
5160
|
+
* important one.
|
|
5161
|
+
*
|
|
5162
|
+
* What it adds is a brand-tinted edge and background, so it reads as "this
|
|
5163
|
+
* decides what the rest of the page means" instead of as one more settings
|
|
5164
|
+
* card — the switch used to sit at the BOTTOM, 400px below this point.
|
|
5165
|
+
*/
|
|
5166
|
+
.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)}
|
|
5167
|
+
.dsm-workbuddy-pool-modebar-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
5168
|
+
.dsm-workbuddy-pool-modebar-label{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
5169
|
+
/* The mode in words. A switch's position is a 20px shape to decode; this says
|
|
5170
|
+
which of the two modes is in force, and therefore how to read the table. */
|
|
5171
|
+
.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)}
|
|
5172
|
+
.dsm-workbuddy-pool-modebar .dsm-workbuddy-pool-dirty{margin-left:auto}
|
|
5173
|
+
.dsm-workbuddy-pool-modebar .dsm-workbuddy-pool-note{line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
5174
|
+
/* Orthogonal concepts in one sentence when they collide ("in use" vs "in the
|
|
5175
|
+
pool"). A left rule marks it as an aside about the row above, not a warning. */
|
|
5176
|
+
.dsm-workbuddy-pool-serving-note{padding-left:10px;border-left:2px solid rgba(86,134,254,.5);color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
5177
|
+
/* Subheading for "the rest of the settings": one step DOWN from the page title,
|
|
5178
|
+
so the panel has one title instead of two equally loud ones. */
|
|
5179
|
+
.dsm-workbuddy-pool-card-head{display:flex;align-items:center;gap:8px}
|
|
5180
|
+
.dsm-workbuddy-pool-card-title{margin:0;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
5181
|
+
/* QR sign-in + export/import, ABOVE the settings block.
|
|
5182
|
+
One toolbar row, not a column of two: with the captions gone (the buttons
|
|
5183
|
+
name themselves) there is nothing left to stack, and a row is where a user
|
|
5184
|
+
looks for instant actions. The dashed edge still separates them from the
|
|
5185
|
+
saved preferences below — that contrast is now the whole point, since the
|
|
5186
|
+
heading under this row is the one that says 「点保存才生效」.
|
|
5187
|
+
Left-aligned, not right: 扫码登录 is the primary action and it should read
|
|
5188
|
+
first, in the same left-hand column as 一键签到 above and 「更多设置」 below.
|
|
5189
|
+
Pushed to the right edge the three buttons looked like the tail of the row
|
|
5190
|
+
above rather than the start of this one. */
|
|
5191
|
+
.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}
|
|
5192
|
+
/* A bar inside the toolbar is a flex item, not a full-width row: it keeps only
|
|
5193
|
+
its own content width, and the toolbar decides where the group sits. */
|
|
5194
|
+
.dsm-workbuddy-pool-tools .dsm-workbuddy-bar{flex:0 1 auto;min-width:0;margin:0}
|
|
5195
|
+
/* Status text may wrap here instead of truncating: a toolbar row is short, and
|
|
5196
|
+
「已导出」「等待扫码」 are the whole message — an ellipsis would eat it. */
|
|
5197
|
+
.dsm-workbuddy-pool-tools .dsm-workbuddy-bar-status{white-space:normal}
|
|
4921
5198
|
.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
5199
|
.dsm-workbuddy-pool-set:first-child{border-top:0}
|
|
4923
5200
|
.dsm-workbuddy-pool-set-copy{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
|
|
@@ -4927,7 +5204,12 @@ window.__ModuleLoader__.load({
|
|
|
4927
5204
|
.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
5205
|
.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
5206
|
.dsm-workbuddy-pool-select:disabled,.dsm-workbuddy-pool-num:disabled{opacity:.5;cursor:default}
|
|
4930
|
-
|
|
5207
|
+
/* The commit row: pinned to the bottom of the viewport while the panel is on
|
|
5208
|
+
* screen. The panel is taller than a screen, and the two buttons that make any
|
|
5209
|
+
* edit real used to sit at its very bottom — off-screen at the moment the user
|
|
5210
|
+
* finished editing, and invisible entirely in a screenshot. Opaque, because
|
|
5211
|
+
* content scrolls under it. */
|
|
5212
|
+
.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
5213
|
.dsm-workbuddy-pool-save-buttons{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex:none}
|
|
4932
5214
|
.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
5215
|
.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 +5251,16 @@ window.__ModuleLoader__.load({
|
|
|
4969
5251
|
.dsm-workbuddy-pool .sw input:disabled+.sw-track{opacity:.4}
|
|
4970
5252
|
.dsm-workbuddy-pool .sw input:disabled{cursor:default}
|
|
4971
5253
|
/* Account action bars: QR sign-in and export/import.
|
|
4972
|
-
One row each —
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5254
|
+
One row each — live status, actions — instead of a padded block per feature.
|
|
5255
|
+
There is no caption: each bar holds buttons that name themselves, so a
|
|
5256
|
+
heading above them would repeat the button in a smaller font (「扫码添加账号」
|
|
5257
|
+
over 「扫码登录」). The section keeps an aria-label for the same reason a
|
|
5258
|
+
caption existed, so the name is still available to a screen reader.
|
|
5259
|
+
The bar is the resting height; only an open dialog (below) grows vertically,
|
|
5260
|
+
so the card does not scroll past a wall of chrome to reach the buttons.
|
|
5261
|
+
Status text shares the row rather than claiming its own, and is what
|
|
5262
|
+
truncates first when the card is narrow. */
|
|
4977
5263
|
.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
5264
|
.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
5265
|
.dsm-workbuddy-bar-hint{color:var(--dsw-alias-label-tertiary,#999)}
|
|
4981
5266
|
.dsm-workbuddy-bar-actions{display:flex;gap:8px;flex:none;margin-left:auto}
|
|
@@ -5225,6 +5510,32 @@ window.__ModuleLoader__.load({
|
|
|
5225
5510
|
}
|
|
5226
5511
|
.dsm-workbuddy-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
|
|
5227
5512
|
.dsm-workbuddy-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
|
|
5513
|
+
|
|
5514
|
+
/* The CLI live pill shares the composer dock row with the credit readout above
|
|
5515
|
+
and the shell's own ContextMeter, so it takes ITS metrics from the same place
|
|
5516
|
+
the credit trigger does: 12px/18px, label-tertiary, tabular numerals. Rendered
|
|
5517
|
+
with inline styles it inherited the dock's body font and read as stray text
|
|
5518
|
+
sitting next to two native-looking controls. */
|
|
5519
|
+
.dsm-workbuddy-cli-live{
|
|
5520
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
5521
|
+
padding:2px 6px;border-radius:6px;
|
|
5522
|
+
font-size:12px;line-height:18px;
|
|
5523
|
+
color:var(--dsw-alias-label-tertiary,#999);
|
|
5524
|
+
white-space:nowrap;font-variant-numeric:tabular-nums;
|
|
5525
|
+
}
|
|
5526
|
+
/* A quiet pulse is the whole signal that the turn is still moving: the label's
|
|
5527
|
+
second counter also advances, but at one tick a second it is easy to miss. */
|
|
5528
|
+
.dsm-workbuddy-cli-live-dot{
|
|
5529
|
+
width:6px;height:6px;border-radius:50%;flex:0 0 auto;
|
|
5530
|
+
background:var(--dsw-alias-brand-primary,#5686fe);
|
|
5531
|
+
animation:dsm-workbuddy-cli-live-pulse 1.6s ease-in-out infinite;
|
|
5532
|
+
}
|
|
5533
|
+
@keyframes dsm-workbuddy-cli-live-pulse{0%,100%{opacity:.3}50%{opacity:1}}
|
|
5534
|
+
/* Respect the OS setting: a blinking dot is decoration, the numbers are the
|
|
5535
|
+
information, and they keep updating with the animation off. */
|
|
5536
|
+
@media (prefers-reduced-motion:reduce){
|
|
5537
|
+
.dsm-workbuddy-cli-live-dot{animation:none;opacity:.8}
|
|
5538
|
+
}
|
|
5228
5539
|
`;
|
|
5229
5540
|
//#endregion
|
|
5230
5541
|
//#region src/client/searched-paths.ts
|
|
@@ -6871,6 +7182,18 @@ window.__ModuleLoader__.load({
|
|
|
6871
7182
|
},
|
|
6872
7183
|
children: t("row.panelModels")
|
|
6873
7184
|
}),
|
|
7185
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7186
|
+
type: "button",
|
|
7187
|
+
role: "tab",
|
|
7188
|
+
id: "dsm-workbuddy-tab-capabilities",
|
|
7189
|
+
"aria-controls": "dsm-workbuddy-panel-capabilities",
|
|
7190
|
+
"aria-selected": panel === "capabilities",
|
|
7191
|
+
className: `dsm-workbuddy-panel-tab${panel === "capabilities" ? " dsm-workbuddy-panel-tab-active" : ""}`,
|
|
7192
|
+
onClick: () => {
|
|
7193
|
+
setPanel("capabilities");
|
|
7194
|
+
},
|
|
7195
|
+
children: t("row.panelCapabilities")
|
|
7196
|
+
}),
|
|
6874
7197
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6875
7198
|
type: "button",
|
|
6876
7199
|
role: "tab",
|
|
@@ -6904,7 +7227,6 @@ window.__ModuleLoader__.load({
|
|
|
6904
7227
|
className: "dsm-workbuddy-tab-off-notice",
|
|
6905
7228
|
children: t("row.tabOffNotice")
|
|
6906
7229
|
}) : null,
|
|
6907
|
-
creditPreferences,
|
|
6908
7230
|
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
7231
|
className: "dsm-workbuddy-usage-hint",
|
|
6910
7232
|
children: t("row.reloginHint")
|
|
@@ -7077,11 +7399,7 @@ window.__ModuleLoader__.load({
|
|
|
7077
7399
|
className: "dsm-workbuddy-model-name",
|
|
7078
7400
|
children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7079
7401
|
className: "dsm-workbuddy-model-name-rate",
|
|
7080
|
-
children: [
|
|
7081
|
-
"(",
|
|
7082
|
-
model.creditMultiplier.toFixed(2),
|
|
7083
|
-
"x)"
|
|
7084
|
-
]
|
|
7402
|
+
children: ["x", model.creditMultiplier.toFixed(2)]
|
|
7085
7403
|
})]
|
|
7086
7404
|
}),
|
|
7087
7405
|
promoChip(model),
|
|
@@ -7259,6 +7577,17 @@ window.__ModuleLoader__.load({
|
|
|
7259
7577
|
})
|
|
7260
7578
|
})
|
|
7261
7579
|
}) : null,
|
|
7580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7581
|
+
className: "dsm-workbuddy-panel",
|
|
7582
|
+
role: "tabpanel",
|
|
7583
|
+
id: "dsm-workbuddy-panel-capabilities",
|
|
7584
|
+
"aria-labelledby": "dsm-workbuddy-tab-capabilities",
|
|
7585
|
+
hidden: signedIn && panel !== "capabilities",
|
|
7586
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7587
|
+
className: "dsm-workbuddy-panel-note",
|
|
7588
|
+
children: t("row.panelCapabilitiesHint")
|
|
7589
|
+
}), creditPreferences]
|
|
7590
|
+
}),
|
|
7262
7591
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7263
7592
|
className: "dsm-workbuddy-panel",
|
|
7264
7593
|
role: "tabpanel",
|
|
@@ -8273,27 +8602,46 @@ window.__ModuleLoader__.load({
|
|
|
8273
8602
|
//#endregion
|
|
8274
8603
|
//#region src/client/WorkBuddyModelLabel.tsx
|
|
8275
8604
|
/**
|
|
8276
|
-
* The
|
|
8605
|
+
* The HOST's model picker label: the catalog name, the upstream's promotion
|
|
8606
|
+
* badge, and the load-level icon.
|
|
8277
8607
|
*
|
|
8278
8608
|
* WHY A SLOT. The host renders the model name as a plain string, so the only
|
|
8279
8609
|
* thing a provider can put there is a character — and a character's colour is
|
|
8280
8610
|
* the emoji font's business, not ours. The host therefore offers exactly one
|
|
8281
8611
|
* seam for this: the `conversation.input.model.label` slot, whose contract is
|
|
8282
8612
|
* "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.
|
|
8613
|
+
* while the picker keeps owning clicks, focus, check marks and the accessibility
|
|
8614
|
+
* text. The shipped example is the official DeepSeek "1017" release badge — a
|
|
8615
|
+
* coloured marker next to a model name — which is the same job.
|
|
8616
|
+
*
|
|
8617
|
+
* WHY THE BADGE BELONGS HERE AT ALL. The picker is where the choice is made,
|
|
8618
|
+
* and a time-boxed promotion is a choice criterion: "free until the 25th" and
|
|
8619
|
+
* "half price at night" both change which row you click. The card already drew
|
|
8620
|
+
* these badges, but the card is the settings surface — by the time you read
|
|
8621
|
+
* them there you have already picked. The badge is not decoration added here;
|
|
8622
|
+
* it is the same upstream fact, moved to the place where it can still change a
|
|
8623
|
+
* decision.
|
|
8286
8624
|
*
|
|
8287
8625
|
* WHY A SEPARATE FETCH. The slot is rendered by the host, outside the plugin's
|
|
8288
8626
|
* card, so it cannot read the card's React state. It therefore asks the Host
|
|
8289
|
-
* for the
|
|
8290
|
-
*
|
|
8291
|
-
*
|
|
8627
|
+
* for the snapshot itself — and asks for the SAME route the card polls
|
|
8628
|
+
* (`WORKBUDDY_USAGE_PATH`), which carries both the load levels and each model's
|
|
8629
|
+
* unresolved promotions. One request, both facts. The answer is cached briefly:
|
|
8630
|
+
* a picker that re-fetched on every re-render would be wasteful, and a level
|
|
8631
|
+
* that is a few seconds old is still true.
|
|
8632
|
+
*
|
|
8633
|
+
* WHY THE CLOCK IS RE-READ ON EVERY RENDER. A promotion's badge is a function
|
|
8634
|
+
* of the clock (a night window opens and closes; a campaign expires). Resolving
|
|
8635
|
+
* it once at fetch time and holding it would leave "夜间折扣" on the composer
|
|
8636
|
+
* button the next morning — the exact overstatement `src/promo.ts` refuses to
|
|
8637
|
+
* make. The snapshot is therefore fetched once and the BADGE is resolved at
|
|
8638
|
+
* render time; a shared one-minute ticker re-renders the mounted labels so a
|
|
8639
|
+
* window edge is picked up without one timer per model row.
|
|
8292
8640
|
*
|
|
8293
8641
|
* WHY THE HOST STILL OWNS THE NAME. The contract says to keep `model.name` as
|
|
8294
8642
|
* the text and render the addition beside it, so this component returns the
|
|
8295
|
-
* name
|
|
8296
|
-
*
|
|
8643
|
+
* name first — it never replaces it, and it renders nothing at all for a
|
|
8644
|
+
* provider that is not ours.
|
|
8297
8645
|
*/
|
|
8298
8646
|
/** The provider ids this plugin registers, and the region each one serves. */
|
|
8299
8647
|
const PROVIDER_REGIONS = /* @__PURE__ */ new Map([["workbuddy", "cn"], ["workbuddy-global", "global"]]);
|
|
@@ -8308,70 +8656,381 @@ window.__ModuleLoader__.load({
|
|
|
8308
8656
|
function regionOfProvider(providerId) {
|
|
8309
8657
|
return PROVIDER_REGIONS.get(providerId);
|
|
8310
8658
|
}
|
|
8311
|
-
/** How long a fetched
|
|
8659
|
+
/** How long a fetched snapshot is trusted, in milliseconds. */
|
|
8312
8660
|
const CACHE_MS = 6e4;
|
|
8313
|
-
/**
|
|
8661
|
+
/** How often a mounted label re-reads the clock, in milliseconds. */
|
|
8662
|
+
const TICK_MS = 6e4;
|
|
8663
|
+
/** The snapshot per region, plus when it was read. */
|
|
8314
8664
|
const caches = /* @__PURE__ */ new Map();
|
|
8315
8665
|
/**
|
|
8316
|
-
* The
|
|
8666
|
+
* The snapshot, read from the Host once and reused briefly.
|
|
8317
8667
|
*
|
|
8318
8668
|
* `undefined` means "not known" — the caller renders nothing rather than
|
|
8319
|
-
* guessing, because a badge that claims a level
|
|
8320
|
-
* worse than no badge.
|
|
8669
|
+
* guessing, because a badge that claims a level or a promotion the upstream
|
|
8670
|
+
* never stated is worse than no badge.
|
|
8321
8671
|
*/
|
|
8322
|
-
async function
|
|
8672
|
+
async function statusNow(region) {
|
|
8323
8673
|
const cached = caches.get(region);
|
|
8324
|
-
if (cached !== void 0 && Date.now() - cached.at < CACHE_MS) return cached.
|
|
8674
|
+
if (cached !== void 0 && Date.now() - cached.at < CACHE_MS) return cached.snapshot;
|
|
8325
8675
|
try {
|
|
8326
8676
|
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, region), {
|
|
8327
8677
|
headers: { accept: "application/json" },
|
|
8328
8678
|
credentials: "same-origin"
|
|
8329
8679
|
});
|
|
8330
8680
|
if (!response.ok) return void 0;
|
|
8331
|
-
const
|
|
8332
|
-
|
|
8681
|
+
const body = await response.json();
|
|
8682
|
+
const promos = {};
|
|
8683
|
+
for (const model of body.models ?? []) if (model.promos !== void 0) promos[model.id] = model.promos;
|
|
8684
|
+
const snapshot = {
|
|
8685
|
+
levels: body.busyLevels ?? {},
|
|
8686
|
+
promos
|
|
8687
|
+
};
|
|
8333
8688
|
caches.set(region, {
|
|
8334
|
-
|
|
8689
|
+
snapshot,
|
|
8335
8690
|
at: Date.now()
|
|
8336
8691
|
});
|
|
8337
|
-
return
|
|
8692
|
+
return snapshot;
|
|
8338
8693
|
} catch {
|
|
8339
8694
|
return;
|
|
8340
8695
|
}
|
|
8341
8696
|
}
|
|
8697
|
+
/** One shared minute ticker, so N mounted labels cost one timer, not N. */
|
|
8698
|
+
const minuteTicker = {
|
|
8699
|
+
timer: void 0,
|
|
8700
|
+
listeners: /* @__PURE__ */ new Set()
|
|
8701
|
+
};
|
|
8702
|
+
/**
|
|
8703
|
+
* Re-render the caller once a minute while it is mounted.
|
|
8704
|
+
*
|
|
8705
|
+
* @param listener - The callback to run at each tick.
|
|
8706
|
+
* @returns The unsubscribe function, which stops the shared timer when the last
|
|
8707
|
+
* listener leaves.
|
|
8708
|
+
*/
|
|
8709
|
+
function subscribeMinute(listener) {
|
|
8710
|
+
minuteTicker.listeners.add(listener);
|
|
8711
|
+
if (minuteTicker.timer === void 0) {
|
|
8712
|
+
minuteTicker.timer = setInterval(() => {
|
|
8713
|
+
for (const each of [...minuteTicker.listeners]) each();
|
|
8714
|
+
}, TICK_MS);
|
|
8715
|
+
minuteTicker.timer.unref?.();
|
|
8716
|
+
}
|
|
8717
|
+
return () => {
|
|
8718
|
+
minuteTicker.listeners.delete(listener);
|
|
8719
|
+
if (minuteTicker.listeners.size === 0 && minuteTicker.timer !== void 0) {
|
|
8720
|
+
clearInterval(minuteTicker.timer);
|
|
8721
|
+
minuteTicker.timer = void 0;
|
|
8722
|
+
}
|
|
8723
|
+
};
|
|
8724
|
+
}
|
|
8725
|
+
/**
|
|
8726
|
+
* The upstream's promotion badge, as the picker draws it.
|
|
8727
|
+
*
|
|
8728
|
+
* Reuses the card's chip class so the two surfaces cannot drift apart, and adds
|
|
8729
|
+
* one modifier for the two things a menu row changes: the chip must fit a row
|
|
8730
|
+
* that is one line wide (see `.dsm-workbuddy-model-label-promo` in the card's
|
|
8731
|
+
* stylesheet), and it is the only place the full tooltip is available.
|
|
8732
|
+
*
|
|
8733
|
+
* @param props - The resolved promotion, and whether space is tight.
|
|
8734
|
+
* @returns The chip.
|
|
8735
|
+
*/
|
|
8736
|
+
function PromoBadge(props) {
|
|
8737
|
+
const { promo, compact } = props;
|
|
8738
|
+
const language = uiLanguage();
|
|
8739
|
+
const label = compact ? promoBadgeShortLabel(promo, language) : promoBadgeLabel(promo, language);
|
|
8740
|
+
const color = promoBadgeColor(promo);
|
|
8741
|
+
const tooltip = promoBadgeTooltip(promo, language);
|
|
8742
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8743
|
+
className: "dsm-workbuddy-model-promo dsm-workbuddy-model-label-promo",
|
|
8744
|
+
title: tooltip === "" ? void 0 : tooltip,
|
|
8745
|
+
style: color === void 0 ? void 0 : { "--dsm-workbuddy-promo-color": color },
|
|
8746
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8747
|
+
className: "dsm-workbuddy-model-label-promo-text",
|
|
8748
|
+
children: label
|
|
8749
|
+
})
|
|
8750
|
+
});
|
|
8751
|
+
}
|
|
8342
8752
|
/**
|
|
8343
8753
|
* The label for one model row in the host's picker.
|
|
8344
8754
|
*
|
|
8345
8755
|
* @param props - The host's model identity and the placement of this label.
|
|
8346
|
-
* @returns The catalog name,
|
|
8756
|
+
* @returns The catalog name, the promotion chip when one holds, and the load
|
|
8757
|
+
* badge when the probe reported one.
|
|
8347
8758
|
*/
|
|
8348
8759
|
function WorkBuddyModelLabel(props) {
|
|
8349
|
-
const { provider, model } = props;
|
|
8760
|
+
const { provider, model, placement } = props;
|
|
8350
8761
|
const region = regionOfProvider(provider.id);
|
|
8351
8762
|
if (region === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
|
|
8352
8763
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkBuddyModelLabelInner, {
|
|
8353
8764
|
modelId: model.id,
|
|
8354
8765
|
name: model.name,
|
|
8355
|
-
region
|
|
8766
|
+
region,
|
|
8767
|
+
compact: placement !== "trigger"
|
|
8356
8768
|
});
|
|
8357
8769
|
}
|
|
8358
|
-
/** The stateful half: fetches the
|
|
8770
|
+
/** The stateful half: fetches the snapshot, then renders what holds right now. */
|
|
8359
8771
|
function WorkBuddyModelLabelInner(props) {
|
|
8360
|
-
const { modelId, name, region } = props;
|
|
8361
|
-
const [
|
|
8772
|
+
const { modelId, name, region, compact } = props;
|
|
8773
|
+
const [snapshot, setSnapshot] = (0, react.useState)(void 0);
|
|
8774
|
+
const [, setTick] = (0, react.useState)(0);
|
|
8362
8775
|
(0, react.useEffect)(() => {
|
|
8363
8776
|
let live = true;
|
|
8364
|
-
|
|
8365
|
-
if (!live ||
|
|
8366
|
-
|
|
8777
|
+
statusNow(region).then((next) => {
|
|
8778
|
+
if (!live || next === void 0) return;
|
|
8779
|
+
setSnapshot(next);
|
|
8367
8780
|
});
|
|
8368
8781
|
return () => {
|
|
8369
8782
|
live = false;
|
|
8370
8783
|
};
|
|
8371
8784
|
}, [modelId, region]);
|
|
8785
|
+
(0, react.useEffect)(() => subscribeMinute(() => setTick((count) => count + 1)), []);
|
|
8786
|
+
const level = snapshot?.levels[modelId];
|
|
8787
|
+
const promo = snapshot === void 0 ? void 0 : selectPromoBadge(snapshot.promos[modelId], Date.now());
|
|
8372
8788
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8373
8789
|
className: "dsm-workbuddy-model-label",
|
|
8374
|
-
children: [
|
|
8790
|
+
children: [
|
|
8791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }),
|
|
8792
|
+
promo === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PromoBadge, {
|
|
8793
|
+
promo,
|
|
8794
|
+
compact
|
|
8795
|
+
}),
|
|
8796
|
+
level === void 0 ? null : busyIcon(level, "zh")
|
|
8797
|
+
]
|
|
8798
|
+
});
|
|
8799
|
+
}
|
|
8800
|
+
//#endregion
|
|
8801
|
+
//#region src/client/CodebuddyLive.tsx
|
|
8802
|
+
/**
|
|
8803
|
+
* The composer pill that says what the CodeBuddy CLI route is doing RIGHT NOW.
|
|
8804
|
+
*
|
|
8805
|
+
* WHY THIS EXISTS. A bridged turn is a spawned agent: minutes of reading files
|
|
8806
|
+
* and running commands, then one answer. The Host tool contract has no progress
|
|
8807
|
+
* channel — `execute()` returns when it returns — so without this pill the user
|
|
8808
|
+
* watches a spinner with no way to tell "still working" from "hung". The pill is
|
|
8809
|
+
* fed by the plugin's own read-only route (`WORKBUDDY_CLI_LIVE_PATH`), which
|
|
8810
|
+
* serializes the host-side live board (see `../cli-bridge/live.ts`).
|
|
8811
|
+
*
|
|
8812
|
+
* WHY IT RENDERS NOTHING WHEN IDLE. This is the ambient dock under the composer,
|
|
8813
|
+
* a row the shell already fills with round/token pills. A permanent "no turn
|
|
8814
|
+
* running" line would be a claim about a feature most users never switch on
|
|
8815
|
+
* (`cliBridge.enabled` defaults to false), so the empty state is `null`: the row
|
|
8816
|
+
* is untouched until there is something true to say.
|
|
8817
|
+
*
|
|
8818
|
+
* WHY IT ONLY DESCRIBES THIS SESSION'S TURN. The board is host-wide: one route
|
|
8819
|
+
* answers for every session in the process, so an unfiltered pill would show the
|
|
8820
|
+
* turn another tab started — a line about a directory the user is not looking at,
|
|
8821
|
+
* sitting under the composer they are typing in. The seat is declared
|
|
8822
|
+
* `scope: 'session'` (`conversation.composer.dock`), so the framework hands the
|
|
8823
|
+
* registration its `sessionId`; the pill keeps the rows that belong to it.
|
|
8824
|
+
*
|
|
8825
|
+
* WHY NOT THE PROVIDER GATE THE CREDIT READOUT USES. `ComposerCredits` renders
|
|
8826
|
+
* only while the session's model is a WorkBuddy provider, because it reports that
|
|
8827
|
+
* provider's quota. This pill reports a spawned CLI process, and the CLI route is
|
|
8828
|
+
* deliberately model-independent: the whole point is that it works from any
|
|
8829
|
+
* session, including one running DeepSeek. Gating it on the provider would hide
|
|
8830
|
+
* the readout exactly when the feature is being used the way it was designed.
|
|
8831
|
+
* `cliBridge.enabled` needs no gate either — with the bridge off no turn can
|
|
8832
|
+
* begin, so the board is empty and the pill renders nothing.
|
|
8833
|
+
*
|
|
8834
|
+
* WHY POLLING, AND WHY THE INTERVAL IS 5s. The Host has no push channel to a
|
|
8835
|
+
* session component, so a poll is the only option. Five seconds is the slowest
|
|
8836
|
+
* cadence at which a turn that just started still feels immediate; the elapsed
|
|
8837
|
+
* counter between polls is advanced locally from `startedAt` (a 1s tick that
|
|
8838
|
+
* costs no request), so the number moves every second without the network
|
|
8839
|
+
* traffic moving with it. Concurrent sessions share ONE in-flight request per
|
|
8840
|
+
* interval (see {@link readLive}), which is what keeps N open tabs from becoming
|
|
8841
|
+
* N polls per tick.
|
|
8842
|
+
*
|
|
8843
|
+
* WHY THE CLOCK OFFSET IS APPLIED. `startedAt` is the HOST's clock. A browser
|
|
8844
|
+
* whose clock is a minute behind would compute a negative age and the pill would
|
|
8845
|
+
* print nonsense. The document ships `serverTime`, so the offset is measured at
|
|
8846
|
+
* each successful read and applied to the local clock instead of trusting the
|
|
8847
|
+
* two machines to agree.
|
|
8848
|
+
*
|
|
8849
|
+
* @module dsh-connect-workbuddy/client/CodebuddyLive
|
|
8850
|
+
*/
|
|
8851
|
+
/**
|
|
8852
|
+
* Inject the dock stylesheet on module load, once.
|
|
8853
|
+
*
|
|
8854
|
+
* `ComposerCredits` injects the same sheet for its own classes, and in the
|
|
8855
|
+
* shipped bundle both modules are static imports of the client entry, so in
|
|
8856
|
+
* practice the sheet is already there. This block is not redundant: it makes the
|
|
8857
|
+
* pill independent of whether the credit readout is mounted at all (the gate can
|
|
8858
|
+
* hide it for a non-WorkBuddy model), and the `data-plugin-css` guard means the
|
|
8859
|
+
* two never produce a second tag.
|
|
8860
|
+
*/
|
|
8861
|
+
if (typeof document !== "undefined") {
|
|
8862
|
+
const cssId = "dsh-connect-workbuddy/composer-credits.css";
|
|
8863
|
+
if (!document.querySelector(`style[data-plugin-css="${cssId}"]`)) {
|
|
8864
|
+
const styleTag = document.createElement("style");
|
|
8865
|
+
styleTag.dataset.plugin = "dsh-connect-workbuddy";
|
|
8866
|
+
styleTag.dataset.pluginCss = cssId;
|
|
8867
|
+
styleTag.textContent = WORKBUDDY_COMPOSER_CSS;
|
|
8868
|
+
document.head.appendChild(styleTag);
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
/** How often the route is asked. See the module comment for why 5s. */
|
|
8872
|
+
const CLI_LIVE_POLL_MS = 5e3;
|
|
8873
|
+
/**
|
|
8874
|
+
* The last document any component in this page saw, shared across sessions.
|
|
8875
|
+
*
|
|
8876
|
+
* A module-level cache is safe here because the document is host state, not
|
|
8877
|
+
* component state: every session polls the same board, so one request serves
|
|
8878
|
+
* them all. `inflight` collapses the case where several components mount in the
|
|
8879
|
+
* same tick — without it, opening three sessions at once would fire three
|
|
8880
|
+
* requests and each would overwrite the others' result.
|
|
8881
|
+
*/
|
|
8882
|
+
let cached;
|
|
8883
|
+
let inflight;
|
|
8884
|
+
/**
|
|
8885
|
+
* Accept a document only if every number the pill does arithmetic on is a real
|
|
8886
|
+
* number.
|
|
8887
|
+
*
|
|
8888
|
+
* The route is our own, so this is not defence against an attacker — it is
|
|
8889
|
+
* defence against a HALF-AGREED wire: a field renamed on the host side, an
|
|
8890
|
+
* `undefined` that `JSON.stringify` drops, or a future route that answers a
|
|
8891
|
+
* different shape. Without the check `serverTime` becomes `NaN`, the clock
|
|
8892
|
+
* offset becomes `NaN`, and the pill renders `NaNs` — a number that is wrong in
|
|
8893
|
+
* the one place a user is looking for reassurance. Dropping the whole document
|
|
8894
|
+
* degrades to "no turn", which is the honest answer when the wire is unreadable.
|
|
8895
|
+
*/
|
|
8896
|
+
function acceptDocument(value) {
|
|
8897
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
8898
|
+
const candidate = value;
|
|
8899
|
+
if (!Array.isArray(candidate.turns)) return void 0;
|
|
8900
|
+
if (typeof candidate.serverTime !== "number" || !Number.isFinite(candidate.serverTime)) return void 0;
|
|
8901
|
+
return {
|
|
8902
|
+
turns: candidate.turns.filter((turn) => {
|
|
8903
|
+
if (typeof turn !== "object" || turn === null) return false;
|
|
8904
|
+
const row = turn;
|
|
8905
|
+
return typeof row.startedAt === "number" && Number.isFinite(row.startedAt) && typeof row.toolCalls === "number" && Number.isFinite(row.toolCalls);
|
|
8906
|
+
}),
|
|
8907
|
+
serverTime: candidate.serverTime
|
|
8908
|
+
};
|
|
8909
|
+
}
|
|
8910
|
+
/** Read the board, reusing a recent document or a request already in flight. */
|
|
8911
|
+
async function readLive(fetchImpl, now = Date.now(), cacheMs = CLI_LIVE_POLL_MS) {
|
|
8912
|
+
if (cached !== void 0 && now - cached.at < cacheMs) return cached.document;
|
|
8913
|
+
if (inflight !== void 0) return inflight;
|
|
8914
|
+
inflight = (async () => {
|
|
8915
|
+
try {
|
|
8916
|
+
const response = await fetchImpl(WORKBUDDY_CLI_LIVE_PATH, { headers: { accept: "application/json" } });
|
|
8917
|
+
if (!response.ok) return void 0;
|
|
8918
|
+
const document = acceptDocument(await response.json());
|
|
8919
|
+
if (document === void 0) return void 0;
|
|
8920
|
+
cached = {
|
|
8921
|
+
at: Date.now(),
|
|
8922
|
+
document
|
|
8923
|
+
};
|
|
8924
|
+
return document;
|
|
8925
|
+
} catch {
|
|
8926
|
+
return;
|
|
8927
|
+
} finally {
|
|
8928
|
+
inflight = void 0;
|
|
8929
|
+
}
|
|
8930
|
+
})();
|
|
8931
|
+
return inflight;
|
|
8932
|
+
}
|
|
8933
|
+
/**
|
|
8934
|
+
* The text of one pill.
|
|
8935
|
+
*
|
|
8936
|
+
* Split out so the format is testable without a DOM, and so the same function
|
|
8937
|
+
* produces the visible label and the accessible one.
|
|
8938
|
+
*/
|
|
8939
|
+
function liveLabel(turn, elapsedMs, t) {
|
|
8940
|
+
const seconds = Math.max(0, Math.round(elapsedMs / 1e3));
|
|
8941
|
+
const parts = [t("cliLive.running"), t("cliLive.toolCalls", { count: turn.toolCalls })];
|
|
8942
|
+
if (turn.phase === "starting") parts.push(t("cliLive.starting"));
|
|
8943
|
+
else if (turn.phase === "finishing") parts.push(t("cliLive.finishing"));
|
|
8944
|
+
else parts.push(t("cliLive.elapsed", { seconds }));
|
|
8945
|
+
if (turn.lastTool !== "") parts.push(t("cliLive.lastTool", { tool: turn.lastTool }));
|
|
8946
|
+
return parts.join(" · ");
|
|
8947
|
+
}
|
|
8948
|
+
/**
|
|
8949
|
+
* Keep the rows this pill is allowed to describe.
|
|
8950
|
+
*
|
|
8951
|
+
* A row with an empty `sessionId` is one the host could not attribute (the tool
|
|
8952
|
+
* ran without an agent on the execution object). Hiding those would make the pill
|
|
8953
|
+
* silently useless on such a host, so they are kept: an unattributed row is
|
|
8954
|
+
* evidence of real work, and the alternative is a readout that never appears.
|
|
8955
|
+
*/
|
|
8956
|
+
function turnsForSession(turns, sessionId) {
|
|
8957
|
+
if (sessionId === void 0 || sessionId === "") return turns;
|
|
8958
|
+
return turns.filter((turn) => turn.sessionId === "" || turn.sessionId === sessionId);
|
|
8959
|
+
}
|
|
8960
|
+
/**
|
|
8961
|
+
* The pill itself.
|
|
8962
|
+
*
|
|
8963
|
+
* Shows the NEWEST turn (the one the last click started) and appends `+N` when
|
|
8964
|
+
* several are live, because the dock is a single line and a queue listing would
|
|
8965
|
+
* wrap. The count is the whole point of the `+N`: it tells the user there is
|
|
8966
|
+
* more work running than the line describes.
|
|
8967
|
+
*/
|
|
8968
|
+
function CodebuddyLive(props) {
|
|
8969
|
+
const { t } = props;
|
|
8970
|
+
const fetchImpl = props.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0);
|
|
8971
|
+
const pollMs = props.pollMs ?? 5e3;
|
|
8972
|
+
const tickMs = props.tickMs ?? 1e3;
|
|
8973
|
+
const [document, setDocument] = (0, react.useState)(void 0);
|
|
8974
|
+
const [, setTick] = (0, react.useState)(0);
|
|
8975
|
+
const offsetRef = (0, react.useRef)(0);
|
|
8976
|
+
const offsetForRef = (0, react.useRef)(void 0);
|
|
8977
|
+
const failuresRef = (0, react.useRef)(0);
|
|
8978
|
+
(0, react.useEffect)(() => {
|
|
8979
|
+
if (fetchImpl === void 0) return void 0;
|
|
8980
|
+
let live = true;
|
|
8981
|
+
const read = () => {
|
|
8982
|
+
readLive(fetchImpl, Date.now(), pollMs).then((next) => {
|
|
8983
|
+
if (!live) return;
|
|
8984
|
+
if (next === void 0) {
|
|
8985
|
+
failuresRef.current += 1;
|
|
8986
|
+
if (failuresRef.current >= 3) setDocument(void 0);
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8989
|
+
failuresRef.current = 0;
|
|
8990
|
+
if (next.serverTime !== offsetForRef.current) {
|
|
8991
|
+
offsetForRef.current = next.serverTime;
|
|
8992
|
+
offsetRef.current = next.serverTime - Date.now();
|
|
8993
|
+
}
|
|
8994
|
+
setDocument(next);
|
|
8995
|
+
});
|
|
8996
|
+
};
|
|
8997
|
+
read();
|
|
8998
|
+
const timer = setInterval(read, pollMs);
|
|
8999
|
+
return () => {
|
|
9000
|
+
live = false;
|
|
9001
|
+
clearInterval(timer);
|
|
9002
|
+
};
|
|
9003
|
+
}, [fetchImpl, pollMs]);
|
|
9004
|
+
const turns = turnsForSession(document?.turns ?? [], props.sessionId);
|
|
9005
|
+
const active = turns.length > 0;
|
|
9006
|
+
(0, react.useEffect)(() => {
|
|
9007
|
+
if (!active) return void 0;
|
|
9008
|
+
const timer = setInterval(() => {
|
|
9009
|
+
setTick((value) => value + 1);
|
|
9010
|
+
}, tickMs);
|
|
9011
|
+
return () => {
|
|
9012
|
+
clearInterval(timer);
|
|
9013
|
+
};
|
|
9014
|
+
}, [active, tickMs]);
|
|
9015
|
+
if (!active) return null;
|
|
9016
|
+
const turn = turns[turns.length - 1];
|
|
9017
|
+
const label = liveLabel(turn, Math.max(0, Date.now() + offsetRef.current - turn.startedAt), t);
|
|
9018
|
+
const extra = turns.length > 1 ? ` +${String(turns.length - 1)}` : "";
|
|
9019
|
+
const dirTail = turn.workingDir.replace(/[\\/]+$/u, "").split(/[\\/]/u).pop() ?? "";
|
|
9020
|
+
const title = dirTail === "" ? `${label}${extra}` : `${label}${extra} · ${dirTail}`;
|
|
9021
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9022
|
+
role: "status",
|
|
9023
|
+
"aria-label": t("cliLive.aria", { detail: label }),
|
|
9024
|
+
title,
|
|
9025
|
+
className: "dsm-workbuddy-cli-live",
|
|
9026
|
+
children: [
|
|
9027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9028
|
+
"aria-hidden": "true",
|
|
9029
|
+
className: "dsm-workbuddy-cli-live-dot"
|
|
9030
|
+
}),
|
|
9031
|
+
label,
|
|
9032
|
+
extra
|
|
9033
|
+
]
|
|
8375
9034
|
});
|
|
8376
9035
|
}
|
|
8377
9036
|
//#endregion
|
|
@@ -8538,6 +9197,8 @@ window.__ModuleLoader__.load({
|
|
|
8538
9197
|
"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
9198
|
"row.panelAccounts": "Accounts",
|
|
8540
9199
|
"row.panelModels": "Models",
|
|
9200
|
+
"row.panelCapabilities": "Capabilities",
|
|
9201
|
+
"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
9202
|
"row.panelDiag": "Diagnostics",
|
|
8542
9203
|
"row.panelAria": "Card sections",
|
|
8543
9204
|
"row.diagHint": "What this card actually read, so a report can quote it. Nothing here changes a setting.",
|
|
@@ -8662,9 +9323,9 @@ window.__ModuleLoader__.load({
|
|
|
8662
9323
|
"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
9324
|
"row.poolModelWideBusy": "Every member is blocked on the same model — this is the model, not your accounts.",
|
|
8664
9325
|
"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
|
|
9326
|
+
"row.poolCheckinAll": "Check in selected",
|
|
8666
9327
|
"row.poolCheckingIn": "Checking in…",
|
|
8667
|
-
"row.poolTestAll": "Test
|
|
9328
|
+
"row.poolTestAll": "Test selected",
|
|
8668
9329
|
"row.poolTesting": "Testing…",
|
|
8669
9330
|
"row.poolBusyHint": "Running one account at a time…",
|
|
8670
9331
|
"row.poolTargetAuto": "Auto",
|
|
@@ -8682,9 +9343,9 @@ window.__ModuleLoader__.load({
|
|
|
8682
9343
|
"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
9344
|
"row.poolColumnAccount": "Account",
|
|
8684
9345
|
"row.poolColumnCredits": "Credits",
|
|
8685
|
-
"row.poolColumnProbe": "
|
|
9346
|
+
"row.poolColumnProbe": "Status",
|
|
8686
9347
|
"row.poolColumnCheckin": "Check-in",
|
|
8687
|
-
"row.poolNeverTested": "
|
|
9348
|
+
"row.poolNeverTested": "Untested",
|
|
8688
9349
|
"row.poolTestedAt": "Tested {at}",
|
|
8689
9350
|
"row.poolCheckedIn": "Checked in",
|
|
8690
9351
|
"row.poolNotCheckedIn": "Not checked in",
|
|
@@ -8724,15 +9385,16 @@ window.__ModuleLoader__.load({
|
|
|
8724
9385
|
"row.poolProbeSourceUnknown": "source unknown",
|
|
8725
9386
|
"row.poolProbeModel": "model {model}",
|
|
8726
9387
|
"row.poolProbeModelUnknown": "model not recorded",
|
|
8727
|
-
"row.poolSettingsTitle": "
|
|
9388
|
+
"row.poolSettingsTitle": "More settings",
|
|
8728
9389
|
"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
9390
|
"row.poolEnabled": "Enable the account pool",
|
|
8730
|
-
"row.poolEnabledHint": "
|
|
9391
|
+
"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.",
|
|
9392
|
+
"row.poolModeAuto": "picking automatically",
|
|
9393
|
+
"row.poolModeManual": "you pick",
|
|
8731
9394
|
"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
9395
|
"row.poolManualAccountNone": "No account in effect",
|
|
8734
|
-
"row.poolTargetLabel": "
|
|
8735
|
-
"row.poolTargetHint": "Auto
|
|
9396
|
+
"row.poolTargetLabel": "Test model (whole pool)",
|
|
9397
|
+
"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
9398
|
"row.poolTargetAutoOption": "Auto — pick a free model",
|
|
8737
9399
|
"row.poolProbeSizeLabel": "Test request size",
|
|
8738
9400
|
"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 +9422,7 @@ window.__ModuleLoader__.load({
|
|
|
8760
9422
|
"row.poolLogTestRowMalformed": "{accountName}: the host returned no result for this account",
|
|
8761
9423
|
"row.poolRegionNote": "The domestic and international pools are fully independent: accounts, switches, intervals, and target models never affect each other.",
|
|
8762
9424
|
"row.poolMembersTitle": "Accounts in this pool",
|
|
8763
|
-
"row.poolMembersHint": "Only checked accounts
|
|
9425
|
+
"row.poolMembersHint": "Only checked accounts rotate, get checked in, and get tested.",
|
|
8764
9426
|
"row.poolSelectAll": "Select all",
|
|
8765
9427
|
"row.poolSelectNone": "Clear all",
|
|
8766
9428
|
"row.poolSelectedCount": "{count} of {total} selected",
|
|
@@ -8778,16 +9440,17 @@ window.__ModuleLoader__.load({
|
|
|
8778
9440
|
"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
9441
|
"row.poolOrderRulesToggle": "Show the full ordering rule",
|
|
8780
9442
|
"row.poolNoneSelected": "No account is checked into this pool.",
|
|
8781
|
-
"row.poolNoneSelectedHint": "Check at least one account
|
|
9443
|
+
"row.poolNoneSelectedHint": "Check at least one account to enable check-in and test.",
|
|
8782
9444
|
"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
9445
|
"row.poolGhostRemove": "Remove {account} from this pool",
|
|
8784
9446
|
"row.poolGhostRemoveNamed": "Remove {account} from this pool. This account is no longer signed in on this machine.",
|
|
8785
9447
|
"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
9448
|
"row.poolSelectAria": "Include {accountName} in this region's account pool",
|
|
8787
|
-
"row.poolMember": "In pool",
|
|
8788
9449
|
"row.poolCurrentBadge": "In use",
|
|
8789
9450
|
"row.poolCurrentHeader": "In use: {account}",
|
|
8790
9451
|
"row.poolNotMember": "Not in pool",
|
|
9452
|
+
"row.poolNotMemberHint": "Not in the pool: rotation skips it, and batch check-in and test leave it alone.",
|
|
9453
|
+
"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
9454
|
"row.poolUnsavedMembers": "Selection changed — save to apply",
|
|
8792
9455
|
"row.poolRankLabel": "No. {rank}",
|
|
8793
9456
|
"row.poolRankManualTitle": "You placed this account at position {rank} in the pool manually.",
|
|
@@ -8845,7 +9508,15 @@ window.__ModuleLoader__.load({
|
|
|
8845
9508
|
"sidebar.refreshing": "Refreshing…",
|
|
8846
9509
|
"sidebar.lastRefresh": "Last refreshed {time}",
|
|
8847
9510
|
"sidebar.refreshFailed": "Refresh failed",
|
|
8848
|
-
"sidebar.signedOut": "Not signed in"
|
|
9511
|
+
"sidebar.signedOut": "Not signed in",
|
|
9512
|
+
"cliLive.running": "CodeBuddy",
|
|
9513
|
+
"cliLive.toolCalls": "{count} tools",
|
|
9514
|
+
"cliLive.elapsed": "{seconds}s",
|
|
9515
|
+
"cliLive.starting": "starting…",
|
|
9516
|
+
"cliLive.finishing": "finishing…",
|
|
9517
|
+
"cliLive.lastTool": "last: {tool}",
|
|
9518
|
+
"cliLive.chars": "{count} chars",
|
|
9519
|
+
"cliLive.aria": "A CodeBuddy turn is running: {detail}"
|
|
8849
9520
|
};
|
|
8850
9521
|
const zh = {
|
|
8851
9522
|
"row.title": "接入使用 WorkBuddy 积分与模型(dsh-connect-workbuddy)",
|
|
@@ -8904,6 +9575,8 @@ window.__ModuleLoader__.load({
|
|
|
8904
9575
|
"row.selectionLostMessage": "该区域保存的账号已不在本机登录列表中(WorkBuddy 更换了登录或清理了备份文件)。本机其他登录仍然可用——请在上方列表中重新选择一个当前账号。重新登录桌面端 App 无法修复此问题。",
|
|
8905
9576
|
"row.panelAccounts": "账号",
|
|
8906
9577
|
"row.panelModels": "模型",
|
|
9578
|
+
"row.panelCapabilities": "能力",
|
|
9579
|
+
"row.panelCapabilitiesHint": "插件可以替你做什么。这些开关点一下立即生效 —— 这一区里没有保存按钮。",
|
|
8907
9580
|
"row.panelDiag": "诊断",
|
|
8908
9581
|
"row.panelAria": "卡片分区",
|
|
8909
9582
|
"row.diagHint": "这张卡片实际读到了什么,方便贴进问题反馈。这里没有任何会改动设置的控件。",
|
|
@@ -9028,9 +9701,9 @@ window.__ModuleLoader__.load({
|
|
|
9028
9701
|
"row.poolNoCandidateHint": "成员要么在限流冷却中、要么积分耗尽、要么凭据被拒、要么上游无响应。限流、积分耗尽与瞬时故障都会自动回池——按上游给出的恢复时间,没给时间就按 30 分钟兜底,不需要你操作;只有「凭据被拒」需要重新登录。",
|
|
9029
9702
|
"row.poolModelWideBusy": "所有成员都卡在同一个模型上 —— 这是模型的问题,不是你的账号。",
|
|
9030
9703
|
"row.poolModelWideBusyHint": "换账号解决不了:限制跟随模型,换一个账号撞的是同一堵墙。等大约 1 分钟,或把这个会话切到负载较低的模型。",
|
|
9031
|
-
"row.poolCheckinAll": "
|
|
9704
|
+
"row.poolCheckinAll": "签到勾选的账号",
|
|
9032
9705
|
"row.poolCheckingIn": "签到中…",
|
|
9033
|
-
"row.poolTestAll": "
|
|
9706
|
+
"row.poolTestAll": "测试勾选的账号",
|
|
9034
9707
|
"row.poolTesting": "测试中…",
|
|
9035
9708
|
"row.poolBusyHint": "正在逐个账号执行…",
|
|
9036
9709
|
"row.poolTargetAuto": "自动",
|
|
@@ -9048,9 +9721,9 @@ window.__ModuleLoader__.load({
|
|
|
9048
9721
|
"row.poolTargetNoneHint": "国内版的倍率要等模型目录刷新后才会出现。为了不让你在不知情的情况下花掉积分,插件不会自动改用收费模型——请先刷新模型目录,或自己指定一个目标模型。",
|
|
9049
9722
|
"row.poolColumnAccount": "账号",
|
|
9050
9723
|
"row.poolColumnCredits": "积分",
|
|
9051
|
-
"row.poolColumnProbe": "
|
|
9724
|
+
"row.poolColumnProbe": "状态",
|
|
9052
9725
|
"row.poolColumnCheckin": "签到",
|
|
9053
|
-
"row.poolNeverTested": "
|
|
9726
|
+
"row.poolNeverTested": "未测试",
|
|
9054
9727
|
"row.poolTestedAt": "测试于 {at}",
|
|
9055
9728
|
"row.poolCheckedIn": "已签到",
|
|
9056
9729
|
"row.poolNotCheckedIn": "未签到",
|
|
@@ -9090,15 +9763,16 @@ window.__ModuleLoader__.load({
|
|
|
9090
9763
|
"row.poolProbeSourceUnknown": "来源未知",
|
|
9091
9764
|
"row.poolProbeModel": "模型 {model}",
|
|
9092
9765
|
"row.poolProbeModelUnknown": "未记录模型",
|
|
9093
|
-
"row.poolSettingsTitle": "
|
|
9766
|
+
"row.poolSettingsTitle": "更多设置",
|
|
9094
9767
|
"row.poolSettingsHint": "下面是偏好设置:点保存才生效(与「模型管理」一致)。测试结果由插件自动写入,不受这个保存按钮影响。",
|
|
9095
9768
|
"row.poolEnabled": "启用账号池",
|
|
9096
|
-
"row.poolEnabledHint": "
|
|
9769
|
+
"row.poolEnabledHint": "开启:由账号池为每次请求挑号,失败就换下一个。关闭:你选的账号服务每一次请求,失败如实报错。",
|
|
9770
|
+
"row.poolModeAuto": "自动挑号",
|
|
9771
|
+
"row.poolModeManual": "手动选号",
|
|
9097
9772
|
"row.poolManualAccountLabel": "使用哪个账号",
|
|
9098
|
-
"row.poolManualAccountHint": "账号池关闭时,由你在这里选定服务本区域的账号。开启账号池后改由排序自动决定。",
|
|
9099
9773
|
"row.poolManualAccountNone": "当前没有生效的账号",
|
|
9100
|
-
"row.poolTargetLabel": "
|
|
9101
|
-
"row.poolTargetHint": "
|
|
9774
|
+
"row.poolTargetLabel": "测试用模型(全池统一)",
|
|
9775
|
+
"row.poolTargetHint": "全池共用这一个模型,测试就是拿它逐个去测。「自动」会从本区域目录里挑一个倍率为 0 的免费模型,多个候选取上下文窗口最大的;手动指定优先于自动。",
|
|
9102
9776
|
"row.poolTargetAutoOption": "自动 —— 挑一个免费模型",
|
|
9103
9777
|
"row.poolProbeSizeLabel": "测试请求体积",
|
|
9104
9778
|
"row.poolProbeSizeHint": "测试时发送多少文本。上游在请求大约超过 2 万~3 万 token 时会开始拒绝,所以测试体积若小于你真实对话的体积,就会出现「测出可用、实际一发就被限」。体积越大测得越准,但每次测试消耗的积分也越多。",
|
|
@@ -9126,7 +9800,7 @@ window.__ModuleLoader__.load({
|
|
|
9126
9800
|
"row.poolLogTestRowMalformed": "{accountName}:Host 未返回该账号的结果",
|
|
9127
9801
|
"row.poolRegionNote": "国内版与国际版是完全独立的两个账号池:账号、开关、间隔、目标模型互不影响。",
|
|
9128
9802
|
"row.poolMembersTitle": "本池包含的账号",
|
|
9129
|
-
"row.poolMembersHint": "
|
|
9803
|
+
"row.poolMembersHint": "只有勾选的账号参与轮换、签到与测试。",
|
|
9130
9804
|
"row.poolSelectAll": "全选",
|
|
9131
9805
|
"row.poolSelectNone": "全不选",
|
|
9132
9806
|
"row.poolSelectedCount": "已选 {count} / {total}",
|
|
@@ -9143,17 +9817,18 @@ window.__ModuleLoader__.load({
|
|
|
9143
9817
|
"row.poolOrderWhy": "为什么临期积分压过手动顺序:临期积分是马上要过期的钱,而手动顺序可能已经过时、也可能是手滑拖错了。如果让手动顺序赢,一次误拖就足以让积分白白过期。所以有 3 天内到期积分的账号,永远排在没有的账号前面 —— 哪怕你把它拖到了最后一位。",
|
|
9144
9818
|
"row.poolOrderBadgeNote": "徽标显示的是你「保存」的位置,不是此刻的实际排序。一个账号可能写着「第 2 位」却因为被限流而暂时不被选中 —— 那个数字代表你的偏好,池子会跳过当下无法扣费的账号。",
|
|
9145
9819
|
"row.poolOrderRulesToggle": "查看完整排序规则",
|
|
9146
|
-
"row.poolNoneSelected": "
|
|
9147
|
-
"row.poolNoneSelectedHint": "
|
|
9820
|
+
"row.poolNoneSelected": "还没有勾选任何账号。",
|
|
9821
|
+
"row.poolNoneSelectedHint": "勾选至少一个账号,才能签到与测试。",
|
|
9148
9822
|
"row.poolGhostMembers": "有 {count} 个已选入本池的账号在本机已不再登录,因此不计入、也不会被使用:{names}。不需要的可在下方逐个移除。",
|
|
9149
9823
|
"row.poolGhostRemove": "把 {account} 移出本池",
|
|
9150
9824
|
"row.poolGhostRemoveNamed": "把 {account} 移出本池。该账号在本机已不再登录。",
|
|
9151
9825
|
"row.poolGhostRemoveUnnamed": "把 {account} 移出本池。这个账号从未记录过名字,显示的是它 id 的开头几位。",
|
|
9152
9826
|
"row.poolSelectAria": "把 {accountName} 加入本区域的账号池",
|
|
9153
|
-
"row.poolMember": "已在池中",
|
|
9154
9827
|
"row.poolCurrentBadge": "当前使用",
|
|
9155
9828
|
"row.poolCurrentHeader": "当前使用:{account}",
|
|
9156
9829
|
"row.poolNotMember": "未入池",
|
|
9830
|
+
"row.poolNotMemberHint": "不在池里:不参与轮换,也不进批量签到与测试。",
|
|
9831
|
+
"row.poolServingOutside": "「{account}」正在使用,但不在这个池里:轮换不会挑它,批量签到与测试也不会动它。",
|
|
9157
9832
|
"row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
|
|
9158
9833
|
"row.poolRankLabel": "第 {rank} 位",
|
|
9159
9834
|
"row.poolRankManualTitle": "你在本池中手动把该账号排在第 {rank} 位。",
|
|
@@ -9211,7 +9886,15 @@ window.__ModuleLoader__.load({
|
|
|
9211
9886
|
"sidebar.refreshing": "刷新中…",
|
|
9212
9887
|
"sidebar.lastRefresh": "最近刷新 {time}",
|
|
9213
9888
|
"sidebar.refreshFailed": "刷新失败",
|
|
9214
|
-
"sidebar.signedOut": "未登录"
|
|
9889
|
+
"sidebar.signedOut": "未登录",
|
|
9890
|
+
"cliLive.running": "CodeBuddy",
|
|
9891
|
+
"cliLive.toolCalls": "{count} 次工具",
|
|
9892
|
+
"cliLive.elapsed": "{seconds} 秒",
|
|
9893
|
+
"cliLive.starting": "启动中…",
|
|
9894
|
+
"cliLive.finishing": "收尾中…",
|
|
9895
|
+
"cliLive.lastTool": "最近:{tool}",
|
|
9896
|
+
"cliLive.chars": "{count} 字",
|
|
9897
|
+
"cliLive.aria": "有一个 CodeBuddy 回合正在跑:{detail}"
|
|
9215
9898
|
};
|
|
9216
9899
|
//#endregion
|
|
9217
9900
|
//#region src/client/index.tsx
|
|
@@ -9391,6 +10074,42 @@ window.__ModuleLoader__.load({
|
|
|
9391
10074
|
}
|
|
9392
10075
|
};
|
|
9393
10076
|
/**
|
|
10077
|
+
* The live CodeBuddy turn pill, in the same ambient dock as the readout.
|
|
10078
|
+
*
|
|
10079
|
+
* `order` 11 puts it immediately after the credit readout (10) and still
|
|
10080
|
+
* right of the shell's own pills (0): the two numbers a user glances at while
|
|
10081
|
+
* a turn runs sit together. The component renders `null` whenever no turn is
|
|
10082
|
+
* live, which is the normal case — the bridge is off by default — so an
|
|
10083
|
+
* unused seat costs one poll interval and nothing on screen.
|
|
10084
|
+
*
|
|
10085
|
+
* THE SESSION ID IS THE PILL'S DISPLAY CONDITION. `conversation.composer.dock`
|
|
10086
|
+
* is declared `scope: 'session'`, so the registration's `inject` receives the
|
|
10087
|
+
* session id and the pill can keep only the rows that session started. The
|
|
10088
|
+
* board itself is host-wide (one route for every session in the process), so
|
|
10089
|
+
* without this the pill would describe whatever turn is newest anywhere —
|
|
10090
|
+
* including one from another tab, about a directory the reader is not in.
|
|
10091
|
+
* Unlike the credit readout there is deliberately no provider gate: the CLI
|
|
10092
|
+
* route is model-independent, and a turn started from a DeepSeek session is
|
|
10093
|
+
* exactly a case the readout has to cover.
|
|
10094
|
+
*/
|
|
10095
|
+
const registerCliLive = () => {
|
|
10096
|
+
try {
|
|
10097
|
+
return ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
10098
|
+
name: "conversation.composer.dock",
|
|
10099
|
+
id: "dsh-connect-workbuddy-cli-live",
|
|
10100
|
+
order: 11,
|
|
10101
|
+
locale: namespace,
|
|
10102
|
+
inject: (sessionId) => ({
|
|
10103
|
+
t,
|
|
10104
|
+
sessionId
|
|
10105
|
+
})
|
|
10106
|
+
}, CodebuddyLive));
|
|
10107
|
+
} catch (error) {
|
|
10108
|
+
console.error("[dsh-connect-workbuddy] cli live slot failed to register (host provider unaffected):", error);
|
|
10109
|
+
return;
|
|
10110
|
+
}
|
|
10111
|
+
};
|
|
10112
|
+
/**
|
|
9394
10113
|
* The frame-wide quota notice, DECLINED so the host keeps its own Toast
|
|
9395
10114
|
* (issue #63 follow-up, the maintainer's call on a screenshot).
|
|
9396
10115
|
*
|
|
@@ -9454,6 +10173,7 @@ window.__ModuleLoader__.load({
|
|
|
9454
10173
|
...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.row.config", `${key}#${key}`)),
|
|
9455
10174
|
registerComposerCredits(),
|
|
9456
10175
|
registerStatsCredits(),
|
|
10176
|
+
registerCliLive(),
|
|
9457
10177
|
registerQuotaNotice(),
|
|
9458
10178
|
registerModelLabel()
|
|
9459
10179
|
].filter((disposer) => disposer !== void 0);
|