dsh-workbuddy-xdpool 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -93,6 +93,23 @@ window.__ModuleLoader__.load({
93
93
 
94
94
  /* Body layout: status row + accounts list + models list. */
95
95
  .dsm-workbuddy-xdpool-usage{display:flex;flex-direction:column;gap:14px;margin:0;padding:14px 0 4px}
96
+ /* Region tabs: two independent suppliers, one shown at a time. */
97
+ .dsm-workbuddy-xdpool-tabs{display:flex;gap:6px;padding:4px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
98
+ /* Empty-region guide: steps for signing in on the other gateway. */
99
+ .dsm-workbuddy-xdpool-empty{display:flex;flex-direction:column;gap:10px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#24262c)}
100
+ .dsm-workbuddy-xdpool-empty-title{margin:0;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:14px;font-weight:600;line-height:20px}
101
+ .dsm-workbuddy-xdpool-empty-steps{display:flex;flex-direction:column;gap:6px;padding:12px;border-radius:10px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
102
+ .dsm-workbuddy-xdpool-empty-steps-title{margin:0;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;font-weight:600;line-height:18px}
103
+ .dsm-workbuddy-xdpool-empty-list{margin:0;padding-left:20px;display:flex;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
104
+ .dsm-workbuddy-xdpool-empty-list li{min-width:0}
105
+ .dsm-workbuddy-xdpool-empty-note{margin:0;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:11px;line-height:17px}
106
+ .dsm-workbuddy-xdpool-tab{appearance:none;font:inherit;cursor:pointer;flex:1;border:0;border-radius:7px;padding:7px 10px;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;font-weight:500;line-height:18px;background:transparent;transition:color .16s,background .16s,box-shadow .16s}
107
+ .dsm-workbuddy-xdpool-tab:hover:not(.dsm-workbuddy-xdpool-tab-active){color:var(--dsw-alias-label-primary,#e6e6e6)}
108
+ .dsm-workbuddy-xdpool-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
109
+ .dsm-workbuddy-xdpool-tab-active{color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-2,#232529);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2,#3a3d45)}
110
+ .dsm-workbuddy-xdpool-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;vertical-align:baseline;background:var(--dsw-alias-state-success-primary,#22a06b)}
111
+ .dsm-workbuddy-xdpool-tab-dot[data-state="error"]{background:var(--dsw-alias-state-error-primary,#ef4444)}
112
+ .dsm-workbuddy-xdpool-tab-dot[data-state="idle"]{background:var(--dsw-alias-label-dimmed,#9aa0a6)}
96
113
  .dsm-workbuddy-xdpool-usage-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
97
114
  .dsm-workbuddy-xdpool-usage-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
98
115
  .dsm-workbuddy-xdpool-usage-status{display:flex;align-items:center;gap:10px;font-size:15px;font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6)}
@@ -117,19 +134,21 @@ window.__ModuleLoader__.load({
117
134
  .dsm-workbuddy-xdpool-account-modelcool-chip{display:inline-flex;align-items:center;gap:4px;padding:1px 8px;border-radius:999px;font-size:11px;line-height:18px;background:var(--dsw-alias-state-warning-subtle,rgba(217,119,6,.12));color:var(--dsw-alias-state-warning-primary,#d97706)}
118
135
  .dsm-workbuddy-xdpool-account-error{margin:0;color:var(--dsw-alias-state-error-primary,#ef4444);font-size:13px;line-height:20px}
119
136
 
120
- /* Credit packages under each account: package list with remain/size. */
121
- .dsm-workbuddy-xdpool-credits-panels{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(150px,.8fr);gap:10px;margin-top:10px}
122
- .dsm-workbuddy-xdpool-credit-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#24262c)}
123
- .dsm-workbuddy-xdpool-credit-panel-title{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
124
- .dsm-workbuddy-xdpool-credit-panel-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;line-height:21px;font-variant-numeric:tabular-nums}
125
- .dsm-workbuddy-xdpool-credit-packages{display:flex;flex-direction:column;gap:5px;margin:0;padding:0;list-style:none}
126
- .dsm-workbuddy-xdpool-credit-packages li{display:flex;align-items:baseline;justify-content:space-between;gap:10px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
127
- .dsm-workbuddy-xdpool-credit-packages li span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
128
- .dsm-workbuddy-xdpool-credit-packages li span:last-child{flex:none;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-variant-numeric:tabular-nums}
129
- .dsm-workbuddy-xdpool-credit-panel-total{position:relative;align-items:center;text-align:center;overflow:hidden}
130
- .dsm-workbuddy-xdpool-credit-panel-total::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:var(--dsw-alias-state-success-primary,#22a06b)}
131
- .dsm-workbuddy-xdpool-credit-total-body{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;width:100%}
132
- .dsm-workbuddy-xdpool-credit-total-value{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:30px;line-height:34px;font-weight:700;letter-spacing:-.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
137
+ /* Two-column stats: packages on the left, total + check-in on the right. */
138
+ .dsm-workbuddy-xdpool-stats{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(170px,.85fr);gap:10px;margin-top:10px}
139
+ .dsm-workbuddy-xdpool-panel{display:flex;flex-direction:column;min-width:0;gap:7px;padding:14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#24262c)}
140
+ .dsm-workbuddy-xdpool-panel-title{color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
141
+ .dsm-workbuddy-xdpool-panel-empty{color:var(--dsw-alias-label-tertiary,#999);font-size:14px;line-height:20px}
142
+ .dsm-workbuddy-xdpool-panel-error{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:12px;line-height:18px;word-break:break-word}
143
+ .dsm-workbuddy-xdpool-panel-foot{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-top:9px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
144
+ .dsm-workbuddy-xdpool-panel-foot strong{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;font-variant-numeric:tabular-nums}
145
+ .dsm-workbuddy-xdpool-packages{display:flex;flex-direction:column;gap:5px;margin:0;padding:0;list-style:none}
146
+ .dsm-workbuddy-xdpool-packages li{display:flex;align-items:baseline;justify-content:space-between;gap:10px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
147
+ .dsm-workbuddy-xdpool-packages-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
148
+ .dsm-workbuddy-xdpool-packages-value{flex:none;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-variant-numeric:tabular-nums}
149
+ .dsm-workbuddy-xdpool-panel-total{position:relative;align-items:center;text-align:center;overflow:hidden}
150
+ .dsm-workbuddy-xdpool-panel-total::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;opacity:.9;background:var(--dsw-alias-state-success-primary,#22a06b)}
151
+ .dsm-workbuddy-xdpool-total-value{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:30px;line-height:34px;font-weight:700;letter-spacing:-.5px;white-space:nowrap;font-variant-numeric:tabular-nums}
133
152
 
134
153
  /* Model directory list. */
135
154
  .dsm-workbuddy-xdpool-models{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2,#36373b);padding-top:14px}
@@ -147,30 +166,44 @@ window.__ModuleLoader__.load({
147
166
  .dsm-workbuddy-xdpool-model-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
148
167
  .dsm-workbuddy-xdpool-model-meta-tag{padding:1px 8px;border-radius:999px;font-size:11px;line-height:16px;background:rgba(174,179,187,.11);color:var(--dsw-alias-label-secondary,#c6c9d0)}
149
168
  .dsm-workbuddy-xdpool-model-cap{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}
169
+ /* Model row: checkbox + image toggle + context-budget radios. */
170
+ .dsm-workbuddy-xdpool-model-off{opacity:.55}
171
+ .dsm-workbuddy-xdpool-model-check{display:flex;align-items:center;gap:8px;min-width:0;flex:1;cursor:pointer}
172
+ .dsm-workbuddy-xdpool-model-check input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe);flex:none}
173
+ .dsm-workbuddy-xdpool-model-controls{display:flex;align-items:center;gap:10px;flex:none;flex-wrap:wrap;justify-content:flex-end}
174
+ .dsm-workbuddy-xdpool-model-image{display:inline-flex;align-items:center;gap:5px;flex:none;cursor:pointer;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px}
175
+ .dsm-workbuddy-xdpool-model-image input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
176
+ .dsm-workbuddy-xdpool-model-budget{display:flex;align-items:center;gap:9px;flex:none;margin:0;padding:0;border:0;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px}
177
+ .dsm-workbuddy-xdpool-model-budget label{display:inline-flex;align-items:center;gap:4px;cursor:pointer}
178
+ .dsm-workbuddy-xdpool-model-budget input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
179
+ .dsm-workbuddy-xdpool-models-heading{display:flex;flex-direction:column;gap:2px;min-width:0}
180
+ .dsm-workbuddy-xdpool-models-actions{display:flex;align-items:center;gap:8px;flex:none}
150
181
 
151
- /* Daily check-in block: streak chips on the left, one claim button per account. */
152
- .dsm-workbuddy-xdpool-checkin{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#24262c);flex-wrap:wrap}
153
- .dsm-workbuddy-xdpool-checkin-copy{display:flex;flex-direction:column;gap:6px;min-width:0}
154
- .dsm-workbuddy-xdpool-checkin-title{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;font-weight:600;line-height:19px}
155
- .dsm-workbuddy-xdpool-checkin-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
156
- .dsm-workbuddy-xdpool-checkin-chip{padding:1px 8px;border-radius:999px;font-size:11px;line-height:18px;background:rgba(174,179,187,.11);color:var(--dsw-alias-label-secondary,#c6c9d0);font-variant-numeric:tabular-nums}
157
- .dsm-workbuddy-xdpool-checkin-chip-bonus{background:var(--dsw-alias-state-success-subtle,rgba(51,160,107,.14));color:var(--dsw-alias-state-success-primary,#22a06b)}
158
- .dsm-workbuddy-xdpool-checkin-btn{flex:none;padding:6px 16px;border-radius:9px;border:1px solid transparent;font-size:12px;font-weight:600;line-height:18px;cursor:pointer;background:var(--dsw-alias-state-success-primary,#22a06b);color:#fff;transition:opacity .16s,border-color .16s,background .16s}
182
+
183
+ /* Check-in docked under the total, inside the right-hand panel. */
184
+ .dsm-workbuddy-xdpool-checkin{display:flex;flex-direction:column;align-items:center;gap:7px;width:100%;margin-top:10px;padding-top:11px;border-top:1px solid var(--dsw-alias-border-l2,#36373b)}
185
+ .dsm-workbuddy-xdpool-checkin-meta{display:flex;flex-direction:column;align-items:center;gap:2px;width:100%}
186
+ .dsm-workbuddy-xdpool-checkin-streak{color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:17px;font-variant-numeric:tabular-nums}
187
+ .dsm-workbuddy-xdpool-checkin-daily{color:var(--dsw-alias-state-success-primary,#22a06b);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}
188
+ .dsm-workbuddy-xdpool-checkin-bonus{padding:1px 8px;border-radius:999px;font-size:11px;line-height:16px;background:var(--dsw-alias-state-success-subtle,rgba(51,160,107,.14));color:var(--dsw-alias-state-success-primary,#22a06b);text-align:center}
189
+ .dsm-workbuddy-xdpool-checkin-btn{width:100%;padding:5px 10px;border-radius:8px;border:1px solid transparent;font-size:12px;font-weight:600;line-height:18px;cursor:pointer;background:var(--dsw-alias-state-success-primary,#22a06b);color:#fff;transition:opacity .16s,border-color .16s,background .16s}
159
190
  .dsm-workbuddy-xdpool-checkin-btn:hover:not(:disabled){opacity:.88}
160
191
  .dsm-workbuddy-xdpool-checkin-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
161
192
  .dsm-workbuddy-xdpool-checkin-btn:disabled{cursor:default;background:transparent;border-color:var(--dsw-alias-border-l2,#3a3d45);color:var(--dsw-alias-label-tertiary,#9aa0a8);opacity:1}
193
+ .dsm-workbuddy-xdpool-checkin-error{color:var(--dsw-alias-state-error-primary,#ef4444);font-size:11px;line-height:16px;text-align:center;word-break:break-word}
162
194
 
163
195
  /* Inline notes + error messages. */
164
196
  .dsm-workbuddy-xdpool-note{margin:0;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:13px;line-height:20px}
165
197
  .dsm-workbuddy-xdpool-error{margin:0;color:var(--dsw-alias-state-error-primary,#ef4444);font-size:13px;line-height:20px}
166
198
 
167
- /* Responsive: collapse the two-column credit panels on narrow screens. */
199
+ /* Responsive: stack the stats columns on narrow screens. */
168
200
  @media (max-width:760px){
169
- .dsm-workbuddy-xdpool-credits-panels{grid-template-columns:1fr}
170
- .dsm-workbuddy-xdpool-credit-panel-total{align-items:flex-start;text-align:left}
171
- .dsm-workbuddy-xdpool-credit-total-body{align-items:flex-start}
172
- .dsm-workbuddy-xdpool-checkin{align-items:stretch;flex-direction:column}
173
- .dsm-workbuddy-xdpool-checkin-btn{width:100%}
201
+ .dsm-workbuddy-xdpool-stats{grid-template-columns:1fr}
202
+ .dsm-workbuddy-xdpool-panel-total{align-items:stretch;text-align:left}
203
+ .dsm-workbuddy-xdpool-total-value{text-align:left}
204
+ .dsm-workbuddy-xdpool-checkin{align-items:stretch}
205
+ .dsm-workbuddy-xdpool-checkin-meta{align-items:flex-start}
206
+ .dsm-workbuddy-xdpool-checkin-bonus{text-align:left}
174
207
  }
175
208
  `.trim();
176
209
  //#endregion
@@ -189,6 +222,12 @@ window.__ModuleLoader__.load({
189
222
  *
190
223
  * @module dsh-workbuddy-xdpool/client/PoolCard
191
224
  */
225
+ /**
226
+ * Default context window the card offers as the "capped" choice, in tokens.
227
+ * Mirrors the host-side DEFAULT_CONTEXT_BUDGET; declared here rather than
228
+ * imported, because the browser bundle must not pull in the host entry.
229
+ */
230
+ const DEFAULT_CONTEXT_BUDGET = 2e5;
192
231
  const POLL_INTERVAL_MS = 3e4;
193
232
  /** Inject or refresh the shared card CSS for the current client bundle. */
194
233
  if (typeof document !== "undefined") {
@@ -235,7 +274,39 @@ window.__ModuleLoader__.load({
235
274
  if (value >= 1e3 && value % 1e3 === 0) return `${value / 1e3}K`;
236
275
  return String(value);
237
276
  }
238
- /** Pick the right promotion chip for a model. */
277
+ /** Build the draft from the server's selection + catalog flags. */
278
+ function draftFromStatus(status) {
279
+ const selection = status.selection;
280
+ const enabled = selection.enabledModelIds;
281
+ const images = selection.imageModelIds;
282
+ const budgets = selection.contextBudgets;
283
+ const out = {};
284
+ for (const model of status.models) {
285
+ const entry = {
286
+ enabled: enabled === void 0 || enabled.includes(model.id),
287
+ images: images === void 0 ? model.supportsImages : images.includes(model.id)
288
+ };
289
+ const budget = budgets?.[model.id];
290
+ if (budget !== void 0) entry.budget = budget;
291
+ out[model.id] = entry;
292
+ }
293
+ return out;
294
+ }
295
+ /** True when the draft differs from what the server last reported. */
296
+ function draftIsDirty(status, draft) {
297
+ const selection = status.selection;
298
+ const enabled = new Set(selection.enabledModelIds ?? status.models.filter((m) => m.enabled).map((m) => m.id));
299
+ const images = new Set(selection.imageModelIds ?? status.models.filter((m) => m.supportsImages).map((m) => m.id));
300
+ const budgets = selection.contextBudgets ?? {};
301
+ for (const model of status.models) {
302
+ const entry = draft[model.id];
303
+ if (entry === void 0) continue;
304
+ if (entry.enabled !== enabled.has(model.id)) return true;
305
+ if (entry.images !== images.has(model.id)) return true;
306
+ if ((budgets[model.id] ?? model.nativeContextWindow) !== (entry.budget ?? model.nativeContextWindow)) return true;
307
+ }
308
+ return false;
309
+ }
239
310
  function tagFor(model) {
240
311
  const tags = model.tags ?? [];
241
312
  if (tags.includes("free")) return "free";
@@ -243,15 +314,32 @@ window.__ModuleLoader__.load({
243
314
  if (tags.includes("night-discount")) return "night";
244
315
  }
245
316
  /** Render pool health, per-account credits/cooldown, and the model directory. */
246
- function PoolCard({ t }) {
317
+ function PoolCard({ t, settingsScope }) {
318
+ const settingsWritable = settingsScope?.getSnapshot().writable === true;
319
+ /** Which region tab is showing. A CN-only install never leaves this. */
320
+ const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
247
321
  const [open, setOpen] = (0, react.useState)(false);
248
- const [status, setStatus] = (0, react.useState)(void 0);
322
+ /**
323
+ * Last-known status per region. Kept per region (not a single slot) so
324
+ * switching tabs shows the other side's last answer immediately instead of
325
+ * a blank frame, and the tab dots stay meaningful while a tab is hidden.
326
+ */
327
+ const [statusByRegion, setStatusByRegion] = (0, react.useState)({});
328
+ /** The document for the tab on screen; undefined until its first answer. */
329
+ const status = statusByRegion[activeRegion];
249
330
  const [error, setError] = (0, react.useState)(void 0);
250
331
  const [busy, setBusy] = (0, react.useState)(false);
251
332
  const [cooldownBusy, setCooldownBusy] = (0, react.useState)(false);
252
333
  const [flash, setFlash] = (0, react.useState)(void 0);
253
334
  /** Account id whose daily claim is currently in flight. */
254
335
  const [checkinBusyId, setCheckinBusyId] = (0, react.useState)(void 0);
336
+ /**
337
+ * Draft model selection. `undefined` means "no local edits"; once a checkbox
338
+ * is touched the draft takes over and is what the Save button posts. Discard
339
+ * drops it back to the copy the server last reported.
340
+ */
341
+ const [draft, setDraft] = (0, react.useState)(void 0);
342
+ const [savingModels, setSavingModels] = (0, react.useState)(false);
255
343
  const mounted = (0, react.useRef)(true);
256
344
  (0, react.useEffect)(() => {
257
345
  mounted.current = true;
@@ -259,9 +347,15 @@ window.__ModuleLoader__.load({
259
347
  mounted.current = false;
260
348
  };
261
349
  }, []);
262
- const refresh = (0, react.useCallback)(async (signal) => {
350
+ /**
351
+ * Fetch one region's status. `region` is a parameter rather than a closure
352
+ * read so the callback identity does not change with the tab: the polling
353
+ * effect can key off it without restarting on every switch, and each region's
354
+ * last answer stays in its own slot (see `statusByRegion`).
355
+ */
356
+ const refresh = (0, react.useCallback)(async (region, signal) => {
263
357
  try {
264
- const response = await fetch(`${POOL_STATUS_PATH}`, {
358
+ const response = await fetch(`${POOL_STATUS_PATH}?region=${region}`, {
265
359
  headers: { accept: "application/json" },
266
360
  credentials: "same-origin",
267
361
  ...signal === void 0 ? {} : { signal }
@@ -269,7 +363,10 @@ window.__ModuleLoader__.load({
269
363
  const value = await response.json().catch(() => void 0);
270
364
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
271
365
  if (mounted.current && signal?.aborted !== true) {
272
- setStatus(value);
366
+ setStatusByRegion((prev) => ({
367
+ ...prev,
368
+ [region]: value
369
+ }));
273
370
  setError(void 0);
274
371
  }
275
372
  } catch (cause) {
@@ -279,15 +376,19 @@ window.__ModuleLoader__.load({
279
376
  (0, react.useEffect)(() => {
280
377
  if (!open) return;
281
378
  const controller = new AbortController();
282
- refresh(controller.signal);
379
+ refresh(activeRegion, controller.signal);
283
380
  const timer = window.setInterval(() => {
284
- refresh(controller.signal);
381
+ refresh(activeRegion, controller.signal);
285
382
  }, POLL_INTERVAL_MS);
286
383
  return () => {
287
384
  window.clearInterval(timer);
288
385
  controller.abort();
289
386
  };
290
- }, [open, refresh]);
387
+ }, [
388
+ open,
389
+ refresh,
390
+ activeRegion
391
+ ]);
291
392
  const rescan = async () => {
292
393
  setBusy(true);
293
394
  setFlash(void 0);
@@ -299,7 +400,7 @@ window.__ModuleLoader__.load({
299
400
  });
300
401
  const body = await response.json();
301
402
  if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
302
- await refresh();
403
+ await refresh(activeRegion);
303
404
  if (mounted.current) setFlash(t?.("row.accountsRescanned", { count: body.accounts ?? 0 }) ?? "");
304
405
  } catch (cause) {
305
406
  if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
@@ -317,7 +418,7 @@ window.__ModuleLoader__.load({
317
418
  credentials: "same-origin"
318
419
  });
319
420
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
320
- await refresh();
421
+ await refresh(activeRegion);
321
422
  if (mounted.current) setFlash(t?.("row.resetCooldownsDone") ?? "");
322
423
  } catch (cause) {
323
424
  if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
@@ -346,7 +447,7 @@ window.__ModuleLoader__.load({
346
447
  });
347
448
  const body = await response.json().catch(() => void 0);
348
449
  if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
349
- await refresh();
450
+ await refresh(activeRegion);
350
451
  const credit = body?.claim?.credit ?? 0;
351
452
  if (mounted.current) setFlash(t?.("row.checkinClaimedReward", { credit: formatNumber(credit) }) ?? `Claimed +${formatNumber(credit)} credits`);
352
453
  } catch (cause) {
@@ -355,12 +456,109 @@ window.__ModuleLoader__.load({
355
456
  if (mounted.current) setCheckinBusyId(void 0);
356
457
  }
357
458
  };
459
+ /** Keep the draft in step with the server copy while nothing is dirty. */
460
+ const modelDraft = draft ?? (status === void 0 ? {} : draftFromStatus(status));
461
+ /** Model edits need a writable settings scope; otherwise the rows are read-only. */
462
+ const modelsEditable = settingsWritable;
463
+ const modelsDirty = draft !== void 0 && status !== void 0 && draftIsDirty(status, draft);
464
+ const enabledCount = Object.values(modelDraft).filter((entry) => entry.enabled).length;
465
+ const toggleModel = (id) => {
466
+ if (status === void 0) return;
467
+ const base = draft ?? draftFromStatus(status);
468
+ const entry = base[id];
469
+ if (entry === void 0) return;
470
+ setDraft({
471
+ ...base,
472
+ [id]: {
473
+ ...entry,
474
+ enabled: !entry.enabled
475
+ }
476
+ });
477
+ };
478
+ const toggleModelImage = (id) => {
479
+ if (status === void 0) return;
480
+ const base = draft ?? draftFromStatus(status);
481
+ const entry = base[id];
482
+ if (entry === void 0) return;
483
+ setDraft({
484
+ ...base,
485
+ [id]: {
486
+ ...entry,
487
+ images: !entry.images
488
+ }
489
+ });
490
+ };
491
+ const setModelBudget = (id, budget) => {
492
+ if (status === void 0) return;
493
+ const base = draft ?? draftFromStatus(status);
494
+ const entry = base[id];
495
+ if (entry === void 0) return;
496
+ setDraft({
497
+ ...base,
498
+ [id]: {
499
+ ...entry,
500
+ budget
501
+ }
502
+ });
503
+ };
504
+ const discardModels = () => {
505
+ setDraft(void 0);
506
+ setFlash(void 0);
507
+ };
508
+ /**
509
+ * Persist the draft. The route validates the payload again on the host side,
510
+ * so a malformed draft is rejected there rather than silently stored. The
511
+ * card refuses to save an empty enable-list: that would leave the picker
512
+ * with nothing to offer and no obvious way back.
513
+ */
514
+ /**
515
+ * Persist the draft into the plugin settings section.
516
+ *
517
+ * The write goes through `settingsScope` rather than a bespoke route: that is
518
+ * the same document the model picker reads, so one save covers every account
519
+ * and survives account rotation — the selection is a property of the pool,
520
+ * not of whichever account happens to be serving right now.
521
+ *
522
+ * The card refuses an empty enable-list: saving one would leave the picker
523
+ * with nothing to offer and no obvious way back.
524
+ */
525
+ const saveModels = async () => {
526
+ if (draft === void 0 || status === void 0) return;
527
+ if (enabledCount === 0) {
528
+ setError(t?.("row.modelsEmpty") ?? "No model enabled");
529
+ return;
530
+ }
531
+ const write = settingsScope?.set;
532
+ if (write === void 0) {
533
+ setError(t?.("row.modelsSaveError", { message: "settings scope is read-only" }) ?? "settings scope is read-only");
534
+ return;
535
+ }
536
+ setSavingModels(true);
537
+ setFlash(void 0);
538
+ try {
539
+ const enabledModelIds = Object.entries(draft).filter(([, e]) => e.enabled).map(([id]) => id);
540
+ const imageModelIds = Object.entries(draft).filter(([, e]) => e.images).map(([id]) => id);
541
+ const contextBudgets = {};
542
+ for (const [id, entry] of Object.entries(draft)) if (entry.budget !== void 0) contextBudgets[id] = entry.budget;
543
+ await write.call(settingsScope, "enabledModelIds", enabledModelIds);
544
+ await write.call(settingsScope, "imageModelIds", imageModelIds);
545
+ await write.call(settingsScope, "contextBudgets", contextBudgets);
546
+ setDraft(void 0);
547
+ if (mounted.current) setFlash(t?.("row.modelsSaved") ?? "Saved");
548
+ } catch (cause) {
549
+ if (mounted.current) setError(t?.("row.modelsSaveError", { message: cause instanceof Error ? cause.message : String(cause) }) ?? String(cause));
550
+ } finally {
551
+ if (mounted.current) setSavingModels(false);
552
+ }
553
+ };
358
554
  const title = t?.("row.title") ?? "WorkBuddy XD Pool";
359
555
  const description = t?.("row.desc") ?? "";
360
556
  const accountCount = status?.accounts.length ?? 0;
361
557
  const cooling = status?.cooling ?? 0;
362
558
  const state = error !== void 0 ? "error" : status === void 0 && error === void 0 ? "idle" : accountCount > 0 && cooling < accountCount ? "ok" : "idle";
363
- const stateLabel = error !== void 0 ? t?.("row.requestFailed") ?? "Request failed" : accountCount === 0 ? t?.("row.poolEmpty") ?? "No account yet" : state === "ok" ? t?.("row.ok") ?? "Healthy" : t?.("row.allCooling") ?? "All cooling";
559
+ /** Human label for the active tab, used inside the empty-state copy. */
560
+ const regionLabel = activeRegion === "cn" ? t?.("row.tabCn") ?? "CN" : t?.("row.tabGlobal") ?? "Global";
561
+ const stateLabel = error !== void 0 ? t?.("row.requestFailed") ?? "Request failed" : accountCount === 0 ? t?.("row.regionEmpty") ?? t?.("row.poolEmpty") ?? "No account yet" : state === "ok" ? t?.("row.ok") ?? "Healthy" : t?.("row.allCooling") ?? "All cooling";
364
562
  const shimRunning = status?.shim.running === true;
365
563
  const shimHint = status === void 0 ? null : shimRunning ? `${t?.("row.shimRunning") ?? "Provider listening"}${status.shim.baseUrl === void 0 ? "" : ` · ${status.shim.baseUrl}`}` : t?.("row.shimStopped") ?? "Provider loopback not running";
366
564
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
@@ -400,6 +598,23 @@ window.__ModuleLoader__.load({
400
598
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
401
599
  className: "dsm-workbuddy-xdpool-usage",
402
600
  children: [
601
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
602
+ className: "dsm-workbuddy-xdpool-tabs",
603
+ role: "tablist",
604
+ children: status?.regions.map((region) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
605
+ type: "button",
606
+ role: "tab",
607
+ "aria-selected": region === activeRegion,
608
+ className: `dsm-workbuddy-xdpool-tab${region === activeRegion ? " dsm-workbuddy-xdpool-tab-active" : ""}`,
609
+ onClick: () => {
610
+ setActiveRegion(region);
611
+ },
612
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
613
+ className: "dsm-workbuddy-xdpool-tab-dot",
614
+ "data-state": state
615
+ }), region === "cn" ? t?.("row.tabCn") ?? "CN" : t?.("row.tabGlobal") ?? "Global"]
616
+ }, region))
617
+ }),
403
618
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
404
619
  className: "dsm-workbuddy-xdpool-usage-head",
405
620
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -455,9 +670,33 @@ window.__ModuleLoader__.load({
455
670
  className: "dsm-workbuddy-xdpool-error",
456
671
  children: t?.("row.error", { message: error }) ?? `Pool status unavailable: ${error}`
457
672
  }),
458
- accountCount === 0 && error === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
459
- className: "dsm-workbuddy-xdpool-note",
460
- children: t?.("row.poolEmptyHint") ?? ""
673
+ accountCount === 0 && error === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
674
+ className: "dsm-workbuddy-xdpool-empty",
675
+ children: [
676
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
677
+ className: "dsm-workbuddy-xdpool-empty-title",
678
+ children: t?.("row.regionEmptyTitle", { region: regionLabel }) ?? t?.("row.regionEmpty") ?? "No account yet"
679
+ }),
680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
681
+ className: "dsm-workbuddy-xdpool-empty-steps",
682
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
683
+ className: "dsm-workbuddy-xdpool-empty-steps-title",
684
+ children: t?.("row.regionHowToTitle", { region: regionLabel }) ?? `How to sign in to the ${regionLabel} version`
685
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ol", {
686
+ className: "dsm-workbuddy-xdpool-empty-list",
687
+ children: [
688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("row.regionHowTo1") ?? "" }),
689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("row.regionHowTo2") ?? "" }),
690
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("row.regionHowTo3") ?? "" }),
691
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: t?.("row.regionHowTo4") ?? "" })
692
+ ]
693
+ })]
694
+ }),
695
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
696
+ className: "dsm-workbuddy-xdpool-empty-note",
697
+ children: t?.("row.regionHowToNote") ?? ""
698
+ })
699
+ ]
461
700
  }) : null,
462
701
  accountCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
463
702
  className: "dsm-workbuddy-xdpool-accounts",
@@ -489,18 +728,49 @@ window.__ModuleLoader__.load({
489
728
  "aria-label": t?.("row.modelsTitle") ?? "Models",
490
729
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
491
730
  className: "dsm-workbuddy-xdpool-models-head",
492
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
493
- className: "dsm-workbuddy-xdpool-models-title",
494
- children: t?.("row.modelsTitle") ?? "Models"
495
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
496
- className: "dsm-workbuddy-xdpool-models-summary",
497
- children: t?.("row.modelsSummary", { count: status?.models.length ?? 0 }) ?? `${status?.models.length ?? 0} model(s) in the live catalog`
731
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
732
+ className: "dsm-workbuddy-xdpool-models-heading",
733
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
734
+ className: "dsm-workbuddy-xdpool-models-title",
735
+ children: t?.("row.modelsTitle") ?? "Models"
736
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
737
+ className: "dsm-workbuddy-xdpool-models-summary",
738
+ children: t?.("row.modelsEnabledCount", {
739
+ enabled: enabledCount,
740
+ total: status?.models.length ?? 0
741
+ }) ?? `${enabledCount} / ${status?.models.length ?? 0} enabled`
742
+ })]
743
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
744
+ className: "dsm-workbuddy-xdpool-models-actions",
745
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
746
+ type: "button",
747
+ className: "dsm-btn dsm-btn-outline",
748
+ disabled: !modelsDirty || savingModels,
749
+ onClick: discardModels,
750
+ children: t?.("row.modelsDiscard") ?? "Discard"
751
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
752
+ type: "button",
753
+ className: "dsm-btn dsm-btn-primary",
754
+ disabled: !modelsDirty || savingModels || enabledCount === 0,
755
+ onClick: () => {
756
+ saveModels();
757
+ },
758
+ children: savingModels ? t?.("row.modelsSaving") ?? "Saving…" : t?.("row.modelsSave") ?? "Save"
759
+ })]
498
760
  })]
499
761
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
500
762
  className: "dsm-workbuddy-xdpool-model-list",
501
763
  children: status?.models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
502
764
  model,
503
- t
765
+ t,
766
+ draft: modelDraft[model.id] ?? {
767
+ enabled: model.enabled,
768
+ images: model.supportsImages
769
+ },
770
+ editable: modelsEditable,
771
+ onToggle: toggleModel,
772
+ onToggleImage: toggleModelImage,
773
+ onBudget: setModelBudget
504
774
  }, model.id))
505
775
  })]
506
776
  }) : null
@@ -524,60 +794,53 @@ window.__ModuleLoader__.load({
524
794
  } : null;
525
795
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
526
796
  className: "dsm-workbuddy-xdpool-account",
527
- children: [
528
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
529
- className: "dsm-workbuddy-xdpool-account-copy",
530
- children: [
531
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
532
- className: "dsm-workbuddy-xdpool-account-label",
533
- children: account.label
534
- }),
535
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
536
- className: "dsm-workbuddy-xdpool-account-tags",
537
- children: tag === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
538
- className: tag.cls,
539
- children: tag.text
540
- })
541
- }),
542
- account.domain !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
543
- className: "dsm-workbuddy-xdpool-account-meta",
544
- children: account.domain
545
- }),
546
- account.expiresAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
547
- className: "dsm-workbuddy-xdpool-account-meta",
548
- children: t?.("row.tokenExpiry", { time: formatDateTime(account.expiresAt) }) ?? `token ${formatDateTime(account.expiresAt)}`
549
- }) : null,
550
- isCooling && cooldownUntil !== void 0 && !Number.isNaN(cooldownUntil) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
551
- className: "dsm-workbuddy-xdpool-account-meta",
552
- children: [
553
- t?.("row.cooldownUntil", { time: formatTime(cooldownUntil) }) ?? `until ${formatTime(cooldownUntil)}`,
554
- " · ",
555
- t?.("row.cooldownHits", { hits: account.rateLimitHits ?? 0 }) ?? `${account.rateLimitHits ?? 0} hit(s)`
556
- ]
557
- }) : null,
558
- modelCooldowns.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
559
- className: "dsm-workbuddy-xdpool-account-modelcool",
560
- children: modelCooldowns.map((mc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
561
- className: "dsm-workbuddy-xdpool-account-modelcool-chip",
562
- children: t?.("row.modelCooling", {
563
- model: mc.modelId,
564
- time: formatDateTime(mc.until)
565
- }) ?? `${mc.modelId} cooling to ${formatDateTime(mc.until)}`
566
- }, mc.modelId))
567
- }) : null
568
- ]
569
- }),
570
- account.credits === void 0 && account.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountCredits, {
571
- account,
572
- t
573
- }),
574
- account.checkin === void 0 && account.checkinError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountCheckin, {
575
- account,
576
- t,
577
- busy: checkinBusyId === account.id,
578
- onClaim: onClaimCheckin
579
- })
580
- ]
797
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
798
+ className: "dsm-workbuddy-xdpool-account-copy",
799
+ children: [
800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
801
+ className: "dsm-workbuddy-xdpool-account-label",
802
+ children: account.label
803
+ }),
804
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
805
+ className: "dsm-workbuddy-xdpool-account-tags",
806
+ children: tag === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
807
+ className: tag.cls,
808
+ children: tag.text
809
+ })
810
+ }),
811
+ account.domain !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
812
+ className: "dsm-workbuddy-xdpool-account-meta",
813
+ children: account.domain
814
+ }),
815
+ account.expiresAt !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
816
+ className: "dsm-workbuddy-xdpool-account-meta",
817
+ children: t?.("row.tokenExpiry", { time: formatDateTime(account.expiresAt) }) ?? `token ${formatDateTime(account.expiresAt)}`
818
+ }) : null,
819
+ isCooling && cooldownUntil !== void 0 && !Number.isNaN(cooldownUntil) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
820
+ className: "dsm-workbuddy-xdpool-account-meta",
821
+ children: [
822
+ t?.("row.cooldownUntil", { time: formatTime(cooldownUntil) }) ?? `until ${formatTime(cooldownUntil)}`,
823
+ " · ",
824
+ t?.("row.cooldownHits", { hits: account.rateLimitHits ?? 0 }) ?? `${account.rateLimitHits ?? 0} hit(s)`
825
+ ]
826
+ }) : null,
827
+ modelCooldowns.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
828
+ className: "dsm-workbuddy-xdpool-account-modelcool",
829
+ children: modelCooldowns.map((mc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
830
+ className: "dsm-workbuddy-xdpool-account-modelcool-chip",
831
+ children: t?.("row.modelCooling", {
832
+ model: mc.modelId,
833
+ time: formatDateTime(mc.until)
834
+ }) ?? `${mc.modelId} cooling to ${formatDateTime(mc.until)}`
835
+ }, mc.modelId))
836
+ }) : null
837
+ ]
838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountStats, {
839
+ account,
840
+ t,
841
+ checkinBusy: checkinBusyId === account.id,
842
+ onClaim: onClaimCheckin
843
+ })]
581
844
  });
582
845
  }
583
846
  /**
@@ -585,132 +848,192 @@ window.__ModuleLoader__.load({
585
848
  * Every account in the pool gets its own button, so a multi-account user can
586
849
  * collect each reward without switching the pool's preferred account first.
587
850
  */
588
- function AccountCheckin({ account, t, busy, onClaim }) {
589
- if (account.checkinError !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
590
- className: "dsm-workbuddy-xdpool-account-error",
591
- children: t?.("row.checkinError", { message: account.checkinError }) ?? account.checkinError
592
- });
593
- const checkin = account.checkin;
594
- if (checkin === void 0) return null;
595
- const claimable = checkin.active && !checkin.todayCheckedIn;
596
- const label = !checkin.active ? t?.("row.checkinInactive") ?? "Check-in not available" : checkin.todayCheckedIn ? t?.("row.checkinClaimed") ?? "Checked in today" : busy ? t?.("row.checkinClaiming") ?? "Checking in…" : t?.("row.checkinClaim") ?? "Check in";
597
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
598
- className: "dsm-workbuddy-xdpool-checkin",
599
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
600
- className: "dsm-workbuddy-xdpool-checkin-copy",
601
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
602
- className: "dsm-workbuddy-xdpool-checkin-title",
603
- children: t?.("row.checkinTitle") ?? "Daily check-in"
604
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
605
- className: "dsm-workbuddy-xdpool-checkin-meta",
606
- children: [
607
- checkin.streakDays > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
608
- className: "dsm-workbuddy-xdpool-checkin-chip",
609
- children: t?.("row.checkinStreak", { days: checkin.streakDays }) ?? `${checkin.streakDays}-day streak`
610
- }) : null,
611
- checkin.dailyCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
612
- className: "dsm-workbuddy-xdpool-checkin-chip",
613
- children: t?.("row.checkinDaily", { credit: formatNumber(checkin.dailyCredit) }) ?? `+${formatNumber(checkin.dailyCredit)}/day`
614
- }) : null,
615
- checkin.isStreakDay ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
616
- className: "dsm-workbuddy-xdpool-checkin-chip dsm-workbuddy-xdpool-checkin-chip-bonus",
617
- children: t?.("row.checkinStreakBonus", {
618
- days: formatNumber(checkin.nextStreakDay),
619
- credit: formatNumber(checkin.streakBonusCredit)
620
- }) ?? `bonus +${formatNumber(checkin.streakBonusCredit)}`
621
- }) : null
622
- ]
623
- })]
624
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
625
- type: "button",
626
- className: "dsm-workbuddy-xdpool-checkin-btn",
627
- disabled: !claimable || busy,
628
- onClick: () => {
629
- onClaim(account.id);
630
- },
631
- children: label
632
- })]
633
- });
634
- }
635
- /** Two-panel credit layout: package list on the left, big total on the right. */
636
- function AccountCredits({ account, t }) {
637
- if (account.creditsError !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
638
- className: "dsm-workbuddy-xdpool-account-error",
639
- children: t?.("row.creditsError", { message: account.creditsError }) ?? account.creditsError
640
- });
851
+ /**
852
+ * Credit panels: package breakdown on the left, the big total on the right with
853
+ * the daily check-in action docked beneath it. Mirrors the two-column credit
854
+ * layout the LaoDing plugin family uses, so the numbers stay scannable and the
855
+ * claim button sits where the eye already is.
856
+ */
857
+ function AccountStats({ account, t, checkinBusy, onClaim }) {
641
858
  const credits = account.credits;
642
- if (credits === void 0) return null;
643
- const packages = credits.packages.filter((p) => (p.size ?? 0) > 0).slice(0, 5);
859
+ const checkin = account.checkin;
860
+ const hasCredits = credits !== void 0 || account.creditsError !== void 0;
861
+ const hasCheckin = checkin !== void 0 || account.checkinError !== void 0;
862
+ if (!hasCredits && !hasCheckin) return null;
863
+ const packages = (credits?.packages ?? []).filter((p) => (p.size ?? 0) > 0).slice(0, 6);
644
864
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
645
- className: "dsm-workbuddy-xdpool-credits-panels",
646
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
647
- className: "dsm-workbuddy-xdpool-credit-panel",
648
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
649
- className: "dsm-workbuddy-xdpool-credit-panel-title",
650
- children: t?.("row.creditsPackages") ?? "Credit packages"
651
- }), packages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
652
- className: "dsm-workbuddy-xdpool-credit-panel-value",
653
- children: "–"
654
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
655
- className: "dsm-workbuddy-xdpool-credit-packages",
656
- children: packages.map((pack, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: pack.packageName }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("row.creditsPackage", {
657
- remain: formatNumber(pack.remain),
658
- size: formatNumber(pack.size)
659
- }) ?? `${formatNumber(pack.remain)} / ${formatNumber(pack.size)}` })] }, `${pack.packageName}-${String(index)}`))
660
- })]
661
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
662
- className: "dsm-workbuddy-xdpool-credit-panel dsm-workbuddy-xdpool-credit-panel-total",
663
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
664
- className: "dsm-workbuddy-xdpool-credit-total-body",
665
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
666
- className: "dsm-workbuddy-xdpool-credit-panel-title",
865
+ className: "dsm-workbuddy-xdpool-stats",
866
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
867
+ className: "dsm-workbuddy-xdpool-panel dsm-workbuddy-xdpool-panel-packages",
868
+ children: [
869
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
870
+ className: "dsm-workbuddy-xdpool-panel-title",
871
+ children: t?.("row.creditsPackages") ?? "Credit packages"
872
+ }),
873
+ account.creditsError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
874
+ className: "dsm-workbuddy-xdpool-panel-error",
875
+ children: account.creditsError
876
+ }) : packages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
877
+ className: "dsm-workbuddy-xdpool-panel-empty",
878
+ children: "–"
879
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
880
+ className: "dsm-workbuddy-xdpool-packages",
881
+ children: packages.map((pack, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
882
+ className: "dsm-workbuddy-xdpool-packages-name",
883
+ children: pack.packageName
884
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
885
+ className: "dsm-workbuddy-xdpool-packages-value",
886
+ children: t?.("row.creditsPackage", {
887
+ remain: formatNumber(pack.remain),
888
+ size: formatNumber(pack.size)
889
+ }) ?? `${formatNumber(pack.remain)} / ${formatNumber(pack.size)}`
890
+ })] }, `${pack.packageName}-${String(index)}`))
891
+ }),
892
+ credits?.expiringSoon !== void 0 && credits.expiringSoon > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
893
+ className: "dsm-workbuddy-xdpool-panel-foot",
894
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("row.creditsSoon") ?? "Expiring in 3 days" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: formatNumber(credits.expiringSoon) })]
895
+ }) : null
896
+ ]
897
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
898
+ className: "dsm-workbuddy-xdpool-panel dsm-workbuddy-xdpool-panel-total",
899
+ children: [
900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
901
+ className: "dsm-workbuddy-xdpool-panel-title",
667
902
  children: t?.("row.creditsTotal") ?? "Total"
668
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
669
- className: "dsm-workbuddy-xdpool-credit-total-value",
670
- children: formatNumber(credits.total)
671
- })]
672
- })
903
+ }),
904
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
905
+ className: "dsm-workbuddy-xdpool-total-value",
906
+ children: formatNumber(credits?.total)
907
+ }),
908
+ hasCheckin ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
909
+ className: "dsm-workbuddy-xdpool-checkin",
910
+ children: account.checkinError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
911
+ className: "dsm-workbuddy-xdpool-checkin-error",
912
+ children: account.checkinError
913
+ }) : checkin === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
915
+ className: "dsm-workbuddy-xdpool-checkin-meta",
916
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
917
+ className: "dsm-workbuddy-xdpool-checkin-streak",
918
+ children: t?.("row.checkinStreak", { days: checkin.streakDays }) ?? `${checkin.streakDays}-day streak`
919
+ }), checkin.dailyCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
920
+ className: "dsm-workbuddy-xdpool-checkin-daily",
921
+ children: t?.("row.checkinDaily", { credit: formatNumber(checkin.dailyCredit) }) ?? `+${formatNumber(checkin.dailyCredit)}/day`
922
+ }) : null]
923
+ }),
924
+ checkin.isStreakDay && checkin.streakBonusCredit > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
925
+ className: "dsm-workbuddy-xdpool-checkin-bonus",
926
+ children: t?.("row.checkinStreakBonus", {
927
+ days: formatNumber(checkin.nextStreakDay),
928
+ credit: formatNumber(checkin.streakBonusCredit)
929
+ }) ?? `bonus +${formatNumber(checkin.streakBonusCredit)}`
930
+ }) : null,
931
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
932
+ type: "button",
933
+ className: "dsm-workbuddy-xdpool-checkin-btn",
934
+ disabled: !checkin.active || checkin.todayCheckedIn || checkinBusy,
935
+ onClick: () => {
936
+ onClaim(account.id);
937
+ },
938
+ children: !checkin.active ? t?.("row.checkinInactive") ?? "Unavailable" : checkin.todayCheckedIn ? t?.("row.checkinClaimed") ?? "Checked in" : checkinBusy ? t?.("row.checkinClaiming") ?? "Checking in…" : t?.("row.checkinClaim") ?? "Check in"
939
+ })
940
+ ] })
941
+ }) : null
942
+ ]
673
943
  })]
674
944
  });
675
945
  }
676
- /** One model row: name + rate + tag chip + image badge + context size. */
677
- function ModelRow({ model, t }) {
946
+ /**
947
+ * One model row.
948
+ *
949
+ * Read-only when the card has no writable settings scope: the checkbox and the
950
+ * context radios stay disabled rather than pretending an edit took hold. The
951
+ * draft lives in the parent, so this component only ever reports intent.
952
+ */
953
+ function ModelRow({ model, t, draft, editable, onToggle, onToggleImage, onBudget }) {
678
954
  const tag = tagFor(model);
679
955
  const tagText = tag === "free" ? t?.("row.free") ?? "free" : tag === "limited" ? t?.("row.limitedFree") ?? "limited free" : tag === "night" ? t?.("row.nightDiscount") ?? "night" : null;
680
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
681
- className: "dsm-workbuddy-xdpool-model",
682
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
956
+ const native = model.nativeContextWindow;
957
+ const capped = native > DEFAULT_CONTEXT_BUDGET;
958
+ const currentBudget = draft.budget ?? native;
959
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
960
+ className: `dsm-workbuddy-xdpool-model${draft.enabled ? "" : " dsm-workbuddy-xdpool-model-off"}`,
961
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
683
962
  className: "dsm-workbuddy-xdpool-model-head",
684
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
685
- className: "dsm-workbuddy-xdpool-model-copy",
686
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
687
- className: "dsm-workbuddy-xdpool-model-name",
688
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), model.multiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
689
- className: "dsm-workbuddy-xdpool-model-name-rate",
690
- children: t?.("row.rate", { rate: model.multiplier.toFixed(2) }) ?? `${model.multiplier.toFixed(2)}x`
963
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
964
+ className: "dsm-workbuddy-xdpool-model-check",
965
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
966
+ type: "checkbox",
967
+ checked: draft.enabled,
968
+ disabled: !editable,
969
+ onChange: () => {
970
+ onToggle(model.id);
971
+ }
972
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
973
+ className: "dsm-workbuddy-xdpool-model-copy",
974
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
975
+ className: "dsm-workbuddy-xdpool-model-name",
976
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), model.multiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
977
+ className: "dsm-workbuddy-xdpool-model-name-rate",
978
+ children: t?.("row.rate", { rate: model.multiplier.toFixed(2) }) ?? `${model.multiplier.toFixed(2)}x`
979
+ })]
980
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
981
+ className: "dsm-workbuddy-xdpool-model-id",
982
+ children: model.id
691
983
  })]
692
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
693
- className: "dsm-workbuddy-xdpool-model-id",
694
- children: model.id
695
984
  })]
696
985
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
697
- className: "dsm-workbuddy-xdpool-model-meta",
698
- children: [
699
- tagText === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
700
- className: "dsm-workbuddy-xdpool-model-meta-tag",
701
- children: tagText
702
- }),
703
- model.supportsImages === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
704
- className: "dsm-workbuddy-xdpool-model-meta-tag",
705
- children: t?.("row.imageCapable") ?? "image"
706
- }) : null,
707
- model.contextWindow === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
708
- className: "dsm-workbuddy-xdpool-model-cap",
709
- children: formatCapacity(model.contextWindow)
710
- })
711
- ]
986
+ className: "dsm-workbuddy-xdpool-model-controls",
987
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
988
+ className: "dsm-workbuddy-xdpool-model-image",
989
+ title: t?.("row.modelImage") ?? "Image input",
990
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
991
+ type: "checkbox",
992
+ checked: draft.images,
993
+ disabled: !editable,
994
+ onChange: () => {
995
+ onToggleImage(model.id);
996
+ }
997
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t?.("row.modelImage") ?? "Image" })]
998
+ }), capped ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
999
+ className: "dsm-workbuddy-xdpool-model-budget",
1000
+ "aria-label": t?.("row.modelContextBudget") ?? "Context",
1001
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1002
+ type: "radio",
1003
+ name: `budget-${model.id}`,
1004
+ checked: currentBudget === DEFAULT_CONTEXT_BUDGET,
1005
+ disabled: !editable,
1006
+ onChange: () => {
1007
+ onBudget(model.id, DEFAULT_CONTEXT_BUDGET);
1008
+ }
1009
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(DEFAULT_CONTEXT_BUDGET) })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1010
+ type: "radio",
1011
+ name: `budget-${model.id}`,
1012
+ checked: currentBudget === native,
1013
+ disabled: !editable,
1014
+ onChange: () => {
1015
+ onBudget(model.id, native);
1016
+ }
1017
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(native) })] })]
1018
+ }) : null]
712
1019
  })]
713
- })
1020
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1021
+ className: "dsm-workbuddy-xdpool-model-meta",
1022
+ children: [
1023
+ tagText === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1024
+ className: "dsm-workbuddy-xdpool-model-meta-tag",
1025
+ children: tagText
1026
+ }),
1027
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1028
+ className: "dsm-workbuddy-xdpool-model-cap",
1029
+ children: t?.("row.modelOutput", { size: formatCapacity(model.maxOutputTokens) }) ?? `out ${formatCapacity(model.maxOutputTokens)}`
1030
+ }),
1031
+ model.supportedEfforts === void 0 || model.supportedEfforts.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1032
+ className: "dsm-workbuddy-xdpool-model-cap",
1033
+ children: t?.("row.modelReasoning", { efforts: model.supportedEfforts.join(" / ") }) ?? model.supportedEfforts.join(" / ")
1034
+ })
1035
+ ]
1036
+ })]
714
1037
  });
715
1038
  }
716
1039
  //#endregion
@@ -730,9 +1053,21 @@ window.__ModuleLoader__.load({
730
1053
  "row.requestFailed": "Request failed",
731
1054
  "row.poolEmpty": "No WorkBuddy account discovered yet.",
732
1055
  "row.poolEmptyHint": "Sign in to one or more WorkBuddy accounts in the WorkBuddy desktop app, then click “Detect accounts again”. Each sign-in is picked up automatically as a pool member.",
1056
+ "row.regionEmpty": "No account signed in for this region yet.",
1057
+ "row.regionEmptyHint": "Sign in to a WorkBuddy account for this region in the desktop app, then choose “Detect accounts again”. Domestic and international accounts can be signed in side by side.",
1058
+ "row.regionEmptyTitle": "No {region} account is signed in yet.",
1059
+ "row.regionHowToTitle": "How to sign in to the {region} version",
1060
+ "row.regionHowTo1": "Download and install the {region} client: the international build is named WorkBuddy AI while the domestic one is WorkBuddy, and they are different apps. The web version at https://www.workbuddy.ai/ also works.",
1061
+ "row.regionHowTo2": "Pick a sign-in method: email sign-up / sign-in (most common), OAuth with Google, GitHub or X, or WeChat QR scan on some builds.",
1062
+ "row.regionHowTo3": "Make sure this machine can reach overseas sites when signing in: the international version targets users outside mainland China and may fail to load behind a restricted network.",
1063
+ "row.regionHowTo4": "Back here, press the re-detect button. Every sign-in the client has left on this machine is absorbed into its own region - the two sides stay separate and can run at the same time.",
1064
+ "row.regionHowToNote": "The two versions keep separate accounts, credits and data: a domestic account cannot sign in to the international one, and vice versa, so each needs its own registration. This plugin only reads the sign-ins the client has already performed.",
733
1065
  "row.shimStopped": "Provider loopback is not running.",
734
1066
  "row.shimRunning": "Provider listening on loopback",
735
1067
  "row.accountsTitle": "Accounts in the pool",
1068
+ "row.tabCn": "国内版",
1069
+ "row.tabGlobal": "国际版",
1070
+ "row.tabHint": "每个 tab 是一个独立供应商,各有自己的账号、积分与模型;两边同时生效,一侧的改动不影响另一侧。",
736
1071
  "row.accountsSummary": "{count} account(s) · {cooling} cooling",
737
1072
  "row.ok": "Healthy — requests auto-rotate across accounts",
738
1073
  "row.allCooling": "Every account is rate-limited right now; requests pause until a cooldown lifts.",
@@ -746,6 +1081,7 @@ window.__ModuleLoader__.load({
746
1081
  "row.creditsPackages": "Credit packages",
747
1082
  "row.creditsPackage": "{remain} / {size}",
748
1083
  "row.creditsError": "credits unavailable",
1084
+ "row.creditsSoon": "Expiring in 3 days",
749
1085
  "row.checkinTitle": "Daily check-in",
750
1086
  "row.checkinClaim": "Check in",
751
1087
  "row.checkinClaiming": "Checking in…",
@@ -760,6 +1096,20 @@ window.__ModuleLoader__.load({
760
1096
  "row.modelsTitle": "Models",
761
1097
  "row.modelsSummary": "{count} model(s) in the live catalog",
762
1098
  "row.modelsHint": "Read from the live WorkBuddy catalog. Free tiers are marked.",
1099
+ "row.modelEnabled": "Enabled",
1100
+ "row.modelImage": "Image input",
1101
+ "row.modelContextBudget": "Context window",
1102
+ "row.modelContextNative": "{size} (max)",
1103
+ "row.modelContextCapped": "{size}",
1104
+ "row.modelOutput": "Output {size}",
1105
+ "row.modelReasoning": "Thinking: {efforts}",
1106
+ "row.modelsEnabledCount": "{enabled} / {total} enabled",
1107
+ "row.modelsSave": "Save",
1108
+ "row.modelsSaving": "Saving…",
1109
+ "row.modelsDiscard": "Discard",
1110
+ "row.modelsSaved": "Model selection saved",
1111
+ "row.modelsSaveError": "Could not save: {message}",
1112
+ "row.modelsEmpty": "No model enabled — enable at least one before saving.",
763
1113
  "row.free": "free",
764
1114
  "row.limitedFree": "limited free",
765
1115
  "row.nightDiscount": "night",
@@ -781,9 +1131,21 @@ window.__ModuleLoader__.load({
781
1131
  "row.requestFailed": "请求失败",
782
1132
  "row.poolEmpty": "还没有发现任何 WorkBuddy 账号。",
783
1133
  "row.poolEmptyHint": "先在 WorkBuddy 桌面 App 里登录一个或多个 WorkBuddy 账号,再点“重新检测账号”。每次登录都会被自动纳入池中。",
1134
+ "row.regionEmpty": "这边还没有登录账号。",
1135
+ "row.regionEmptyHint": "在 WorkBuddy 桌面 App 里登录一个该区域的账号,再点「重新检测账号」。国内版与国际版可以同时登录,两边各自独立。",
1136
+ "row.regionEmptyTitle": "这边还没有登录{region}账号。",
1137
+ "row.regionHowToTitle": "{region}怎么登录",
1138
+ "row.regionHowTo1": "下载并安装{region}客户端:国际版安装包名称是「WorkBuddy AI」,国内版是「WorkBuddy」,两者是不同的应用。也可以直接用网页版 https://www.workbuddy.ai/ 登录。",
1139
+ "row.regionHowTo2": "登录方式(任选其一):① 邮箱注册/登录(最常用);② 用 Google、GitHub、X 等海外账号授权登录;③ 部分版本支持微信扫码。",
1140
+ "row.regionHowTo3": "登录时请确保能正常访问海外站点(国际版面向海外用户,网络受限时可能打不开或登录失败)。",
1141
+ "row.regionHowTo4": "回到这里点「重新检测账号」。App 在本机留下的每次登录都会被自动吸收到各自区域 —— 两边互相独立,可以同时使用。",
1142
+ "row.regionHowToNote": "国内版与国际版的账号、积分、数据体系完全隔离,互不相通:国内版账号无法登录国际版,反之亦然,需要各自单独注册。本插件只读取客户端已完成的登录,不会代替你登录。",
784
1143
  "row.shimStopped": "回环提供端未运行。",
785
1144
  "row.shimRunning": "提供端正在回环地址监听",
786
1145
  "row.accountsTitle": "池中账号",
1146
+ "row.tabCn": "国内版",
1147
+ "row.tabGlobal": "国际版",
1148
+ "row.tabHint": "每个 tab 是一个独立供应商,各有自己的账号、积分与模型;两边同时生效,一侧的改动不影响另一侧。",
787
1149
  "row.accountsSummary": "{count} 个账号 · {cooling} 个冷却中",
788
1150
  "row.ok": "运行健康 —— 请求会在各账号间自动轮换",
789
1151
  "row.allCooling": "当前所有账号都处于限流冷却,请求会暂停直到某个冷却结束。",
@@ -797,6 +1159,7 @@ window.__ModuleLoader__.load({
797
1159
  "row.creditsPackages": "积分包",
798
1160
  "row.creditsPackage": "{remain} / {size}",
799
1161
  "row.creditsError": "积分不可用",
1162
+ "row.creditsSoon": "3 天内到期",
800
1163
  "row.checkinTitle": "每日签到",
801
1164
  "row.checkinClaim": "签到",
802
1165
  "row.checkinClaiming": "签到中…",
@@ -811,6 +1174,20 @@ window.__ModuleLoader__.load({
811
1174
  "row.modelsTitle": "模型",
812
1175
  "row.modelsSummary": "实时目录中 {count} 个模型",
813
1176
  "row.modelsHint": "读取自 WorkBuddy 实时目录;免费档位已标注。",
1177
+ "row.modelEnabled": "启用",
1178
+ "row.modelImage": "图片输入",
1179
+ "row.modelContextBudget": "上下文窗口",
1180
+ "row.modelContextNative": "{size}(最大)",
1181
+ "row.modelContextCapped": "{size}",
1182
+ "row.modelOutput": "输出 {size}",
1183
+ "row.modelReasoning": "思考档位:{efforts}",
1184
+ "row.modelsEnabledCount": "已启用 {enabled} / {total}",
1185
+ "row.modelsSave": "保存",
1186
+ "row.modelsSaving": "保存中…",
1187
+ "row.modelsDiscard": "放弃修改",
1188
+ "row.modelsSaved": "模型选择已保存",
1189
+ "row.modelsSaveError": "保存失败:{message}",
1190
+ "row.modelsEmpty": "至少要启用一个模型才能保存。",
814
1191
  "row.free": "免费",
815
1192
  "row.limitedFree": "限量免费",
816
1193
  "row.nightDiscount": "夜间",
@@ -829,7 +1206,11 @@ window.__ModuleLoader__.load({
829
1206
  /** Stable browser-plugin name. */
830
1207
  const name = "dsh-workbuddy-xdpool-client";
831
1208
  /** Client services required by the Plugin configuration contribution. */
832
- const inject = ["slots", "locale"];
1209
+ const inject = [
1210
+ "slots",
1211
+ "locale",
1212
+ "settingsScope"
1213
+ ];
833
1214
  /** Register card copy and the pool card under Plugin configuration. */
834
1215
  function apply(ctx) {
835
1216
  try {
@@ -839,11 +1220,15 @@ window.__ModuleLoader__.load({
839
1220
  en
840
1221
  }), "dsh-workbuddy-xdpool: settings copy");
841
1222
  const t = ctx.locale.bind(namespace);
1223
+ const settingsScope = ctx.settingsScope.bind({ namespace: "workbuddy-xdpool" });
842
1224
  ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
843
1225
  name: "settings.plugin.item",
844
1226
  key: "workbuddy-xdpool",
845
1227
  priority: 30,
846
- inject: () => ({ t })
1228
+ inject: () => ({
1229
+ t,
1230
+ settingsScope
1231
+ })
847
1232
  }, PoolCard));
848
1233
  } catch (error) {
849
1234
  console.error("[dsh-workbuddy-xdpool] client card failed to load (host provider unaffected):", error);