dsh-connect-trae 3.0.1 → 3.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
@@ -33,8 +33,19 @@ window.__ModuleLoader__.load({
33
33
  */
34
34
  const TRAE_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-trae/account-credits";
35
35
  /**
36
+ * Per-account status (check-in / credits / subscription) for the account table.
37
+ *
38
+ * Each row of the account table reads its OWN state through this route — a CN
39
+ * row carries its check-in state and balance, an ai row its subscription.
40
+ * POST with the accountId in the body: one upstream read per account, paid
41
+ * only when the table is actually shown.
42
+ */
43
+ const TRAE_ACCOUNT_STATUS_PATH = "/plugins/dsh-connect-trae/account-status";
44
+ /**
36
45
  * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
37
- * route in this plugin that changes upstream account state.
46
+ * route in this plugin that changes upstream account state. The accountId in
47
+ * the body names WHICH account to claim for, so the button can sit in each
48
+ * account row.
38
49
  */
39
50
  const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
40
51
  /** Query parameter naming the region a card request addresses. */
@@ -259,9 +270,9 @@ window.__ModuleLoader__.load({
259
270
  .dsm-trae-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
260
271
  .dsm-trae-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)}
261
272
  .dsm-trae-tab-off{opacity:.55}
262
- .dsm-trae-tab-switch{display:inline-flex;align-items:center;flex:none;padding:0 8px 0 2px;cursor:pointer}
263
- .dsm-trae-tab-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
264
- .dsm-trae-tab-switch input:disabled{cursor:not-allowed}
273
+ /* Same checkbox as the settings block (.dsm-trae-switch); only the padding
274
+ differs, so it sits inside the tab cell without crowding the label. */
275
+ .dsm-trae-tab-switch{flex:none;padding:0 8px 0 2px}
265
276
  .dsm-trae-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:6px;vertical-align:baseline}
266
277
  .dsm-trae-tab-hint{margin:0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
267
278
  .dsm-trae-tab-off-notice{margin:0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
@@ -269,11 +280,109 @@ window.__ModuleLoader__.load({
269
280
  .dsm-trae-usage-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
270
281
  .dsm-trae-usage-expiry{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
271
282
  .dsm-trae-account-picker{display:block}
272
- .dsm-trae-usage-select-wrap{position:relative}
273
- .dsm-trae-usage-select{appearance:none;width:100%;font:inherit;padding:10px 34px 10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;color:var(--dsw-alias-label-primary,#e6e6e6);background:var(--dsw-alias-bg-layer-3,#2a2c33);cursor:pointer;transition:border-color .15s,box-shadow .15s}
274
- .dsm-trae-usage-select:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary,#5686fe);box-shadow:0 0 0 3px rgba(86,134,254,.22)}
275
- .dsm-trae-usage-select:disabled{opacity:.6;cursor:default}
276
- .dsm-trae-usage-select-wrap::after{content:"";position:absolute;top:50%;right:12px;width:7px;height:7px;transform:translateY(-65%) rotate(45deg);border-right:1.6px solid var(--dsw-alias-label-secondary,#c6c9d0);border-bottom:1.6px solid var(--dsw-alias-label-secondary,#c6c9d0);pointer-events:none}
283
+ /* The 设置 block: label + hint on the left, control on the right, one save for
284
+ the whole block. It replaces the bare checkbox rows that used to write on
285
+ every click — a draft needs a visible way back out, so each row carries its
286
+ own explanation and the footer carries the buttons. */
287
+ /* The preference switches: BARE LINES, one per switch, each with its label
288
+ wrapped around the box so the whole line is a click target.
289
+ *
290
+ * The 设置 block used to be a bordered box with a heading, two rows and a
291
+ * 「保存」/「放弃」 footer. That is gone with the draft it existed to serve — this
292
+ * is the sibling dsh-connect-workbuddy card's shape, where a preference is a
293
+ * checkbox on a line that commits on the click. */
294
+ .dsm-trae-prefs{display:flex;flex-direction:column;gap:8px}
295
+ .dsm-trae-pref{display:flex;align-items:flex-start;gap:8px;font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0);cursor:pointer}
296
+ .dsm-trae-pref input{flex:none;margin:2px 0 0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
297
+ .dsm-trae-pref input:disabled{cursor:default}
298
+ .dsm-trae-pref:has(input:disabled){opacity:.6;cursor:default}
299
+ .dsm-trae-pref b{font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6)}
300
+ .dsm-trae-pref-hint{color:var(--dsw-alias-label-tertiary,#9aa0a8)}
301
+ .dsm-trae-pref span{min-width:0}
302
+ /* The card's checkboxes are the platform's own, styled only with
303
+ accent-color — the SAME idiom .dsm-trae-mainui-switch-label above already
304
+ uses for the composer-line switch, and the one the sibling
305
+ dsh-connect-workbuddy card uses for its tab and credits boxes.
306
+ *
307
+ * These were drawn by hand for one release: a 34x20 track with a 14px knob,
308
+ * --dsw-alias-brand-primary when checked. That token is DSH's CONTRAST colour
309
+ * (#0f1115 light / #f9fafb dark), not an accent, so a checked box came out as a
310
+ * solid black pill — heavier than anything else in the card, and unrelated to
311
+ * the native box sitting two centimetres away in the composer toolbar. The
312
+ * platform checkbox tracks the host's own accent and dark mode for free. */
313
+ .dsm-trae-switch{display:inline-flex;align-items:center;cursor:pointer}
314
+ .dsm-trae-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
315
+ .dsm-trae-switch input:disabled{cursor:default}
316
+ .dsm-trae-switch:has(input:disabled){opacity:.6;cursor:default}
317
+ /* The account block is a real table: the question it answers is "which of these
318
+ accounts, and what is the action on each one" — a dropdown can only ever show
319
+ the one already chosen. */
320
+ .dsm-trae-accounts{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:14px;background:var(--dsw-alias-bg-layer-2,#24262c)}
321
+ .dsm-trae-accounts-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
322
+ /* The refresh button must not be squeezed into two lines by a long hint. */
323
+ .dsm-trae-accounts-head>button{flex:none;white-space:nowrap}
324
+ .dsm-trae-accounts-title{margin:0;font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary,#e6e6e6)}
325
+ .dsm-trae-accounts-hint{margin:3px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#9aa0a8)}
326
+ /* The account rows: two lines each — who the account is, then what it is worth
327
+ — with the one action that applies to THAT row on the right. A four-column
328
+ table was the wrong instrument here: every field demanded its own header, the
329
+ columns drifted with the content ("LaoDing" vs a full expiry stamp), and the
330
+ bound row repeated its "in use" badge in two different columns. */
331
+ /* The account table, shaped like the sibling WorkBuddy card's pool table: a
332
+ bordered container, a tinted header row, and one grid row per account whose
333
+ tracks are FRACTIONS of the container rather than sizes derived from the
334
+ content. That is the whole point — the previous four-column table let each
335
+ column grow with its own text, so the columns did not line up from row to row
336
+ and the table read as noise in a narrow panel. */
337
+ .dsm-trae-account-table{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
338
+ /* Four tracks where the region has a check-in column, three where it does not
339
+ (the international region has no check-in at all: the ug/* family answers
340
+ 404 on every ai gateway, so the column is absent rather than empty and
341
+ permanently unknown). The freed track goes to the account name. */
342
+ .dsm-trae-account-row{display:grid;grid-template-columns:1.7fr 1.1fr 1fr 1.1fr;gap:10px;align-items:center;padding:9px 12px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18));font-size:13px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
343
+ .dsm-trae-account-table:not(.dsm-trae-account-table-checkin) .dsm-trae-account-row{grid-template-columns:2.2fr 1.2fr 1.1fr}
344
+ .dsm-trae-account-row:first-child{border-top:0}
345
+ .dsm-trae-account-row-head{padding:7px 12px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}
346
+ /* The serving row, findable at a glance: the accent bar is what distinguishes it
347
+ in a long table, where the 7%-opacity wash alone reads as a zebra stripe. */
348
+ .dsm-trae-account-row-current{background:rgba(86,134,254,.07);box-shadow:inset 3px 0 0 var(--dsw-alias-brand-primary,#5686fe)}
349
+ .dsm-trae-account-who{display:flex;align-items:center;gap:8px;min-width:0}
350
+ .dsm-trae-account-who .dsm-trae-usage-dot{margin-right:0;flex:none}
351
+ .dsm-trae-account-name{display:flex;flex-direction:column;gap:1px;min-width:0}
352
+ .dsm-trae-account-name b{font-weight:500;color:var(--dsw-alias-label-primary,#e6e6e6);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
353
+ .dsm-trae-account-row-current .dsm-trae-account-name b{font-weight:600}
354
+ /* Declared AFTER the blanket span rule above so it wins: the badge must not
355
+ inherit the muted 11px tertiary treatment the edition line uses. */
356
+ .dsm-trae-account-name .dsm-trae-account-badge{align-self:flex-start;margin-top:2px;padding:1px 6px;border-radius:999px;font-size:10px;line-height:15px;font-weight:600;white-space:nowrap;color:var(--dsw-alias-brand-primary,#5686fe);background:rgba(86,134,254,.14)}
357
+ .dsm-trae-account-meta{display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#9aa0a8);min-width:0}
358
+ .dsm-trae-account-meta i{font-style:normal;opacity:.4}
359
+ .dsm-trae-account-note{font-size:11px;line-height:16px;color:var(--dsw-alias-state-error-primary,#ef4444)}
360
+ /* Its own track, in tabular figures, so digits in different rows sit on the same
361
+ column of the grid instead of jittering with the glyph widths. */
362
+ .dsm-trae-account-worth{display:flex;flex-direction:column;gap:1px;min-width:0;font-variant-numeric:tabular-nums}
363
+ .dsm-trae-account-value{color:var(--dsw-alias-label-primary,#e6e6e6);font-weight:600}
364
+ .dsm-trae-account-value-on{color:var(--dsw-alias-state-success-primary,#22a06b)}
365
+ .dsm-trae-account-checkin-cell{display:flex;align-items:center;min-width:0}
366
+ .dsm-trae-account-actions{display:flex;align-items:center;gap:6px;justify-content:flex-end;flex:none}
367
+ /* The per-row switch is the shared outline button, only smaller: one button
368
+ vocabulary in the card instead of a bespoke one per block. */
369
+ .dsm-trae-account-row .dsm-trae-account-switch{padding:2px 10px;font-size:12px}
370
+ @media (max-width:760px){.dsm-trae-account-row,.dsm-trae-account-table:not(.dsm-trae-account-table-checkin) .dsm-trae-account-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-trae-account-row-head{display:none}}
371
+ /* The check-in is the only action on this card that reaches upstream account
372
+ state, so it is the one button allowed a colour. Everything else in the row
373
+ defers to it. */
374
+ .dsm-trae-account-checkin{appearance:none;font:inherit;cursor:pointer;white-space:nowrap;border:1px solid rgba(86,134,254,.4);border-radius:7px;padding:2px 9px;font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-brand-primary,#5686fe);background:rgba(86,134,254,.08)}
375
+ .dsm-trae-account-checkin:hover:not(:disabled){background:rgba(86,134,254,.16)}
376
+ .dsm-trae-account-checkin:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
377
+ /* A disabled check-in still STATES something ("claimed", "device used"), so it
378
+ stays readable — clearly inactive, not invisible. */
379
+ .dsm-trae-account-checkin:disabled{opacity:.55;cursor:default;border-color:transparent;color:var(--dsw-alias-label-tertiary,#9aa0a8);background:transparent}
380
+ /* The switch is the row's LIVE control, so it is brighter than the inert
381
+ check-in labels beside it: same shape, different weight. */
382
+ .dsm-trae-account-switch{appearance:none;font:inherit;cursor:pointer;white-space:nowrap;border:0;border-radius:7px;padding:3px 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0);background:transparent}
383
+ .dsm-trae-account-switch:hover:not(:disabled){color:var(--dsw-alias-brand-primary,#5686fe);background:rgba(86,134,254,.12)}
384
+ .dsm-trae-account-switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
385
+ .dsm-trae-account-switch:disabled{opacity:.45;cursor:default}
277
386
  .dsm-trae-mark{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:5px;font-size:11px;font-weight:700;color:#111;background:linear-gradient(135deg,#ffd36e,#ffb84d);margin-right:6px}
278
387
  .dsm-trae-usage-list{display:flex;flex-direction:column;gap:10px}
279
388
  .dsm-trae-usage-text{margin:0;font-size:14px;line-height:22px;color:var(--dsw-alias-label-secondary,#b8b8b8)}
@@ -316,11 +425,10 @@ window.__ModuleLoader__.load({
316
425
  checkbox rendered unstyled and read as absent — the whole reason the switch
317
426
  looked missing. */
318
427
  .dsm-trae-models-actions{display:flex;align-items:center;gap:8px;flex:none}
319
- }
320
- .dsm-trae-usage-alternative{display:flex;align-items:flex-start;gap:10px;margin-top:10px;padding:10px 12px;border:1px solid var(--dsw-alias-state-warning-border,#7a5b12);border-radius:10px;background:var(--dsw-alias-state-warning-bg,#2a2313)}
428
+ .dsm-trae-usage-alternative{display:flex;align-items:flex-start;gap:10px;margin-top:10px;padding:10px 12px;border:1px solid var(--dsw-alias-state-warn-secondary,#fbbf24);border-radius:10px;background:var(--dsw-alias-state-warn-tertiary,#fef3c7)}
321
429
  .dsm-trae-usage-alternative-text{margin:0;flex:1 1 auto;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e6e6e6)}
322
430
  .dsm-trae-usage-switch-button{flex:0 0 auto;padding:5px 10px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2c2f36);color:var(--dsw-alias-label-primary,#e6e6e6);font-size:12px;line-height:16px;cursor:pointer}
323
- .dsm-trae-usage-switch-button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4,#333740)}
431
+ .dsm-trae-usage-switch-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14))}
324
432
  .dsm-trae-usage-switch-button:disabled{opacity:.5;cursor:not-allowed}
325
433
  .dsm-trae-usage-note{margin:8px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
326
434
  .dsm-trae-models-hint{margin:4px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
@@ -329,7 +437,7 @@ window.__ModuleLoader__.load({
329
437
  the model. */
330
438
  .dsm-trae-model-probe{flex:none;border-radius:999px;padding:1px 7px;font-size:10px;font-weight:500;line-height:15px;white-space:nowrap}
331
439
  .dsm-trae-model-probe-available{color:var(--dsw-alias-state-success-primary,#22a06b);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#22a06b) 40%,transparent)}
332
- .dsm-trae-model-probe-gated,.dsm-trae-model-probe-unsupported{color:var(--dsw-alias-state-warning-primary,#d98f20);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d98f20) 40%,transparent)}
440
+ .dsm-trae-model-probe-gated,.dsm-trae-model-probe-unsupported{color:var(--dsw-alias-state-warn-primary,#f59e0b);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 40%,transparent)}
333
441
  .dsm-trae-model-probe-auth{color:var(--dsw-alias-state-error-primary,#d92d20);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 40%,transparent)}
334
442
  .dsm-trae-model-probe-rate-limited,.dsm-trae-model-probe-unknown{color:var(--dsw-alias-label-tertiary,#999);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#999) 35%,transparent)}
335
443
  .dsm-trae-model-meta{display:flex;align-items:center;gap:7px 12px;flex-wrap:wrap;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
@@ -475,7 +583,7 @@ window.__ModuleLoader__.load({
475
583
  background:var(--dsw-alias-label-dimmed,rgba(127,127,127,.5));
476
584
  }
477
585
  .dsm-trae-composer-panel-table tr.dsm-trae-composer-panel-row-current .dsm-trae-composer-panel-dot{
478
- background:var(--dsw-state-success,#22a06b);
586
+ background:var(--dsw-alias-state-success-primary,#22c55e);
479
587
  }
480
588
  .dsm-trae-composer-panel-foot{
481
589
  display:flex;justify-content:space-between;gap:8px;
@@ -486,6 +594,52 @@ window.__ModuleLoader__.load({
486
594
  .dsm-trae-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
487
595
  .dsm-trae-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
488
596
  `;
597
+ //#endregion
598
+ //#region src/client/plan-text.ts
599
+ /**
600
+ * The international region's "what plan is this account on" text.
601
+ *
602
+ * WHY THIS IS ITS OWN MODULE: three surfaces now answer that one question — the
603
+ * card's subscription box, the card's per-account rows, and the composer
604
+ * readout. Each previously carried its own copy of the verdict, which is how the
605
+ * card's box and its roster row were able to disagree about one account's tier
606
+ * (the box read the usage document's top-level `payStatus`, which the ai document
607
+ * does not always carry, while the row read the per-account state). A second
608
+ * implementation of the same rule is free to drift; there is now exactly one.
609
+ *
610
+ * @module dsh-connect-trae/client/plan-text
611
+ */
612
+ /**
613
+ * Decide whether an international account is on a paid plan, and name the plan.
614
+ *
615
+ * Two independent things can entitle it: a package row (`has_package`) and a
616
+ * plan identity (`user_pay_identity_str`). Reading only the first is what
617
+ * rendered a Pro member as "No active package" in issue #19, so either one
618
+ * counts. The tier string rides separately so a caller can name the plan instead
619
+ * of only asserting a boolean.
620
+ */
621
+ function planVerdict(payStatus) {
622
+ const tier = payStatus.payIdentityStr?.trim();
623
+ const paid = payStatus.hasPackage || traePlanIsPaid(payStatus.payIdentityStr);
624
+ return {
625
+ paid,
626
+ ...tier === void 0 || tier === "" ? {} : { tier },
627
+ label: paid ? "row.subscribed" : "row.noPackage"
628
+ };
629
+ }
630
+ /**
631
+ * The verdict as ONE short string, for a surface that has a single slot.
632
+ *
633
+ * The composer readout is that surface: it is a few characters wide in the tool
634
+ * row, so it gets the tier name alone (`Pro`, `Free`) rather than the card's
635
+ * two-line label-plus-tier. When the upstream named no tier the label stands in,
636
+ * which is still strictly more than the dash the readout used to show for the
637
+ * whole international region.
638
+ */
639
+ function planText(payStatus, t) {
640
+ const verdict = planVerdict(payStatus);
641
+ return verdict.tier === void 0 ? t(verdict.label) : verdict.tier;
642
+ }
489
643
  //#endregion
490
644
  //#region src/client/TraeUsageCard.tsx
491
645
  /**
@@ -558,24 +712,6 @@ window.__ModuleLoader__.load({
558
712
  return formatNumber(value);
559
713
  }
560
714
  /**
561
- * The subscription row's verdict.
562
- *
563
- * Two independent things can entitle an international account: a package row
564
- * (`has_package`) and a plan identity (`user_pay_identity_str`). Reading only
565
- * the first is what rendered a Pro member as "No active package" in issue #19,
566
- * so either one counts. The tier string is carried separately so the row can
567
- * name the plan instead of just asserting a boolean.
568
- */
569
- function payVerdict(payStatus) {
570
- const tier = payStatus.payIdentityStr?.trim();
571
- const paid = payStatus.hasPackage || traePlanIsPaid(payStatus.payIdentityStr);
572
- return {
573
- paid,
574
- ...tier === void 0 || tier === "" ? {} : { tier },
575
- label: paid ? "row.subscribed" : "row.noPackage"
576
- };
577
- }
578
- /**
579
715
  * The verdict badge for one model, or null when it has not been tested.
580
716
  *
581
717
  * `unknown` renders as inconclusive, never as a failure: a throttle or a dropped
@@ -641,14 +777,6 @@ window.__ModuleLoader__.load({
641
777
  /** Region whose on/off checkbox write is in flight, so its box can't race. */
642
778
  const [togglingRegion, setTogglingRegion] = (0, react.useState)(void 0);
643
779
  /**
644
- * Whether a check-in claim is in flight. A single flag rather than a
645
- * per-region map: the claim exists only on the CN tab, and one shared flag
646
- * keeps a tab switch from stranding a second concurrent claim.
647
- */
648
- const [claiming, setClaiming] = (0, react.useState)(false);
649
- /** Last claim refusal, shown until the next successful refresh. */
650
- const [claimError, setClaimError] = (0, react.useState)(void 0);
651
- /**
652
780
  * Outcome of the last "refresh accounts" press (issue #25).
653
781
  *
654
782
  * The rescan answers 200 with a fresh account list whether or not anything
@@ -659,21 +787,62 @@ window.__ModuleLoader__.load({
659
787
  */
660
788
  const [rescanNote, setRescanNote] = (0, react.useState)(void 0);
661
789
  /**
662
- * Whether this MACHINE has spent today's check-in while the selected account
663
- * has not been paid — the "switched account" state. Kept separate from
664
- * `claimError` because it is not a failure: it is the upstream's one-per-
665
- * device-per-day rule, and rendering it through the "签到失败:…" line would
666
- * present a normal rule as a plugin fault.
790
+ * A preference the user just flipped, held only until its write lands.
791
+ *
792
+ * The 设置 block used to stage both switches in a draft written by 「保存」.
793
+ * That is GONE: the sibling dsh-connect-workbuddy card commits its preference
794
+ * checkboxes on the click, and a Save button for two booleans asks the user to
795
+ * remember something that has no reason to need remembering.
796
+ *
797
+ * What remains is the smallest thing that keeps a click honest: the box reads
798
+ * this optimistic value until the write settles, then falls back to the
799
+ * committed one. So a flip shows immediately, and a write that FAILS snaps the
800
+ * box back to the Host's value instead of leaving it claiming one that never
801
+ * landed.
802
+ */
803
+ const optimisticSettings = (0, react.useRef)({});
804
+ /** True while a preference is crossing the wire; serializes both writers. */
805
+ const [savingSettings, setSavingSettings] = (0, react.useState)(false);
806
+ /**
807
+ * Per-account status (check-in / balances / subscription) shown in each row of
808
+ * the account table, keyed by region then account id. Loaded per account via
809
+ * `TRAE_ACCOUNT_STATUS_PATH` once the table is shown; a row that failed to
810
+ * load (or that has not been read yet) simply renders without its figure,
811
+ * exactly as the composer table treats a missing balance.
667
812
  */
668
- const [deviceSpentNotice, setDeviceSpentNotice] = (0, react.useState)(false);
813
+ const [accountStatusByRegion, setAccountStatusByRegion] = (0, react.useState)({
814
+ cn: {},
815
+ ai: {}
816
+ });
817
+ /** Which account row is claiming right now, so only its button spins. */
818
+ const [claimingFor, setClaimingFor] = (0, react.useState)(void 0);
819
+ /**
820
+ * Per-account claim refusals, keyed by region then account id. A refusal is
821
+ * shown next to the row that caused it rather than in one global error line,
822
+ * because the check-in button now lives per account.
823
+ */
824
+ const [claimErrorByRegion, setClaimErrorByRegion] = (0, react.useState)({
825
+ cn: {},
826
+ ai: {}
827
+ });
828
+ /**
829
+ * Whether this MACHINE has spent today's check-in for a given account — the
830
+ * "device is spent but this account was never rewarded" state, per account so
831
+ * it appears next to the row that owns it. Kept separate from the claim error
832
+ * because it is not a failure: it is the upstream's one-per-device-per-day
833
+ * rule, and rendering it through the "签到失败:…" line would present a normal
834
+ * rule as a plugin fault.
835
+ */
836
+ const [deviceSpentByRegion, setDeviceSpentByRegion] = (0, react.useState)({
837
+ cn: {},
838
+ ai: {}
839
+ });
669
840
  /**
670
841
  * A refused or unpersisted settings write, surfaced instead of silently
671
842
  * reverting. On 0.1.7 a rejected write resolves normally, so without this
672
843
  * the control would flip and then quietly snap back on the next snapshot.
673
844
  */
674
845
  const [writeError, setWriteError] = (0, react.useState)(void 0);
675
- /** True while the composer-credit switch write is in flight. */
676
- const [togglingCredits, setTogglingCredits] = (0, react.useState)(false);
677
846
  const mounted = (0, react.useRef)(true);
678
847
  (0, react.useEffect)(() => {
679
848
  mounted.current = true;
@@ -682,21 +851,78 @@ window.__ModuleLoader__.load({
682
851
  };
683
852
  }, []);
684
853
  /**
685
- * Toggle the composer credit readout (`showPointsInMainUi`). The card's
686
- * checkbox is the ONLY writer of this field — the readout itself carries no
687
- * control — so there is exactly one path that can change it, and the write is
688
- * read-back-checked like every other settings write in this card.
854
+ * The 设置 block: its committed values, its draft, and the ONE write path.
855
+ *
856
+ * The block commits on 「保存」 rather than on every click, so 「放弃」 has a
857
+ * meaning and a half-finished edit cannot land in the settings document. Both
858
+ * fields are written through `writeSettingsField`, which read-back checks the
859
+ * document — `set()` resolving has never been proof a value landed on this
860
+ * Host, so a saved-looking switch that silently reverted is exactly the
861
+ * failure that guard exists for.
862
+ *
863
+ * Committed values are read from the settings document, never echoed from
864
+ * local state: a write made elsewhere (the DSH settings form, another window)
865
+ * then shows up here instead of being shadowed by a stale copy.
866
+ */
867
+ const committedSettings = {
868
+ followAppLogin: followAppLoginEnabled(settingsScope?.getSnapshot().value),
869
+ showPointsInMainUi: unwrapVolatileDeep(settingsScope?.getSnapshot().value)?.showPointsInMainUi === true
870
+ };
871
+ /**
872
+ * Ids for the block's label/hint wiring. `useId`, not constants: DSH can mount
873
+ * this card twice at once (the settings ROW and the expanded PAGE), and two
874
+ * copies sharing an id would leave the second switch labelled by the first
875
+ * card's element.
876
+ */
877
+ /**
878
+ * What the two boxes render: the committed value, with any in-flight flip laid
879
+ * over it. Reading BOTH sources here is what lets the write above stay simple —
880
+ * it never has to write into component state that a Host refresh would fight.
881
+ */
882
+ const settingsEdits = {
883
+ followAppLogin: optimisticSettings.current.followAppLogin ?? committedSettings.followAppLogin,
884
+ showPointsInMainUi: optimisticSettings.current.showPointsInMainUi ?? committedSettings.showPointsInMainUi
885
+ };
886
+ const settingsIds = (0, react.useId)();
887
+ const followLabelId = `${settingsIds}-follow`;
888
+ const followHintId = `${settingsIds}-follow-hint`;
889
+ const creditsLabelId = `${settingsIds}-credits`;
890
+ const creditsHintId = `${settingsIds}-credits-hint`;
891
+ /**
892
+ * Write ONE preference switch, on the click that flipped it.
893
+ *
894
+ * This replaced a draft + 「保存」/「放弃」 pair, which is what the sibling
895
+ * dsh-connect-workbuddy card does NOT have: its switches are plain checkboxes
896
+ * on a bare line that commit immediately, and a card that needs a Save button
897
+ * for two booleans is asking the user to remember something for no reason.
898
+ *
899
+ * The two writers are SERIALIZED by `savingSettings`, because they write the
900
+ * same settings document. The pattern is the tab bar's `toggleRegion`: flip
901
+ * optimistically so the box responds, then re-read the Host so a failed write
902
+ * snaps it back instead of leaving the box claiming a value that never landed.
689
903
  */
690
- const toggleShowPointsInMainUi = async (enabled) => {
904
+ const writePreference = async (field, value) => {
691
905
  if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
692
- setTogglingCredits(true);
906
+ setSavingSettings(true);
693
907
  setWriteError(void 0);
908
+ optimisticSettings.current = {
909
+ ...optimisticSettings.current,
910
+ [field]: value
911
+ };
912
+ setSettingsRevision((revision) => revision + 1);
694
913
  try {
695
- await writeSettingsField(settingsScope, "showPointsInMainUi", enabled, (readBack) => unwrapVolatileDeep(readBack)?.showPointsInMainUi === enabled);
914
+ await writeSettingsField(settingsScope, field, value, (readBack) => field === "followAppLogin" ? followAppLoginEnabled(readBack) === value : unwrapVolatileDeep(readBack)?.showPointsInMainUi === value);
696
915
  } catch (error) {
697
916
  if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
698
917
  } finally {
699
- if (mounted.current) setTogglingCredits(false);
918
+ if (mounted.current) {
919
+ optimisticSettings.current = {
920
+ ...optimisticSettings.current,
921
+ [field]: void 0
922
+ };
923
+ setSettingsRevision((revision) => revision + 1);
924
+ }
925
+ if (mounted.current) setSavingSettings(false);
700
926
  }
701
927
  };
702
928
  (0, react.useEffect)(() => settingsScope?.subscribe(() => {
@@ -761,13 +987,12 @@ window.__ModuleLoader__.load({
761
987
  accounts: []
762
988
  };
763
989
  /**
764
- * Drop a claim refusal when the tab changes. The error belongs to the region
765
- * it came from — carrying it onto the other tab would attribute an
766
- * international account's problem to the CN one (or the reverse).
990
+ * Clear in-flight claim state when the tab changes. `activeRegion` is a single
991
+ * value across both tabs, so a switch must not leave one row's claim spinning
992
+ * under the other tab.
767
993
  */
768
994
  (0, react.useEffect)(() => {
769
- setClaimError(void 0);
770
- setDeviceSpentNotice(false);
995
+ setClaimingFor(void 0);
771
996
  }, [activeRegion]);
772
997
  (0, react.useEffect)(() => {
773
998
  if (!open || !activeRegionOn || status.status !== "signed-in") return;
@@ -797,17 +1022,42 @@ window.__ModuleLoader__.load({
797
1022
  });
798
1023
  const body = await response.json();
799
1024
  if (!response.ok || !Array.isArray(body.accounts)) throw new Error(`HTTP ${response.status}`);
800
- const selected = body.accounts.find((account) => account.selected);
1025
+ /**
1026
+ * What this press must persist, and why it is NOT `accounts.find(selected)`.
1027
+ *
1028
+ * The route's `selected` flag answers "which account is IN USE" — and the
1029
+ * store reports the account it is bound to, OR, when no binding has ever
1030
+ * been saved, the DETECTED default (`preferred`, i.e. whatever the Trae
1031
+ * app is signed in to). Those are the same value on a machine that has
1032
+ * been used for a while and diverge on exactly the press where it matters:
1033
+ * a fresh install, or a Trae sign-in the plugin has not recorded yet.
1034
+ *
1035
+ * Reading the flag alone therefore meant "nothing was explicitly bound"
1036
+ * reduced to "the detected default is what is in use, write nothing" —
1037
+ * so the account the user just refreshed was shown as bound while
1038
+ * `accounts.<region>` stayed EMPTY, and the next store rebuild (restart,
1039
+ * or a Trae sign-in change) silently re-defaulted somewhere else. That is
1040
+ * the "refreshed account was not saved" report.
1041
+ *
1042
+ * The rule is now: persist the account this region is actually using
1043
+ * (the store's verdict, which is the truth the card is displaying), and
1044
+ * fall back to the region's own roster only when the route returned no
1045
+ * verdict at all. A row that is already the SAVED binding is left alone —
1046
+ * one press must not rewrite the settings document for nothing.
1047
+ */
1048
+ const inUse = body.accounts.find((account) => account.selected) ?? body.accounts[0];
801
1049
  const configured = configuredAccountsOf(settingsScope?.getSnapshot().value);
802
- const followed = selected !== void 0 && selected.id !== configured[activeRegion] && settingsScope !== void 0 && settingsScope.getSnapshot().writable === true;
803
- if (followed && settingsScope !== void 0) await writeSettingsField(settingsScope, "accounts", {
1050
+ const alreadyBound = inUse !== void 0 && inUse.id === configured[activeRegion];
1051
+ const writable = settingsScope !== void 0 && settingsScope.getSnapshot().writable === true;
1052
+ const saved = inUse !== void 0 && !alreadyBound && writable;
1053
+ if (saved && settingsScope !== void 0 && inUse !== void 0) await writeSettingsField(settingsScope, "accounts", {
804
1054
  ...configured,
805
- [activeRegion]: selected.id
806
- }, (readBack) => configuredAccountsOf(readBack)[activeRegion] === selected.id);
1055
+ [activeRegion]: inUse.id
1056
+ }, (readBack) => configuredAccountsOf(readBack)[activeRegion] === inUse.id);
807
1057
  await refreshUsage(activeRegion);
808
1058
  if (mounted.current) setRescanNote(body.accounts.length === 0 ? t("row.accountsRefreshedNone") : t("row.accountsRefreshed", {
809
1059
  count: String(body.accounts.length),
810
- selected: followed ? "" : ` · ${t("row.accountsKept")} ${selected?.accountName ?? selected?.id ?? "—"}`
1060
+ selected: saved ? ` · ${t("row.accountsSaved")} ${inUse.accountName ?? inUse.id}` : ` · ${t("row.accountsKept")} ${inUse?.accountName ?? configured[activeRegion] ?? "—"}`
811
1061
  }));
812
1062
  } catch (error) {
813
1063
  if (mounted.current) {
@@ -829,6 +1079,7 @@ window.__ModuleLoader__.load({
829
1079
  [activeRegion]: accountId
830
1080
  }, (readBack) => configuredAccountsOf(readBack)[activeRegion] === accountId);
831
1081
  await refreshUsage(activeRegion);
1082
+ loadAccountStatus(accountId, activeRegion);
832
1083
  } catch (error) {
833
1084
  if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
834
1085
  } finally {
@@ -851,30 +1102,61 @@ window.__ModuleLoader__.load({
851
1102
  await switchAccount(status.appLogin.id);
852
1103
  };
853
1104
  /**
854
- * Toggle the follow-the-app switch (`followAppLogin`, issue #29): the Host's
855
- * automatic rebind follows this field, so the card only commits it. OFF by
856
- * default in the schema; an existing config keeps the strict bind it shipped
857
- * with until the user ticks this.
1105
+ * Load one account row's per-account state (check-in / balances /
1106
+ * subscription) and cache it for the row. Called when the table renders and
1107
+ * again after a claim so the reward/refusal lands on the row that owns it.
858
1108
  */
859
- const toggleFollowAppLogin = async (enabled) => {
860
- if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
861
- setWriteError(void 0);
1109
+ const loadAccountStatus = async (accountId, region = activeRegion) => {
862
1110
  try {
863
- await writeSettingsField(settingsScope, "followAppLogin", enabled, (readBack) => followAppLoginEnabled(readBack) === enabled);
864
- } catch (error) {
865
- if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
1111
+ const url = `${withTraeRegion(TRAE_ACCOUNT_STATUS_PATH, region)}&accountId=${encodeURIComponent(accountId)}`;
1112
+ const response = await fetch(url, {
1113
+ headers: { accept: "application/json" },
1114
+ credentials: "same-origin"
1115
+ });
1116
+ const body = await response.json().catch(() => ({}));
1117
+ if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
1118
+ if (mounted.current) setAccountStatusByRegion((prev) => ({
1119
+ ...prev,
1120
+ [region]: {
1121
+ ...prev[region],
1122
+ [accountId]: body
1123
+ }
1124
+ }));
1125
+ return body;
1126
+ } catch {
1127
+ return;
866
1128
  }
867
1129
  };
868
1130
  /**
869
- * Claim today's check-in reward for the active tab's region.
1131
+ * Re-read EVERY row of one region, not just the one that was acted on.
1132
+ *
1133
+ * Trae spends the DEVICE's daily check-in, not the account's, so one claim
1134
+ * changes what every other row may do: they go from "check in" to "device
1135
+ * already used". Re-reading only the claimed account left its siblings
1136
+ * advertising a claim that could only come back 9095 — a row of live buttons
1137
+ * of which exactly one could work, which is the same false promise the 3.2.0
1138
+ * note refused bulk check-in for. Reading the siblings instead of guessing
1139
+ * them means the server's own `didCheckedIn` decides, so a sibling that is
1140
+ * still genuinely claimable (the device was not spent by this claim) keeps
1141
+ * its button.
1142
+ */
1143
+ const reloadAccountStatuses = async (region) => {
1144
+ if (status.status !== "signed-in") return;
1145
+ const ids = status.accounts.map((account) => account.id);
1146
+ await Promise.all(ids.map((id) => loadAccountStatus(id, region)));
1147
+ };
1148
+ /**
1149
+ * Claim today's check-in reward for ONE named account row (CN only).
870
1150
  *
871
1151
  * The Host route owns every guard that matters — POST only, loopback origin
872
1152
  * only, CN only, and a status read before the claim — because a browser-side
873
- * check is not a guard. The button's own `disabled` is therefore a courtesy
874
- * (it stops an obviously pointless click), not the protection.
1153
+ * check is not a guard. The row button's own `disabled` is therefore a
1154
+ * courtesy (it stops an obviously pointless click), not the protection. The
1155
+ * body carries the `accountId`, so the claim reaches THAT account's
1156
+ * credential without moving the whole binding.
875
1157
  *
876
1158
  * A refusal is not thrown away: the route answers `claimed: false` with the
877
- * upstream's business code and message, and the card shows it. Three answers
1159
+ * upstream's business code and message, and the row shows it. Three answers
878
1160
  * are distinguished rather than collapsed into "failed":
879
1161
  *
880
1162
  * - `alreadyCheckedIn` — this ACCOUNT was already paid today; show it as
@@ -885,32 +1167,88 @@ window.__ModuleLoader__.load({
885
1167
  * blame the plugin for an upstream rule.
886
1168
  * - anything else — a real refusal, reported verbatim.
887
1169
  */
888
- const claimCheckin = async () => {
889
- setClaiming(true);
890
- setClaimError(void 0);
891
- setDeviceSpentNotice(false);
1170
+ const claimFor = async (accountId) => {
1171
+ if (activeRegion !== "cn") return;
1172
+ setClaimingFor(accountId);
1173
+ setClaimErrorByRegion((prev) => {
1174
+ const next = { ...prev[activeRegion] };
1175
+ delete next[accountId];
1176
+ return {
1177
+ ...prev,
1178
+ [activeRegion]: next
1179
+ };
1180
+ });
1181
+ setDeviceSpentByRegion((prev) => {
1182
+ const next = { ...prev[activeRegion] };
1183
+ delete next[accountId];
1184
+ return {
1185
+ ...prev,
1186
+ [activeRegion]: next
1187
+ };
1188
+ });
892
1189
  try {
893
1190
  const response = await fetch(withTraeRegion(TRAE_CHECKIN_PATH, activeRegion), {
894
1191
  method: "POST",
895
- headers: { accept: "application/json" },
896
- credentials: "same-origin"
1192
+ headers: {
1193
+ accept: "application/json",
1194
+ "content-type": "application/json"
1195
+ },
1196
+ credentials: "same-origin",
1197
+ body: JSON.stringify({ accountId })
897
1198
  });
898
1199
  const body = await response.json().catch(() => void 0);
899
1200
  if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
900
1201
  if (body?.claimed === false && body.deviceCheckedIn === true) {
1202
+ setDeviceSpentByRegion((prev) => ({
1203
+ ...prev,
1204
+ [activeRegion]: {
1205
+ ...prev[activeRegion],
1206
+ [accountId]: true
1207
+ }
1208
+ }));
901
1209
  await refreshUsage(activeRegion);
902
- if (mounted.current) setDeviceSpentNotice(true);
1210
+ await reloadAccountStatuses(activeRegion);
903
1211
  return;
904
1212
  }
905
1213
  if (body?.claimed === false && body.alreadyCheckedIn !== true) throw new Error(body.message === void 0 || body.message === "" ? t("row.checkinRefused", { code: String(body.code ?? "") }) : `${t("row.checkinRefused", { code: String(body.code ?? "") })}: ${body.message}`);
906
1214
  await refreshUsage(activeRegion);
1215
+ await reloadAccountStatuses(activeRegion);
907
1216
  } catch (error) {
908
- if (mounted.current) setClaimError(error instanceof Error ? error.message : t("row.requestFailed"));
1217
+ if (mounted.current) {
1218
+ const message = error instanceof Error ? error.message : t("row.requestFailed");
1219
+ setClaimErrorByRegion((prev) => ({
1220
+ ...prev,
1221
+ [activeRegion]: {
1222
+ ...prev[activeRegion],
1223
+ [accountId]: message
1224
+ }
1225
+ }));
1226
+ }
909
1227
  } finally {
910
- if (mounted.current) setClaiming(false);
1228
+ if (mounted.current) setClaimingFor(void 0);
911
1229
  }
912
1230
  };
913
1231
  /**
1232
+ * Load every account row's per-account status when the card shows a
1233
+ * signed-in region. This is what makes the check-in buttons, balances, and
1234
+ * (ai) subscription rows appear. It is scoped to a stable `status.accounts`
1235
+ * identity so switching tabs or re-reading the document does not re-trigger a
1236
+ * burst of upstream calls for rows already loaded.
1237
+ */
1238
+ (0, react.useEffect)(() => {
1239
+ if (!open || !activeRegionOn || status.status !== "signed-in" || status.accounts.length === 0) return;
1240
+ status.accounts.map((account) => account.id).join("\0");
1241
+ const loadedIds = accountStatusByRegion[activeRegion];
1242
+ const todo = status.accounts.filter((account) => loadedIds[account.id] === void 0);
1243
+ if (todo.length === 0) return;
1244
+ for (const account of todo) loadAccountStatus(account.id, activeRegion);
1245
+ }, [
1246
+ open,
1247
+ activeRegionOn,
1248
+ status,
1249
+ accountStatusByRegion
1250
+ ]);
1251
+ /**
914
1252
  * Switch one region's provider off or on (issue #11). The write carries the
915
1253
  * region's whole slot through untouched — only `enabled` changes — so the
916
1254
  * user's directory, model picks, image opt-ins and budgets survive a
@@ -930,16 +1268,17 @@ window.__ModuleLoader__.load({
930
1268
  }
931
1269
  };
932
1270
  /**
933
- * One-click availability test for the ENABLED models of the active region.
1271
+ * One-click availability test for a list of models of the active region.
934
1272
  *
935
- * One model per request, deliberately: the host probe is sequential anyway
936
- * (Trae throttles bursts), and a per-model round trip lets each row show its
937
- * verdict as soon as it is known instead of the card sitting blank for a
938
- * minute. User-started, and it stops on the first authentication failure —
939
- * every later probe would only repeat that answer.
1273
+ * Shared by the「enabled models」and「all models」buttons: the only difference
1274
+ * between them is which ids are sent — the enabled rows or the whole visible
1275
+ * catalog. Each run goes one model per request, deliberately: the host probe
1276
+ * is sequential anyway (Trae throttles bursts), and a per-model round trip
1277
+ * lets each row show its verdict as soon as it is known instead of the card
1278
+ * sitting blank for a minute. User-started, and it stops on the first
1279
+ * authentication failure — every later probe would only repeat that answer.
940
1280
  */
941
- const testEnabledModels = async () => {
942
- const ids = [...activeEnabledIds];
1281
+ const testModels = async (ids) => {
943
1282
  setTestError(void 0);
944
1283
  setTestSummary(void 0);
945
1284
  if (ids.length === 0) {
@@ -1007,6 +1346,33 @@ window.__ModuleLoader__.load({
1007
1346
  if (mounted.current) setTesting(void 0);
1008
1347
  }
1009
1348
  };
1349
+ /** Test only the enabled models of the active region. */
1350
+ const testEnabledModels = async () => {
1351
+ const ids = [...activeEnabledIds];
1352
+ if (ids.length === 0) {
1353
+ setTestError(t("row.modelsTestEmpty"));
1354
+ setTestSummary(void 0);
1355
+ return;
1356
+ }
1357
+ testModels(ids);
1358
+ };
1359
+ /**
1360
+ * One-click availability test for EVERY model in the active region's catalog,
1361
+ * enabled or not — including the rows that are off and the rows the SOLO
1362
+ * channel does not serve. The host validates each id against the live catalog
1363
+ * and reports rows it does not know as skipped, so passing the whole visible
1364
+ * list cannot spend quota on arbitrary config names. Costs quota per model —
1365
+ * meaningfully more than the「enabled only」run, which the hint states.
1366
+ */
1367
+ const testAllModels = async () => {
1368
+ const ids = visibleModels.map((model) => model.id);
1369
+ if (ids.length === 0) {
1370
+ setTestError(t("row.modelsTestEmpty"));
1371
+ setTestSummary(void 0);
1372
+ return;
1373
+ }
1374
+ testModels(ids);
1375
+ };
1010
1376
  const refreshModels = async () => {
1011
1377
  setBusy(true);
1012
1378
  try {
@@ -1083,8 +1449,20 @@ window.__ModuleLoader__.load({
1083
1449
  * message — it never sends one by itself.
1084
1450
  */
1085
1451
  const rowImageIsAutomatic = (model) => model.multimodal === true;
1086
- /** The subscription row's verdict for the current account, when it has one. */
1087
- const payPlan = status.status === "signed-in" && status.payStatus !== void 0 ? payVerdict(status.payStatus) : void 0;
1452
+ /**
1453
+ * The international tab's plan is rendered by `rowValue`, in the account
1454
+ * table's 额度 column — one place, per account, beside the account it belongs
1455
+ * to.
1456
+ *
1457
+ * There used to be a SECOND surface for it: a standalone 「订阅状态」 box below
1458
+ * the table, reporting the bound account's plan on its own. It was added to
1459
+ * fix 「国际版没有那个积分框」 — an asymmetry against the CN tab's credit panel
1460
+ * — but the table's 额度 column had since absorbed exactly that figure, so the
1461
+ * box repeated the row directly above it. Two renderers of one verdict is the
1462
+ * drift risk `plan-text.ts` exists to prevent, and the box could only ever
1463
+ * restate what the marked 「当前使用」 row already said. It is gone; the row is
1464
+ * the single surface.
1465
+ */
1088
1466
  const activeContextBudgets = draft?.contextBudgets ?? savedContextBudgets;
1089
1467
  const dirty = draft !== void 0;
1090
1468
  /** Apply an edit to the ACTIVE tab's draft, seeding it from the current view. */
@@ -1167,6 +1545,85 @@ window.__ModuleLoader__.load({
1167
1545
  }
1168
1546
  };
1169
1547
  const title = t("row.title");
1548
+ /**
1549
+ * Row labels for the account table.
1550
+ *
1551
+ * The raw `edition` token ("solo") told the user nothing about WHICH product a
1552
+ * credential came from, which is exactly what made a leftover TRAE SOLO login
1553
+ * indistinguishable from the Trae CN one they thought they were bound to
1554
+ * (issue #25). Spell the product out, and keep the region as the second axis.
1555
+ */
1556
+ const editionLabel = (edition) => t(`row.edition${edition === "cn" ? "Cn" : edition === "sg" ? "Sg" : edition === "solo" ? "Solo" : "SoloSg"}`);
1557
+ const sourceLabel = (source) => source === "desktop" ? t("row.sourceDesktop") : source === "cli" ? t("row.sourceCli") : t("row.sourceDsh");
1558
+ /**
1559
+ * The trailing value on an account row's meta line: the account's general
1560
+ * balance (CN) or its subscription verdict (ai).
1561
+ *
1562
+ * A status that has not loaded — or whose read failed — renders NOTHING
1563
+ * rather than a dash. The row already names the account, its edition and its
1564
+ * source; a column of placeholder glyphs was pure noise, and it made every
1565
+ * row look like something was wrong. `undefined` means "no figure", never
1566
+ * "zero".
1567
+ */
1568
+ const rowValue = (state) => {
1569
+ if (activeRegion === "ai") {
1570
+ const payStatus = state?.payStatus;
1571
+ if (payStatus === void 0) return void 0;
1572
+ const verdict = planVerdict(payStatus);
1573
+ if (!verdict.paid) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1574
+ className: "dsm-trae-account-value",
1575
+ children: t("row.noPackage")
1576
+ });
1577
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1578
+ className: "dsm-trae-account-value dsm-trae-account-value-on",
1579
+ children: verdict.tier === void 0 ? t("row.subscribed") : t("row.planTier", { tier: verdict.tier })
1580
+ });
1581
+ }
1582
+ const figure = state?.credits?.generalAvailable;
1583
+ if (figure === void 0) return void 0;
1584
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1585
+ className: "dsm-trae-account-value",
1586
+ children: t("row.accountBalance", { credits: formatNumber(figure) })
1587
+ });
1588
+ };
1589
+ /**
1590
+ * The CN check-in control for one account row.
1591
+ *
1592
+ * The reward rides INSIDE the button ("签到 +150") instead of sitting beside
1593
+ * it as a second line, so the row says what the click pays without also
1594
+ * carrying a "150 credits per day" caption. Four states, none of which lies:
1595
+ *
1596
+ * - claimable — live, and names the payout.
1597
+ * - claiming — this row's own claim is in flight; only its button changes.
1598
+ * - claimed — `checkedIn`: this ACCOUNT was paid, so it reads as done.
1599
+ * - device spent — `didCheckedIn` (or a 9095 refusal from this row's click):
1600
+ * the DEVICE is spent but this account was never rewarded, so the button
1601
+ * says exactly that and stays disabled rather than reporting "done" for an
1602
+ * account that got nothing. The full rule lives in `title` — rendered as a
1603
+ * paragraph it dominated the row and drowned out the account itself.
1604
+ *
1605
+ * Until the row's status lands the button waits, disabled, so the row keeps
1606
+ * its shape instead of reflowing the moment the read returns.
1607
+ */
1608
+ const checkinControl = (account, state) => {
1609
+ const checkin = state?.checkin;
1610
+ const reward = checkin === void 0 ? void 0 : formatNumber(checkin.credits);
1611
+ const claimed = checkin?.checkedIn === true;
1612
+ const deviceSpent = deviceSpentByRegion[activeRegion][account.id] === true || checkin?.didCheckedIn === true;
1613
+ const spending = claimingFor === account.id;
1614
+ const usable = checkin !== void 0 && checkin.enabled;
1615
+ const title = reward === void 0 || !usable ? t("row.checkinDisabled") : deviceSpent && !claimed ? t("row.checkinDeviceSpent") : claimed ? t("row.checkinClaimed") : t("row.checkinReward", { reward });
1616
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1617
+ type: "button",
1618
+ className: "dsm-trae-account-checkin",
1619
+ disabled: !usable || claimed || deviceSpent || spending,
1620
+ title,
1621
+ onClick: () => {
1622
+ claimFor(account.id);
1623
+ },
1624
+ children: spending ? t("row.checkinClaiming") : claimed ? t("row.checkinClaimed") : deviceSpent ? t("row.checkinDeviceSpentShort") : !usable || reward === void 0 ? t("row.checkinClaim") : t("row.checkinClaimWith", { reward })
1625
+ });
1626
+ };
1170
1627
  const label = status.status === "signed-in" ? t("row.signedIn", { accountName: status.accountName }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
1171
1628
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1172
1629
  className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
@@ -1229,7 +1686,7 @@ window.__ModuleLoader__.load({
1229
1686
  style: dotStyle(regionStatus.status)
1230
1687
  }), item === "cn" ? t("row.tabCn") : t("row.tabAi")]
1231
1688
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1232
- className: "dsm-trae-tab-switch",
1689
+ className: "dsm-trae-switch dsm-trae-tab-switch",
1233
1690
  title: t("row.tabSwitchHint"),
1234
1691
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1235
1692
  type: "checkbox",
@@ -1258,86 +1715,174 @@ window.__ModuleLoader__.load({
1258
1715
  role: "status",
1259
1716
  children: t("row.tabOffNotice")
1260
1717
  }),
1261
- activeRegion !== "cn" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1262
- className: "dsm-trae-mainui-switch",
1263
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1264
- className: "dsm-trae-mainui-switch-label",
1265
- title: t("row.showPointsInMainUiHint"),
1718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1719
+ className: "dsm-trae-prefs",
1720
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1721
+ className: "dsm-trae-pref",
1722
+ title: t("row.followAppLoginHint"),
1266
1723
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1267
1724
  type: "checkbox",
1268
- checked: settingsValue?.showPointsInMainUi === true,
1269
- disabled: settingsScope?.getSnapshot().writable !== true || togglingCredits,
1725
+ checked: settingsEdits.followAppLogin,
1726
+ disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
1727
+ "aria-describedby": followHintId,
1270
1728
  onChange: (event) => {
1271
- toggleShowPointsInMainUi(event.currentTarget.checked);
1729
+ writePreference("followAppLogin", event.currentTarget.checked);
1272
1730
  }
1273
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.showPointsInMainUi") })]
1274
- })
1275
- }),
1276
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1277
- className: "dsm-trae-mainui-switch",
1278
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1279
- className: "dsm-trae-mainui-switch-label",
1280
- title: t("row.followAppLoginHint"),
1731
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1732
+ id: followHintId,
1733
+ children: [
1734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
1735
+ id: followLabelId,
1736
+ children: t("row.followAppLogin")
1737
+ }),
1738
+ " ",
1739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1740
+ className: "dsm-trae-pref-hint",
1741
+ children: t("row.followAppLoginHint")
1742
+ })
1743
+ ]
1744
+ })]
1745
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1746
+ className: "dsm-trae-pref",
1747
+ title: t(activeRegion === "cn" ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi"),
1281
1748
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1282
1749
  type: "checkbox",
1283
- checked: followAppLoginEnabled(settingsValue),
1284
- disabled: settingsScope?.getSnapshot().writable !== true,
1750
+ checked: settingsEdits.showPointsInMainUi,
1751
+ disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
1752
+ "aria-describedby": creditsHintId,
1285
1753
  onChange: (event) => {
1286
- toggleFollowAppLogin(event.currentTarget.checked);
1754
+ writePreference("showPointsInMainUi", event.currentTarget.checked);
1287
1755
  }
1288
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.followAppLogin") })]
1289
- })
1290
- }),
1291
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1292
- className: "dsm-trae-usage-account",
1293
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1294
- className: "dsm-trae-usage-account-copy",
1295
- role: "status",
1296
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1297
- className: "dsm-trae-usage-status",
1298
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1299
- "aria-hidden": "true",
1300
- className: "dsm-trae-usage-dot",
1301
- style: dotStyle(status.status)
1302
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1303
- }), status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1304
- className: "dsm-trae-usage-expiry",
1305
- children: t("row.tokenExpiry", { expiresAt: formatDateTime(status.tokenExpiresAtMs) })
1306
- }) : null]
1307
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1308
- type: "button",
1309
- className: "dsm-btn dsm-btn-outline",
1310
- disabled: busy,
1311
- onClick: () => {
1312
- rescanAccounts();
1313
- },
1314
- children: busy ? t("row.accountsScanning") : t("row.refreshTokens")
1756
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1757
+ id: creditsHintId,
1758
+ children: [
1759
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", {
1760
+ id: creditsLabelId,
1761
+ children: t("row.showPointsInMainUi")
1762
+ }),
1763
+ " ",
1764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1765
+ className: "dsm-trae-pref-hint",
1766
+ children: t(activeRegion === "cn" ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi")
1767
+ })
1768
+ ]
1769
+ })]
1315
1770
  })]
1316
1771
  }),
1317
- status.status !== "error" && status.accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1318
- className: "dsm-trae-account-picker",
1772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1773
+ className: "dsm-trae-accounts",
1319
1774
  "aria-label": t("row.accountsTitle"),
1320
1775
  children: [
1321
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1322
- className: "dsm-trae-usage-select-wrap",
1323
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1324
- className: "dsm-trae-usage-select",
1325
- value: status.status === "signed-in" ? status.accountId : "",
1326
- disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
1327
- onChange: (event) => {
1328
- switchAccount(event.currentTarget.value);
1776
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1777
+ className: "dsm-trae-accounts-head",
1778
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1779
+ className: "dsm-trae-accounts-title",
1780
+ children: t("row.accountsTitle")
1781
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1782
+ className: "dsm-trae-accounts-hint",
1783
+ children: t("row.accountsHint")
1784
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1785
+ type: "button",
1786
+ className: "dsm-btn dsm-btn-outline",
1787
+ disabled: busy,
1788
+ onClick: () => {
1789
+ rescanAccounts();
1329
1790
  },
1330
- children: status.accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1331
- value: account.id,
1791
+ children: busy ? t("row.accountsScanning") : t("row.refreshTokens")
1792
+ })]
1793
+ }),
1794
+ status.status !== "signed-in" || status.accounts.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1795
+ className: "dsm-trae-usage-account",
1796
+ role: "status",
1797
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1798
+ className: "dsm-trae-usage-account-copy",
1799
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1800
+ className: "dsm-trae-usage-status",
1801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1802
+ "aria-hidden": "true",
1803
+ className: "dsm-trae-usage-dot",
1804
+ style: dotStyle(status.status)
1805
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1806
+ }), status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1807
+ className: "dsm-trae-usage-expiry",
1808
+ children: t("row.tokenExpiry", { expiresAt: formatDateTime(status.tokenExpiresAtMs) })
1809
+ }) : null]
1810
+ })
1811
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1812
+ className: `dsm-trae-account-table${activeRegion === "cn" ? " dsm-trae-account-table-checkin" : ""}`,
1813
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1814
+ className: "dsm-trae-account-row dsm-trae-account-row-head",
1815
+ role: "row",
1816
+ children: [
1817
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnAccount") }),
1818
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnValue") }),
1819
+ activeRegion === "cn" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnCheckin") }) : null,
1820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})
1821
+ ]
1822
+ }), status.accounts.map((account) => {
1823
+ const current = status.status === "signed-in" && account.id === status.accountId;
1824
+ const state = accountStatusByRegion[activeRegion][account.id];
1825
+ const error = claimErrorByRegion[activeRegion][account.id];
1826
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1827
+ className: `dsm-trae-account-row${current ? " dsm-trae-account-row-current" : ""}`,
1828
+ role: "row",
1332
1829
  children: [
1333
- account.accountName,
1334
- " · ",
1335
- account.region === "ai" ? t("row.regionAi") : t("row.regionCn"),
1336
- " · ",
1337
- t(`row.edition${account.edition === "cn" ? "Cn" : account.edition === "sg" ? "Sg" : account.edition === "solo" ? "Solo" : "SoloSg"}`)
1830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1831
+ className: "dsm-trae-account-who",
1832
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1833
+ "aria-hidden": "true",
1834
+ className: "dsm-trae-usage-dot",
1835
+ style: dotStyle(account.selected ? "signed-in" : "signed-out")
1836
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1837
+ className: "dsm-trae-account-name",
1838
+ children: [
1839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: account.accountName }),
1840
+ current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1841
+ className: "dsm-trae-account-badge",
1842
+ children: t("row.accountCurrentBadge")
1843
+ }) : null,
1844
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1845
+ className: "dsm-trae-account-meta",
1846
+ children: [
1847
+ editionLabel(account.edition),
1848
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1849
+ "aria-hidden": "true",
1850
+ children: "·"
1851
+ }),
1852
+ sourceLabel(account.source)
1853
+ ]
1854
+ }),
1855
+ error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1856
+ className: "dsm-trae-account-note",
1857
+ role: "alert",
1858
+ children: t("row.checkinError", { message: error })
1859
+ })
1860
+ ]
1861
+ })]
1862
+ }),
1863
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1864
+ className: "dsm-trae-account-worth",
1865
+ children: rowValue(state)
1866
+ }),
1867
+ activeRegion === "cn" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1868
+ className: "dsm-trae-account-checkin-cell",
1869
+ children: checkinControl(account, state)
1870
+ }) : null,
1871
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1872
+ className: "dsm-trae-account-actions",
1873
+ children: current ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1874
+ type: "button",
1875
+ className: "dsm-btn dsm-btn-outline dsm-trae-account-switch",
1876
+ disabled: switchingAccount || settingsScope?.getSnapshot()?.writable !== true,
1877
+ onClick: () => {
1878
+ switchAccount(account.id);
1879
+ },
1880
+ children: switchingAccount ? t("row.accountSwitching") : t("row.accountSwitch")
1881
+ })
1882
+ })
1338
1883
  ]
1339
- }, account.id))
1340
- })
1884
+ }, account.id);
1885
+ })]
1341
1886
  }),
1342
1887
  status.status === "signed-in" && status.alternatives !== void 0 && status.alternatives.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1343
1888
  className: "dsm-trae-usage-alternative",
@@ -1365,7 +1910,10 @@ window.__ModuleLoader__.load({
1365
1910
  role: "status",
1366
1911
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1367
1912
  className: "dsm-trae-usage-alternative-text",
1368
- children: t("row.appLoginMismatch", { account: status.appLogin.accountName })
1913
+ children: t("row.appLoginMismatch", {
1914
+ account: status.appLogin.accountName,
1915
+ bound: status.accountName
1916
+ })
1369
1917
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1370
1918
  type: "button",
1371
1919
  className: "dsm-trae-usage-switch-button",
@@ -1373,7 +1921,7 @@ window.__ModuleLoader__.load({
1373
1921
  onClick: () => {
1374
1922
  followAppLogin();
1375
1923
  },
1376
- children: t("row.appLoginFollow")
1924
+ children: t("row.appLoginFollow", { account: status.appLogin.accountName })
1377
1925
  })]
1378
1926
  }) : null,
1379
1927
  rescanNote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1382,132 +1930,8 @@ window.__ModuleLoader__.load({
1382
1930
  children: rescanNote
1383
1931
  })
1384
1932
  ]
1385
- }) : null,
1933
+ }),
1386
1934
  status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1387
- status.payStatus === void 0 || payPlan === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1388
- className: "dsm-trae-usage-list",
1389
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1390
- className: "dsm-trae-usage-stats dsm-trae-usage-stats-two",
1391
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1392
- className: "dsm-trae-usage-stat dsm-trae-usage-stat-general",
1393
- children: [
1394
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1395
- className: "dsm-trae-usage-stat-head",
1396
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1397
- className: "dsm-trae-usage-stat-label",
1398
- children: t("row.subscriptionLabel")
1399
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1400
- className: `dsm-trae-usage-stat-badge ${payPlan.paid ? "dsm-trae-usage-stat-badge-on" : "dsm-trae-usage-stat-badge-off"}`,
1401
- children: t(payPlan.label)
1402
- })]
1403
- }),
1404
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1405
- className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-general",
1406
- children: status.payStatus.inTrial && status.payStatus.trialEndTimeMs > 0 ? t("row.trialUntil", { date: formatDateTime(status.payStatus.trialEndTimeMs) }) : [t(payPlan.label), payPlan.tier].filter(Boolean).join(" · ")
1407
- }),
1408
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1409
- className: "dsm-trae-usage-stat-hint",
1410
- children: status.payStatus.enableSoloLite || status.payStatus.enableSoloCoder || status.payStatus.enableSoloBuilder || status.payStatus.enableSoloWeb ? [
1411
- status.payStatus.enableSoloLite ? "SOLO Lite" : "",
1412
- status.payStatus.enableSoloCoder ? "SOLO Coder" : "",
1413
- status.payStatus.enableSoloBuilder ? "SOLO Builder" : "",
1414
- status.payStatus.enableSoloWeb ? "SOLO Web" : ""
1415
- ].filter(Boolean).join(" · ") : status.payStatus.isDollarUsageBilling ? "dollar usage billing" : ""
1416
- })
1417
- ]
1418
- })
1419
- })
1420
- }),
1421
- status.payStatusError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1422
- className: "dsm-trae-usage-error",
1423
- children: t("row.payStatusError", { message: status.payStatusError })
1424
- }),
1425
- status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1426
- className: "dsm-trae-usage-list",
1427
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1428
- className: "dsm-trae-usage-stats dsm-trae-usage-stats-two",
1429
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1430
- className: "dsm-trae-usage-stat dsm-trae-usage-stat-work",
1431
- children: [
1432
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1433
- className: "dsm-trae-usage-stat-head",
1434
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1435
- className: "dsm-trae-usage-stat-label",
1436
- children: t("row.workCreditsLabel")
1437
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1438
- className: "dsm-trae-usage-stat-badge dsm-trae-usage-stat-badge-off",
1439
- children: t("row.workNotUsable")
1440
- })]
1441
- }),
1442
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1443
- className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-work",
1444
- children: formatNumber(status.credits.workAvailable)
1445
- }),
1446
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1447
- className: "dsm-trae-usage-stat-hint",
1448
- children: t("row.workUsableHint")
1449
- })
1450
- ]
1451
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1452
- className: "dsm-trae-usage-stat dsm-trae-usage-stat-general",
1453
- children: [
1454
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1455
- className: "dsm-trae-usage-stat-head",
1456
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1457
- className: "dsm-trae-usage-stat-label",
1458
- children: t("row.generalCreditsLabel")
1459
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1460
- className: "dsm-trae-usage-stat-badge dsm-trae-usage-stat-badge-on",
1461
- children: t("row.generalUsable")
1462
- })]
1463
- }),
1464
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1465
- className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-general",
1466
- children: formatNumber(status.credits.generalAvailable)
1467
- }),
1468
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1469
- className: "dsm-trae-usage-stat-hint",
1470
- children: t("row.generalUsableHint")
1471
- })
1472
- ]
1473
- })]
1474
- })
1475
- }),
1476
- status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1477
- className: "dsm-trae-usage-error",
1478
- children: t("row.creditsError", { message: status.creditsError })
1479
- }),
1480
- status.checkin === void 0 ? null : (() => {
1481
- const accountClaimed = status.checkin.checkedIn;
1482
- const deviceSpent = !accountClaimed && (status.checkin.didCheckedIn || deviceSpentNotice);
1483
- const reward = formatNumber(status.checkin.credits);
1484
- const bonus = status.checkin.extraCredits === void 0 ? "" : ` + ${formatNumber(status.checkin.extraCredits)}`;
1485
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1486
- className: "dsm-trae-checkin",
1487
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1488
- className: "dsm-trae-checkin-copy",
1489
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1490
- className: "dsm-trae-checkin-label",
1491
- children: t("row.checkinLabel")
1492
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1493
- className: "dsm-trae-checkin-hint",
1494
- children: [t("row.checkinReward", { reward: `${reward}${bonus}` }), accountClaimed ? ` · ${t("row.checkinDoneHint")}` : ""]
1495
- })]
1496
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1497
- type: "button",
1498
- className: "dsm-btn dsm-btn-primary dsm-trae-checkin-button",
1499
- disabled: !status.checkin.enabled || accountClaimed || deviceSpent || claiming,
1500
- onClick: () => {
1501
- claimCheckin();
1502
- },
1503
- children: claiming ? t("row.checkinClaiming") : accountClaimed ? t("row.checkinClaimed") : t("row.checkinClaim")
1504
- })]
1505
- }), deviceSpent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1506
- className: "dsm-trae-checkin-note",
1507
- role: "status",
1508
- children: t("row.checkinDeviceSpent")
1509
- }) : null] });
1510
- })(),
1511
1935
  status.checkin !== void 0 && !status.checkin.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1512
1936
  className: "dsm-trae-usage-error",
1513
1937
  children: t("row.checkinDisabled")
@@ -1516,11 +1940,6 @@ window.__ModuleLoader__.load({
1516
1940
  className: "dsm-trae-usage-error",
1517
1941
  children: t("row.checkinError", { message: status.checkinError })
1518
1942
  }),
1519
- claimError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1520
- className: "dsm-trae-usage-error",
1521
- role: "alert",
1522
- children: t("row.checkinError", { message: claimError })
1523
- }),
1524
1943
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1525
1944
  className: "dsm-trae-models",
1526
1945
  "aria-label": t("row.modelsTitle"),
@@ -1535,27 +1954,43 @@ window.__ModuleLoader__.load({
1535
1954
  children: t("row.modelsSummary", { count: activeEnabledIds.size })
1536
1955
  })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1537
1956
  className: "dsm-trae-models-actions",
1538
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1539
- type: "button",
1540
- className: "dsm-btn dsm-btn-outline",
1541
- disabled: busy,
1542
- onClick: () => {
1543
- refreshModels();
1544
- },
1545
- children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1546
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1547
- type: "button",
1548
- className: "dsm-btn dsm-btn-outline",
1549
- disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
1550
- title: t("row.modelsTestHint"),
1551
- onClick: () => {
1552
- testEnabledModels();
1553
- },
1554
- children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
1555
- done: testing.done,
1556
- total: testing.total
1957
+ children: [
1958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1959
+ type: "button",
1960
+ className: "dsm-btn dsm-btn-outline",
1961
+ disabled: busy,
1962
+ onClick: () => {
1963
+ refreshModels();
1964
+ },
1965
+ children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
1966
+ }),
1967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1968
+ type: "button",
1969
+ className: "dsm-btn dsm-btn-outline",
1970
+ disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
1971
+ title: t("row.modelsTestHint"),
1972
+ onClick: () => {
1973
+ testEnabledModels();
1974
+ },
1975
+ children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
1976
+ done: testing.done,
1977
+ total: testing.total
1978
+ })
1979
+ }),
1980
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1981
+ type: "button",
1982
+ className: "dsm-btn dsm-btn-outline",
1983
+ disabled: busy || testing !== void 0 || visibleModels.length === 0,
1984
+ title: t("row.modelsTestAllHint"),
1985
+ onClick: () => {
1986
+ testAllModels();
1987
+ },
1988
+ children: testing === void 0 ? t("row.modelsTestAll") : t("row.modelsTesting", {
1989
+ done: testing.done,
1990
+ total: testing.total
1991
+ })
1557
1992
  })
1558
- })]
1993
+ ]
1559
1994
  })]
1560
1995
  }),
1561
1996
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1897,20 +2332,30 @@ window.__ModuleLoader__.load({
1897
2332
  */
1898
2333
  const COMPOSER_POINTS_REFRESH_INTERVAL_MS = 3e5;
1899
2334
  /**
1900
- * Provider routes whose credit balance this readout can show, mapped to the
1901
- * region to read it from.
2335
+ * Provider routes this readout serves, mapped to the region to read from.
2336
+ *
2337
+ * BOTH regions, but they do not show the same KIND of number, and that is the
2338
+ * data's doing rather than a preference:
1902
2339
  *
1903
- * CN ONLY, and the restriction is the data's, not a preference: the Host builds
1904
- * the international region's usage document from `payStatus` and never sets
1905
- * `credits` on it (`webStatusFor` returns early for `region === 'ai'`), because
1906
- * that side is subscription-based and has no comparable balance. Mapping
1907
- * `trae-global` here would render the hardcoded "Trae CN" label next to a dash
1908
- * — a number-slot with no number — while an international model is selected.
2340
+ * - `cn` is credit-based, so the readout shows the account's general balance.
2341
+ * - `ai` is subscription-based. The Host builds its usage document from
2342
+ * `payStatus` and never sets `credits` on it (`webStatusFor` returns early
2343
+ * for `region === 'ai'`), so there is no balance to show. What it does carry
2344
+ * is the PLAN, so the readout names the plan (`Pro` / `Free`) instead.
1909
2345
  *
1910
- * Mirrors the CN half of `TRAE_PROVIDERS` on the host side; duplicated here
1911
- * because the client bundle must not import the host entry.
2346
+ * The international route was previously omitted entirely, which meant the
2347
+ * readout vanished while an international model was selected — the asymmetry a
2348
+ * user reported. Omitting it was the safer half of a real problem (a plan-shaped
2349
+ * document templated into a number slot renders a dash), but the fix is to
2350
+ * render what that side actually has, not to render nothing.
2351
+ *
2352
+ * Mirrors `TRAE_PROVIDERS` on the host side; duplicated here because the client
2353
+ * bundle must not import the host entry.
1912
2354
  */
1913
- const TRAE_COMPOSER_PROVIDERS = { "trae": "cn" };
2355
+ const TRAE_COMPOSER_PROVIDERS = {
2356
+ "trae": "cn",
2357
+ "trae-global": "ai"
2358
+ };
1914
2359
  /** The general credit balance out of one usage document, or undefined. */
1915
2360
  function generalCreditsOf(usage) {
1916
2361
  if (usage.status !== "signed-in") return void 0;
@@ -1933,6 +2378,16 @@ window.__ModuleLoader__.load({
1933
2378
  const activeRegion = region ?? owned;
1934
2379
  const [usage, setUsage] = (0, react.useState)(void 0);
1935
2380
  const [accountCredits, setAccountCredits] = (0, react.useState)(void 0);
2381
+ /**
2382
+ * Per-account PLANS, international region only.
2383
+ *
2384
+ * The CN panel reads the credit route; that route is CN-shaped (it calls the
2385
+ * credit snapshot, which the ai document does not carry), so the international
2386
+ * panel reads the per-account STATUS route instead — the same one the card's
2387
+ * roster uses, which answers `payStatus` for ai. Reads only while the panel is
2388
+ * open, for the same reason the credit route does.
2389
+ */
2390
+ const [accountPlans, setAccountPlans] = (0, react.useState)(void 0);
1936
2391
  const [busy, setBusy] = (0, react.useState)(false);
1937
2392
  const [switchingId, setSwitchingId] = (0, react.useState)(void 0);
1938
2393
  const [failed, setFailed] = (0, react.useState)(false);
@@ -2018,17 +2473,51 @@ window.__ModuleLoader__.load({
2018
2473
  controller.abort();
2019
2474
  };
2020
2475
  }, [owned, fetchUsage]);
2476
+ /**
2477
+ * Every account's PLAN, one status read per account — ai only.
2478
+ *
2479
+ * Costs one upstream call per account, paid only while the panel is open (the
2480
+ * same rule the credit route follows). A failed read leaves that account's plan
2481
+ * undefined, which the row renders as a dash: the row still switches accounts,
2482
+ * which is the panel's primary job.
2483
+ */
2484
+ const fetchAccountPlans = (0, react.useCallback)(async (signal) => {
2485
+ if (activeRegion !== "ai") return;
2486
+ const roster = usage?.status === "signed-in" ? usage.accounts.filter((account) => account.region === "ai") : [];
2487
+ if (roster.length === 0) {
2488
+ setAccountPlans({});
2489
+ return;
2490
+ }
2491
+ const entries = await Promise.all(roster.map(async (account) => {
2492
+ try {
2493
+ const response = await fetch(`${withTraeRegion(TRAE_ACCOUNT_STATUS_PATH, "ai")}&accountId=${encodeURIComponent(account.id)}`, {
2494
+ headers: { accept: "application/json" },
2495
+ credentials: "same-origin",
2496
+ ...signal === void 0 ? {} : { signal }
2497
+ });
2498
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
2499
+ const state = await response.json();
2500
+ return [account.id, state.payStatus];
2501
+ } catch {
2502
+ return [account.id, void 0];
2503
+ }
2504
+ }));
2505
+ if (!mounted.current || signal?.aborted === true) return;
2506
+ setAccountPlans(Object.fromEntries(entries));
2507
+ }, [activeRegion, usage]);
2021
2508
  (0, react.useEffect)(() => {
2022
2509
  if (!open || activeRegion === void 0) return void 0;
2023
2510
  const controller = new AbortController();
2024
- fetchAccountCredits(controller.signal);
2511
+ if (activeRegion === "ai") fetchAccountPlans(controller.signal);
2512
+ else fetchAccountCredits(controller.signal);
2025
2513
  return () => {
2026
2514
  controller.abort();
2027
2515
  };
2028
2516
  }, [
2029
2517
  open,
2030
2518
  activeRegion,
2031
- fetchAccountCredits
2519
+ fetchAccountCredits,
2520
+ fetchAccountPlans
2032
2521
  ]);
2033
2522
  (0, react.useEffect)(() => {
2034
2523
  if (!open) return void 0;
@@ -2065,7 +2554,8 @@ window.__ModuleLoader__.load({
2065
2554
  const readBack = configuredAccountsOf(settingsScope.getSnapshot().value);
2066
2555
  if (accepted === false || readBack[activeRegion] !== accountId) throw new Error("the Host did not persist the switch");
2067
2556
  await fetchUsage();
2068
- await fetchAccountCredits();
2557
+ if (activeRegion === "ai") await fetchAccountPlans();
2558
+ else await fetchAccountCredits();
2069
2559
  } catch {
2070
2560
  if (mounted.current) setFailed(true);
2071
2561
  } finally {
@@ -2074,10 +2564,21 @@ window.__ModuleLoader__.load({
2074
2564
  };
2075
2565
  if (owned === void 0) return null;
2076
2566
  const signedIn = usage?.status === "signed-in" ? usage : void 0;
2077
- const general = signedIn === void 0 ? void 0 : generalCreditsOf(signedIn);
2078
2567
  const signedOut = usage !== void 0 && usage.status === "signed-out";
2079
- const valueText = general === void 0 ? "—" : formatCredits(general);
2080
- const label = t("composer.points");
2568
+ const general = signedIn === void 0 ? void 0 : generalCreditsOf(signedIn);
2569
+ /**
2570
+ * What the readout shows after the separator.
2571
+ *
2572
+ * Region-dependent because the two sides answer different questions: CN has a
2573
+ * spendable balance, ai has a plan. Reading the credit field on an ai document
2574
+ * would always be `undefined` and print the dash this change exists to remove.
2575
+ */
2576
+ const valueText = activeRegion === "ai" ? signedIn?.payStatus === void 0 ? "—" : planText(signedIn.payStatus, t) : general === void 0 ? "—" : formatCredits(general);
2577
+ /**
2578
+ * The label before the separator. Per REGION, not one hardcoded 'Trae CN':
2579
+ * the international readout sat next to the wrong product name otherwise.
2580
+ */
2581
+ const label = activeRegion === "ai" ? t("composer.pointsGlobal") : t("composer.points");
2081
2582
  /**
2082
2583
  * The table's rows: every account in this region, with its general balance.
2083
2584
  *
@@ -2087,7 +2588,11 @@ window.__ModuleLoader__.load({
2087
2588
  * than a spinner the user has to wait on.
2088
2589
  */
2089
2590
  const selectedId = accountCredits?.find((account) => account.selected)?.id ?? (signedIn === void 0 ? void 0 : signedIn.accountId);
2090
- const rows = (accountCredits ?? (signedIn === void 0 ? [] : signedIn.accounts.filter((account) => account.region === activeRegion).map((account) => ({
2591
+ const rows = (activeRegion === "ai" ? signedIn === void 0 ? [] : signedIn.accounts.filter((account) => account.region === "ai").map((account) => ({
2592
+ id: account.id,
2593
+ accountName: account.accountName,
2594
+ selected: account.selected
2595
+ })) : accountCredits ?? (signedIn === void 0 ? [] : signedIn.accounts.filter((account) => account.region === activeRegion).map((account) => ({
2091
2596
  id: account.id,
2092
2597
  accountName: account.accountName,
2093
2598
  selected: account.selected
@@ -2124,13 +2629,13 @@ window.__ModuleLoader__.load({
2124
2629
  top: 0
2125
2630
  },
2126
2631
  role: "dialog",
2127
- "aria-label": t("composer.panelTitle"),
2632
+ "aria-label": activeRegion === "ai" ? t("composer.panelTitleGlobal") : t("composer.panelTitle"),
2128
2633
  children: [
2129
2634
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2130
2635
  className: "dsm-trae-composer-panel-head",
2131
2636
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2132
2637
  className: "dsm-trae-composer-panel-title",
2133
- children: t("composer.panelTitle")
2638
+ children: activeRegion === "ai" ? t("composer.panelTitleGlobal") : t("composer.panelTitle")
2134
2639
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2135
2640
  type: "button",
2136
2641
  className: "dsm-trae-composer-panel-refresh",
@@ -2155,7 +2660,7 @@ window.__ModuleLoader__.load({
2155
2660
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
2156
2661
  scope: "col",
2157
2662
  className: "dsm-trae-composer-panel-num",
2158
- children: t("composer.generalCredits")
2663
+ children: activeRegion === "ai" ? t("composer.plan") : t("composer.generalCredits")
2159
2664
  })] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row) => {
2160
2665
  const current = row.id === selectedId;
2161
2666
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
@@ -2177,7 +2682,10 @@ window.__ModuleLoader__.load({
2177
2682
  })
2178
2683
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2179
2684
  className: "dsm-trae-composer-panel-num",
2180
- children: row.generalAvailable === void 0 ? "—" : formatCredits(row.generalAvailable)
2685
+ children: activeRegion === "ai" ? (() => {
2686
+ const payStatus = row.id === selectedId && signedIn?.payStatus !== void 0 ? signedIn.payStatus : accountPlans?.[row.id];
2687
+ return payStatus === void 0 ? "—" : planText(payStatus, t);
2688
+ })() : row.generalAvailable === void 0 ? "—" : formatCredits(row.generalAvailable)
2181
2689
  })]
2182
2690
  }, row.id);
2183
2691
  }) })]
@@ -2276,10 +2784,10 @@ window.__ModuleLoader__.load({
2276
2784
  "row.collapse": "Collapse",
2277
2785
  "row.tabCn": "Domestic",
2278
2786
  "row.tabAi": "Global",
2279
- "row.tabHint": "Each tab is a separate provider (trae / trae-global) with its own account, usage, and models. Both sides are live at once: different sessions can pick from either side, and changes on one tab never touch the other.",
2280
- "row.tabSwitchHint": "Uncheck to switch this provider off. Its models disappear from the DSH model picker; its account, usage, and model settings are kept and return when you check it again.",
2787
+ "row.tabHint": "Two independent providers, each with its own account, usage, and models. Both are live at once and never affect each other.",
2788
+ "row.tabSwitchHint": "Turns this provider off: its models leave the DSH picker. Account and settings are kept.",
2281
2789
  "row.tabSwitchAria": "Offer the {region} provider to DSH",
2282
- "row.tabOffNotice": "This provider is switched off: none of its models are offered to DSH. Your account, usage, and model settings are kept — check the box above the tab to bring it back.",
2790
+ "row.tabOffNotice": "This provider is off: its models are not offered to DSH. Account and settings are kept.",
2283
2791
  "row.settingsWriteFailed": "The setting was not saved: {message}",
2284
2792
  "row.signedOut": "Not signed in",
2285
2793
  "row.signedOutHint": "Sign in once in the Trae desktop app; this plugin follows that sign-in automatically.",
@@ -2304,52 +2812,68 @@ window.__ModuleLoader__.load({
2304
2812
  "row.creditsConsumedLabel": "Consumed",
2305
2813
  "row.creditsTotalLabel": "Total",
2306
2814
  "row.creditsError": "Usage unavailable: {message}",
2307
- "row.checkinLabel": "Daily check-in",
2308
2815
  "row.checkinReward": "{reward} credits per day",
2309
- "row.checkinClaim": "Claim today",
2816
+ "row.checkinClaim": "Check in",
2817
+ "row.checkinClaimWith": "Check in +{reward}",
2310
2818
  "row.checkinClaiming": "Claiming…",
2311
- "row.checkinClaimed": "Claimed today",
2312
- "row.checkinDoneHint": "come back tomorrow",
2819
+ "row.checkinClaimed": "Claimed",
2313
2820
  "row.checkinRefused": "Trae refused the check-in (code {code})",
2314
2821
  "row.checkinError": "Check-in failed: {message}",
2315
2822
  "row.checkinDeviceSpent": "This machine already used today's check-in — Trae allows one per device per day, so this account cannot claim here today. Claim it on another device or from the account that checked in, or come back tomorrow.",
2316
2823
  "row.checkinDisabled": "This account has no check-in activity available.",
2317
- "row.regionCn": "CN",
2318
- "row.regionAi": "International",
2824
+ "row.checkinDeviceSpentShort": "Device used",
2319
2825
  "row.subscriptionLabel": "Subscription",
2320
2826
  "row.subscribed": "Active package",
2321
2827
  "row.noPackage": "No active package",
2828
+ "row.planTier": "{tier} plan",
2322
2829
  "row.trialUntil": "Trial until {date}",
2323
2830
  "row.payStatusError": "Subscription status unavailable: {message}",
2831
+ "row.payStatusUnknown": "Plan not loaded yet",
2324
2832
  "row.refresh": "Refresh",
2325
2833
  "row.refreshTokens": "Refresh accounts & tokens",
2326
2834
  "row.refreshCredits": "Refresh usage",
2327
2835
  "row.refreshing": "Refreshing…",
2328
2836
  "row.accountsTitle": "Trae account",
2329
- "row.accountsHint": "Choose from locally detected sign-ins. Tokens are never shown or saved in DSH settings.",
2837
+ "row.accountsHint": "Sign-ins detected on this machine. Tokens are never shown or saved.",
2330
2838
  "row.accountsRescan": "Detect accounts again",
2331
2839
  "row.accountsScanning": "Detecting…",
2840
+ "row.accountsNone": "This region reports no usable account.",
2841
+ "row.accountCurrentBadge": "in use",
2842
+ "row.accountSwitch": "Switch",
2843
+ "row.accountSwitching": "Switching…",
2844
+ "row.accountColumnAccount": "Account",
2845
+ "row.accountColumnValue": "Balance",
2846
+ "row.accountColumnCheckin": "Check-in",
2847
+ "row.accountBalance": "{credits} credits",
2848
+ "row.sourceDsh": "plugin copy",
2332
2849
  "row.modelsTitle": "Models",
2333
2850
  "row.modelsSummary": "{count} enabled",
2334
2851
  "row.modelsRefresh": "Refresh from Trae",
2335
2852
  "row.modelsRefreshing": "Refreshing models…",
2336
2853
  "row.modelsTestEnabled": "Test enabled models",
2854
+ "row.modelsTestAll": "Test all models",
2337
2855
  "row.modelsTesting": "Testing {done}/{total}…",
2338
2856
  "row.modelsTestHint": "Sends one minimal message per enabled model to see which ones actually answer. Costs a small amount of quota.",
2857
+ "row.modelsTestAllHint": "The「test all」button probes every model in the catalog, enabled or not, so it spends noticeably more quota than the enabled-only run.",
2339
2858
  "row.showPointsInMainUi": "Show credits in the composer row",
2340
- "row.showPointsInMainUiHint": "Show a clickable Trae CN · <balance> readout in the composer tool row, only while the selected model belongs to this plugin. Click it for the account and balance panel.",
2859
+ "row.showPointsInMainUiHint": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae CN · <balance> on the CN side, Trae Global · <plan> on the international one.",
2860
+ "row.showPointsInMainUiHintAi": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae Global · <plan>.",
2341
2861
  "composer.switchTo": "Switch to {account}",
2342
2862
  "composer.noAccounts": "No account in this region",
2343
2863
  "composer.panelTitle": "Trae credits",
2864
+ "composer.panelTitleGlobal": "Trae Global plan",
2344
2865
  "composer.account": "Account",
2345
2866
  "composer.generalCredits": "General credits",
2867
+ "composer.plan": "Plan",
2346
2868
  "composer.lastRefresh": "Last refreshed",
2347
2869
  "composer.signedOut": "Not signed in",
2348
2870
  "composer.points": "Trae CN",
2871
+ "composer.pointsGlobal": "Trae Global",
2349
2872
  "composer.refresh": "Refresh credits",
2350
2873
  "composer.refreshing": "Refreshing credits…",
2351
2874
  "composer.refreshFailed": "Could not refresh credits",
2352
2875
  "row.accountsRefreshed": "Accounts refreshed: {count} found{selected}",
2876
+ "row.accountsSaved": "bound to",
2353
2877
  "row.accountsKept": "still bound to",
2354
2878
  "row.accountsRefreshedNone": "Accounts refreshed: no usable Trae account found",
2355
2879
  "row.accountsRescanFailed": "Could not refresh accounts: {message}",
@@ -2360,10 +2884,11 @@ window.__ModuleLoader__.load({
2360
2884
  "row.creditsExhausted": "The bound account ({account}) has run out of credit",
2361
2885
  "row.creditsAlternative": "Another account on this machine still has credit: {accounts}",
2362
2886
  "row.creditsAlternativeSwitch": "Switch to it",
2363
- "row.appLoginMismatch": "Trae is signed in to {account}, but this plugin is still bound to the previous account",
2364
- "row.appLoginFollow": "Switch to it",
2887
+ "row.appLoginMismatch": "Trae is signed in to {account}, but this plugin is bound to {bound}.",
2888
+ "row.appLoginFollow": "Use {account}",
2365
2889
  "row.followAppLogin": "Follow the account Trae is signed in to",
2366
- "row.followAppLoginHint": "When you switch accounts in the Trae desktop app, rebind to the new sign-in automatically instead of staying on the previous one. Off by default: an existing binding is kept until you tick this.",
2890
+ "row.followAppLoginHint": "Rebind automatically when you switch accounts in the Trae desktop app. Off by default, so the current binding is kept.",
2891
+ "row.settingsDirty": "Unsaved changes",
2367
2892
  "row.modelsTestEmpty": "Enable at least one model first.",
2368
2893
  "row.modelsTestError": "Test failed: {message}",
2369
2894
  "row.modelsTestDone": "{available} of {total} answered.",
@@ -2396,10 +2921,10 @@ window.__ModuleLoader__.load({
2396
2921
  "row.collapse": "收起",
2397
2922
  "row.tabCn": "国内版",
2398
2923
  "row.tabAi": "国际版",
2399
- "row.tabHint": "每个 tab 是一个独立供应商(trae / trae-global),各有自己的账号、用量与模型。两边同时生效:不同会话可各选一边,一侧的改动不影响另一侧。",
2400
- "row.tabSwitchHint": "取消勾选即关闭该供应商:它的模型不再出现在 DSH 模型选择器里;账号、用量与模型设置都会保留,重新勾选即恢复。",
2924
+ "row.tabHint": "国内版与国际版是两个独立供应商,各有账号、用量与模型;两边同时生效,互不影响。",
2925
+ "row.tabSwitchHint": "关闭该供应商:其模型不再提供给 DSH;账号与设置保留,可随时恢复。",
2401
2926
  "row.tabSwitchAria": "是否向 DSH 提供{region}供应商",
2402
- "row.tabOffNotice": "该供应商已关闭:它的模型不会提供给 DSH。账号、用量与模型设置均已保留——勾选上方复选框即可恢复。",
2927
+ "row.tabOffNotice": "该供应商已关闭,模型不会提供给 DSH。账号与设置均已保留。",
2403
2928
  "row.settingsWriteFailed": "设置未能保存:{message}",
2404
2929
  "row.signedOut": "未登录",
2405
2930
  "row.signedOutHint": "在 Trae 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
@@ -2424,52 +2949,68 @@ window.__ModuleLoader__.load({
2424
2949
  "row.creditsConsumedLabel": "消耗",
2425
2950
  "row.creditsTotalLabel": "合计",
2426
2951
  "row.creditsError": "用量查询失败:{message}",
2427
- "row.checkinLabel": "每日签到",
2428
2952
  "row.checkinReward": "每日 {reward} 积分",
2429
- "row.checkinClaim": "立即领取",
2953
+ "row.checkinClaim": "签到",
2954
+ "row.checkinClaimWith": "签到 +{reward}",
2430
2955
  "row.checkinClaiming": "领取中…",
2431
- "row.checkinClaimed": "今日已领取",
2432
- "row.checkinDoneHint": "明天再来",
2956
+ "row.checkinClaimed": "已领取",
2433
2957
  "row.checkinRefused": "Trae 拒绝了本次签到(业务码 {code})",
2434
2958
  "row.checkinError": "签到失败:{message}",
2435
2959
  "row.checkinDeviceSpent": "本机今日的签到已用掉——Trae 按「每台设备每天一次」限制,所以当前账号在本机今天无法再签。可换一台设备领取,或用今天已签到的那台设备/账号,或明天再来。",
2436
2960
  "row.checkinDisabled": "该账号当前没有可用的签到活动。",
2437
- "row.regionCn": "国内版",
2438
- "row.regionAi": "国际版",
2961
+ "row.checkinDeviceSpentShort": "本机已签",
2439
2962
  "row.subscriptionLabel": "订阅状态",
2440
- "row.subscribed": "已有有效套餐",
2441
- "row.noPackage": "暂无有效套餐",
2963
+ "row.subscribed": "已有套餐",
2964
+ "row.noPackage": "无套餐",
2965
+ "row.planTier": "{tier} 套餐",
2442
2966
  "row.trialUntil": "试用至 {date}",
2443
2967
  "row.payStatusError": "订阅状态查询失败:{message}",
2968
+ "row.payStatusUnknown": "套餐尚未读到",
2444
2969
  "row.refresh": "刷新",
2445
2970
  "row.refreshTokens": "刷新账号与 Token",
2446
2971
  "row.refreshCredits": "刷新用量",
2447
2972
  "row.refreshing": "正在刷新…",
2448
2973
  "row.accountsTitle": "Trae 账号",
2449
- "row.accountsHint": "选择本机检测到的登录账号;Token 不会显示,也不会保存到 DSH 设置。",
2974
+ "row.accountsHint": "本机检测到的登录账号;Token 不会显示或保存。",
2450
2975
  "row.accountsRescan": "重新检测账号",
2451
2976
  "row.accountsScanning": "正在检测…",
2977
+ "row.accountsNone": "该区域没有可列出的账号。",
2978
+ "row.accountCurrentBadge": "当前使用",
2979
+ "row.accountSwitch": "切换",
2980
+ "row.accountSwitching": "切换中…",
2981
+ "row.accountColumnAccount": "账号",
2982
+ "row.accountColumnValue": "额度",
2983
+ "row.accountColumnCheckin": "签到",
2984
+ "row.accountBalance": "积分 {credits}",
2985
+ "row.sourceDsh": "插件副本",
2452
2986
  "row.modelsTitle": "模型",
2453
2987
  "row.modelsSummary": "已启用 {count} 个",
2454
2988
  "row.modelsRefresh": "从 Trae 刷新",
2455
2989
  "row.modelsRefreshing": "正在刷新模型…",
2456
2990
  "row.modelsTestEnabled": "一键测试启用模型",
2991
+ "row.modelsTestAll": "一键测试所有模型",
2457
2992
  "row.modelsTesting": "正在测试 {done}/{total}…",
2458
2993
  "row.modelsTestHint": "给每个已启用模型发一条最小消息,看哪些真的能应答。会消耗少量额度。",
2994
+ "row.modelsTestAllHint": "「测试所有模型」会遍历目录里的每一个模型(含未启用的),消耗的额度明显多于「仅测试启用」。",
2459
2995
  "row.showPointsInMainUi": "在输入框工具栏显示积分读数",
2460
- "row.showPointsInMainUiHint": "在输入框工具栏显示可点击的「Trae CN · 余额」读数,仅当选中的模型属于本插件时出现;点击可查看账号与积分明细。",
2996
+ "row.showPointsInMainUiHint": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国内版为「Trae CN · 余额」,国际版为「Trae 国际版 · 套餐」。",
2997
+ "row.showPointsInMainUiHintAi": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国际版为「Trae 国际版 · 套餐」。",
2461
2998
  "composer.switchTo": "切换到 {account}",
2462
2999
  "composer.noAccounts": "该区域暂无账号",
2463
3000
  "composer.panelTitle": "Trae 积分",
3001
+ "composer.panelTitleGlobal": "Trae 国际版套餐",
2464
3002
  "composer.account": "账号",
2465
3003
  "composer.generalCredits": "通用积分",
3004
+ "composer.plan": "套餐",
2466
3005
  "composer.lastRefresh": "最近刷新",
2467
3006
  "composer.signedOut": "未登录",
2468
3007
  "composer.points": "Trae CN",
3008
+ "composer.pointsGlobal": "Trae 国际版",
2469
3009
  "composer.refresh": "刷新积分",
2470
3010
  "composer.refreshing": "正在刷新积分…",
2471
3011
  "composer.refreshFailed": "积分刷新失败",
2472
3012
  "row.accountsRefreshed": "已刷新账号:找到 {count} 个账号{selected}",
3013
+ "row.accountsSaved": "已绑定",
2473
3014
  "row.accountsKept": "仍绑定",
2474
3015
  "row.accountsRefreshedNone": "已刷新账号:未找到可用的 Trae 账号",
2475
3016
  "row.accountsRescanFailed": "刷新账号失败:{message}",
@@ -2480,10 +3021,11 @@ window.__ModuleLoader__.load({
2480
3021
  "row.creditsExhausted": "当前绑定账号({account})额度已用尽",
2481
3022
  "row.creditsAlternative": "本机还有可用账号:{accounts}",
2482
3023
  "row.creditsAlternativeSwitch": "切换到它",
2483
- "row.appLoginMismatch": "Trae 当前登录的是 {account},但本插件仍绑定在上一个账号",
2484
- "row.appLoginFollow": "切换过去",
3024
+ "row.appLoginMismatch": "Trae 已登录 {account},但本插件绑定的是 {bound}。",
3025
+ "row.appLoginFollow": "改用 {account}",
2485
3026
  "row.followAppLogin": "跟随 Trae 当前登录的账号",
2486
- "row.followAppLoginHint": "在 Trae 桌面端切换登录后,自动换绑到新登录的账号,而不是停留在旧账号。默认关闭:已有绑定保持不变,直到你勾选它。",
3027
+ "row.followAppLoginHint": "在 Trae 桌面端切换登录后自动换绑到新账号。默认关闭,已有绑定保持不变。",
3028
+ "row.settingsDirty": "有未保存的修改",
2487
3029
  "row.modelsTestEmpty": "请先启用至少一个模型。",
2488
3030
  "row.modelsTestError": "测试失败:{message}",
2489
3031
  "row.modelsTestDone": "{total} 个里有 {available} 个正常应答。",