dsh-connect-trae 4.1.0 → 4.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
@@ -18,6 +18,11 @@ window.__ModuleLoader__.load({
18
18
  function regionOfPanel(panel) {
19
19
  return panel === "ai" ? "ai" : "cn";
20
20
  }
21
+ /** The provider id a panel publishes. */
22
+ function providerOfPanel(panel) {
23
+ if (panel === "enterprise") return "trae-enterprise";
24
+ return panel === "ai" ? "trae-global" : "trae";
25
+ }
21
26
  //#endregion
22
27
  //#region src/status-paths.ts
23
28
  /** Plugin-owned usage endpoint consumed by its browser half. */
@@ -374,6 +379,125 @@ window.__ModuleLoader__.load({
374
379
  return tier !== void 0 && tier !== "" && !/^free$/i.test(tier);
375
380
  }
376
381
  //#endregion
382
+ //#region src/promo.ts
383
+ /** Zone used when upstream omits `off_peak.time_zone` (CN accounts). */
384
+ const TRAE_PROMO_DEFAULT_TIME_ZONE = "Asia/Shanghai";
385
+ const WEEKDAY_INDEX = {
386
+ Mon: 1,
387
+ Tue: 2,
388
+ Wed: 3,
389
+ Thu: 4,
390
+ Fri: 5,
391
+ Sat: 6,
392
+ Sun: 7
393
+ };
394
+ /** Wall clock in one named zone: ISO weekday (1–7) plus minutes since midnight. */
395
+ function zonedClock(now, timeZone) {
396
+ let parts;
397
+ try {
398
+ parts = new Intl.DateTimeFormat("en-US", {
399
+ timeZone,
400
+ weekday: "short",
401
+ hour: "2-digit",
402
+ minute: "2-digit",
403
+ hourCycle: "h23"
404
+ }).formatToParts(now);
405
+ } catch {
406
+ return;
407
+ }
408
+ let isoWeekday;
409
+ let hour;
410
+ let minute;
411
+ for (const part of parts) if (part.type === "weekday") isoWeekday = WEEKDAY_INDEX[part.value];
412
+ else if (part.type === "hour") hour = Number.parseInt(part.value, 10);
413
+ else if (part.type === "minute") minute = Number.parseInt(part.value, 10);
414
+ if (isoWeekday === void 0 || hour === void 0 || minute === void 0) return void 0;
415
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) return void 0;
416
+ return {
417
+ isoWeekday,
418
+ minutes: hour % 24 * 60 + minute
419
+ };
420
+ }
421
+ function clockOf(minutes) {
422
+ const hour = Math.floor(minutes / 60);
423
+ const minute = minutes % 60;
424
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
425
+ }
426
+ function runningWindow(discount, now, defaultTimeZone) {
427
+ const offPeak = discount.offPeak;
428
+ if (offPeak === void 0 || offPeak.windows.length === 0) return void 0;
429
+ const declaredZone = offPeak.timeZone ?? defaultTimeZone;
430
+ let usedZone = declaredZone;
431
+ let clock = zonedClock(now, declaredZone);
432
+ if (clock === void 0 && declaredZone !== defaultTimeZone) {
433
+ usedZone = defaultTimeZone;
434
+ clock = zonedClock(now, defaultTimeZone);
435
+ }
436
+ if (clock === void 0) return void 0;
437
+ const match = offPeak.windows.find((window) => window.weekdays.includes(clock.isoWeekday) && clock.minutes >= window.startMinute && clock.minutes < window.endMinute);
438
+ return match === void 0 ? void 0 : {
439
+ ...match,
440
+ timeZone: usedZone
441
+ };
442
+ }
443
+ /** `subKey` is upstream's machine name; it is the only honest source for "限时". */
444
+ function kindOf(discount) {
445
+ const key = discount.subKey ?? "";
446
+ if (key.includes("off_peak")) return "offPeak";
447
+ if (key.includes("subsidy")) return "subsidy";
448
+ if (key.includes("limited")) return "limited";
449
+ const type = discount.current?.discountType ?? "";
450
+ if (type.includes("off")) return "offPeak";
451
+ if (type.includes("limited")) return "limited";
452
+ return "subsidy";
453
+ }
454
+ /** Pay ratio as upstream would phrase it; falls back to the rates it published. */
455
+ function discountOf(price) {
456
+ if (price.discountPercent !== void 0) return price.discountPercent;
457
+ if (price.beforeRate === void 0 || price.rate === void 0) return void 0;
458
+ if (price.beforeRate <= 0 || price.rate >= price.beforeRate) return void 0;
459
+ return Math.round(price.rate / price.beforeRate * 100);
460
+ }
461
+ function badgeOf(kind, price, discount, window) {
462
+ const discountPercent = discountOf(price);
463
+ if (discountPercent === void 0) return void 0;
464
+ const memberDiscountPercent = discount.member === void 0 ? void 0 : discountOf(discount.member);
465
+ return {
466
+ kind,
467
+ discountPercent,
468
+ percentOff: 100 - discountPercent,
469
+ ...price.beforeRate === void 0 ? {} : { beforeRate: price.beforeRate },
470
+ ...price.rate === void 0 ? {} : { rate: price.rate },
471
+ ...memberDiscountPercent === void 0 ? {} : { memberDiscountPercent },
472
+ ...window === void 0 ? {} : {
473
+ fromClock: clockOf(window.startMinute),
474
+ untilClock: clockOf(window.endMinute),
475
+ timeZone: window.timeZone
476
+ }
477
+ };
478
+ }
479
+ /**
480
+ * Decide the one badge (if any) a model shows right now.
481
+ *
482
+ * `now` is injected so the verdict is testable and so the card, which re-renders
483
+ * on its own poll, always judges against the current clock. `defaultTimeZone` is
484
+ * the account's zone, used only when upstream omits `off_peak.time_zone`.
485
+ */
486
+ function selectTraePromoBadge(discount, now, defaultTimeZone = TRAE_PROMO_DEFAULT_TIME_ZONE) {
487
+ if (discount === void 0) return void 0;
488
+ const current = discount.current;
489
+ if (current !== void 0) {
490
+ if ((current.discountType ?? "none") !== "none" && (current.beforeRate === void 0 || current.rate === void 0 || current.rate < current.beforeRate)) {
491
+ const badge = badgeOf(kindOf(discount), current, discount);
492
+ if (badge !== void 0) return badge;
493
+ }
494
+ }
495
+ const window = runningWindow(discount, now, defaultTimeZone);
496
+ if (window !== void 0 && discount.offPeak !== void 0) {
497
+ if ((discount.current?.discountType ?? "none") === "none" || discountOf(discount.offPeak) !== void 0) return badgeOf("offPeak", discount.offPeak, discount, window);
498
+ }
499
+ }
500
+ //#endregion
377
501
  //#region src/client/icon.ts
378
502
  /**
379
503
  * Plugin icon (LD brand logo, 64px) shared across the LaoDing plugin family.
@@ -437,6 +561,45 @@ window.__ModuleLoader__.load({
437
561
  .dsm-trae-tab-switch{flex:none;margin-left:auto;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24))}
438
562
  .dsm-trae-tab-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:7px;vertical-align:baseline}
439
563
  .dsm-trae-tab-hint{margin:0;padding:0 2px;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
564
+ /* SECTION TABS: the card's own top-level split (account / models / diagnostics).
565
+ *
566
+ * Deliberately NOT the boxed chips above. Those are the REGION scope — a
567
+ * three-way choice of which PROVIDER this whole card is about, each chip
568
+ * carrying its own on/off switch — while these are panels of ONE document, the
569
+ * shape a reader already knows from the app's own settings window. Two rows of
570
+ * boxes would read as one six-way choice.
571
+ *
572
+ * The hidden ATTRIBUTE on each panel is what does the work: an inactive panel
573
+ * stays MOUNTED, so switching back does not refetch, a half-filled model draft
574
+ * survives the trip, and the account pool keeps its log. It also takes the
575
+ * inactive panel out of the accessibility tree, which is exactly what
576
+ * role="tabpanel" wants — unlike display:none reached by a CSS class, the user
577
+ * agent's own rule for [hidden] cannot be overridden by a later declaration. */
578
+ .dsm-trae-panels{display:flex;align-items:stretch;gap:2px;margin:4px 0 0;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24))}
579
+ .dsm-trae-panel-tab{appearance:none;font:inherit;cursor:pointer;border:0;background:transparent;padding:7px 11px;margin-bottom:-1px;border-bottom:2px solid transparent;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;font-weight:600;line-height:20px;white-space:nowrap;transition:color .15s,border-color .15s}
580
+ .dsm-trae-panel-tab:hover:not(.dsm-trae-panel-tab-active){color:var(--dsw-alias-label-primary,#e6e6e6)}
581
+ .dsm-trae-panel-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
582
+ .dsm-trae-panel-tab-active{color:var(--dsw-alias-label-primary,#e6e6e6);border-bottom-color:var(--dsw-alias-brand-primary,#5686fe)}
583
+ /* No top margin of its own: the strip above already carries a border, and the
584
+ card's 16px stack gap would double the separation. */
585
+ .dsm-trae-panel{margin-top:0}
586
+ /* DIAGNOSTICS: a two-column fact table rather than prose. Each row is one thing
587
+ * the card actually READ, so a bug report can quote a line instead of
588
+ * paraphrasing the UI. No control lives here on purpose — a diagnostics panel
589
+ * that can change settings is a settings panel with a worse name. */
590
+ .dsm-trae-diag{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#232529)}
591
+ .dsm-trae-diag-row{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:8px 12px}
592
+ .dsm-trae-diag-row+.dsm-trae-diag-row{border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24))}
593
+ .dsm-trae-diag-label{flex:none;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
594
+ .dsm-trae-diag-value{min-width:0;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:12px;line-height:18px;text-align:right;overflow-wrap:anywhere}
595
+ .dsm-trae-diag-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
596
+ /* A failure line, in the same warn tone the card already uses for an expiring
597
+ credit — NOT a hand-rolled amber. The token set the card asks DSH for is
598
+ audited by the card-styles spec (every name must be one the host actually
599
+ defines), so this deliberately names state-warn-primary rather than the
600
+ sibling card's label-caution, which this suite has never verified. */
601
+ .dsm-trae-diag-bad{color:var(--dsw-alias-state-warn-primary,#b45309)}
602
+ .dsm-trae-diag-hint{margin:10px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
440
603
  .dsm-trae-credits-unavailable{margin:10px 0 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)}
441
604
  .dsm-trae-account-value-muted{color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px}
442
605
  .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)}
@@ -619,6 +782,17 @@ window.__ModuleLoader__.load({
619
782
  .dsm-trae-models-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
620
783
  .dsm-trae-models-title{margin:0;color:var(--dsw-alias-label-primary,#e6e6e6);font-size:14px;font-weight:600;line-height:20px}
621
784
  .dsm-trae-models-summary{margin:2px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:18px}
785
+ /* EVERY model is on the page — no inner scroller, no max-height.
786
+ *
787
+ * A capped list with its own scrollbar was tried and rejected by the user: it
788
+ * turned a settings section into a little window that has to be scrolled inside
789
+ * a page that already scrolls, and put the models a user is looking for behind a
790
+ * gesture that does not match the rest of the card. The section is allowed to be
791
+ * as tall as the catalog is; the card's own page scroll is the only scroll.
792
+ *
793
+ * overflow:hidden is for the rounded border only (so the row hairlines cannot
794
+ * paint outside the radius). It is deliberately NOT auto: nothing here clips
795
+ * content vertically. */
622
796
  .dsm-trae-model-list{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:10px;overflow:hidden}
623
797
  .dsm-trae-model{display:grid;grid-template-columns:minmax(0,1fr);gap:7px;padding:10px 12px;background:var(--dsw-alias-bg-layer-2,#232529);transition:opacity .16s}
624
798
  .dsm-trae-model-disabled{opacity:.55}
@@ -691,6 +865,15 @@ window.__ModuleLoader__.load({
691
865
  .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)}
692
866
  .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)}
693
867
  .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)}
868
+ /* Promotion capsule (src/promo.ts). Unlike WorkBuddy's badge there is NO
869
+ upstream colour to honour — Trae publishes a pay ratio and nothing else — so
870
+ these three colours are the plugin's own choice, picked only so the kinds stay
871
+ distinguishable at a glance: red = subsidy price, orange = explicitly
872
+ "limited", blue = off-peak hours. */
873
+ .dsm-trae-model-promo{flex:none;border-radius:999px;padding:1px 7px;font-size:10px;font-weight:500;line-height:15px;white-space:nowrap;border:1px solid transparent}
874
+ .dsm-trae-model-promo-subsidy{color:var(--dsw-alias-state-error-primary,#d92d20);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 10%,transparent)}
875
+ .dsm-trae-model-promo-limited{color:var(--dsw-alias-state-warn-primary,#f59e0b);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 12%,transparent)}
876
+ .dsm-trae-model-promo-offPeak{color:var(--dsw-alias-brand-primary,#5686fe);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#5686fe) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5686fe) 10%,transparent)}
694
877
  .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}
695
878
  .dsm-trae-model-options{display:flex;align-items:center;justify-content:flex-end;gap:12px;flex:none}
696
879
  .dsm-trae-model-image{display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px;cursor:pointer}
@@ -2800,6 +2983,418 @@ window.__ModuleLoader__.load({
2800
2983
  });
2801
2984
  }
2802
2985
  //#endregion
2986
+ //#region src/client/display.ts
2987
+ /** A group-separated integer, in the reader's own locale. */
2988
+ function formatNumber(value) {
2989
+ return new Intl.NumberFormat(void 0, {
2990
+ minimumFractionDigits: 0,
2991
+ maximumFractionDigits: 0
2992
+ }).format(value);
2993
+ }
2994
+ /** A date and time, in the reader's own locale. */
2995
+ function formatDateTime(value) {
2996
+ return new Intl.DateTimeFormat(void 0, {
2997
+ year: "numeric",
2998
+ month: "2-digit",
2999
+ day: "2-digit",
3000
+ hour: "2-digit",
3001
+ minute: "2-digit"
3002
+ }).format(new Date(value));
3003
+ }
3004
+ /**
3005
+ * The colour of a status dot, as an inline style.
3006
+ *
3007
+ * Inline rather than a class because the colour IS the value (signed-in / error
3008
+ * / signed-out); a class per state would be three names that say the same thing.
3009
+ */
3010
+ function dotStyle(status) {
3011
+ return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
3012
+ }
3013
+ /** How close a refresh token must be to lapsing before the card warns, in days. */
3014
+ const REFRESH_WARN_DAYS = 30;
3015
+ /**
3016
+ * The refresh-token warning for one account, or undefined when there is nothing
3017
+ * worth saying.
3018
+ *
3019
+ * The access token renews itself every few minutes, so its own expiry answers
3020
+ * "when does this need attention" with "never, until the refresh token lapses".
3021
+ * The refresh token is therefore the only honest countdown on this card, and it
3022
+ * has to be shown BEFORE it runs out: once it lapses the plugin cannot renew at
3023
+ * all and the only recovery is signing in to Trae again, which a user who has
3024
+ * been reading "auto-renewal" for months has no reason to expect.
3025
+ *
3026
+ * Three tones, matching the credit-expiry notice: `lapsed` (renewal already
3027
+ * dead), `soon` (inside the warning window), and nothing otherwise — a healthy
3028
+ * token must not add a line to every account row forever.
3029
+ */
3030
+ function refreshTokenWarning(expiresAtMs, nowMs, t, formatTime) {
3031
+ if (expiresAtMs === void 0) return void 0;
3032
+ const text = formatTime(expiresAtMs);
3033
+ if (expiresAtMs <= nowMs) return {
3034
+ tone: "lapsed",
3035
+ text: t("row.refreshTokenLapsed", { expiresAt: text })
3036
+ };
3037
+ const days = Math.ceil((expiresAtMs - nowMs) / 864e5);
3038
+ if (days > REFRESH_WARN_DAYS) return void 0;
3039
+ return {
3040
+ tone: "soon",
3041
+ text: t("row.refreshTokenExpiring", {
3042
+ expiresAt: text,
3043
+ days
3044
+ })
3045
+ };
3046
+ }
3047
+ //#endregion
3048
+ //#region src/client/TraeAccountPanel.tsx
3049
+ /**
3050
+ * A warning that credit is about to lapse.
3051
+ *
3052
+ * The balance alone cannot express this: a pack lapsing in an hour and one
3053
+ * lapsing next month both render as "150", so a user watching only the total
3054
+ * loses the first one silently. The notice names the amount, the pack count and
3055
+ * the soonest expiry, and escalates 7d → 24h → already-lapsed.
3056
+ *
3057
+ * Renders nothing when the upstream reported no end times at all — absence of
3058
+ * expiry data is not evidence that nothing lapses, so the component stays silent
3059
+ * rather than reassuring.
3060
+ */
3061
+ function ExpiryNotice({ expiry, t }) {
3062
+ const { expired, within24h, within7d } = expiry;
3063
+ let message;
3064
+ let soonest;
3065
+ let tone;
3066
+ if (within24h.packs > 0) {
3067
+ message = t("row.expiryWarningSoon", {
3068
+ total: formatNumber(within24h.total),
3069
+ packs: String(within24h.packs)
3070
+ });
3071
+ soonest = within24h.earliestExpiresAtMs;
3072
+ tone = "dsm-trae-expiry-soon";
3073
+ } else if (within7d.packs > 0) {
3074
+ message = t("row.expiryWarningWeek", {
3075
+ total: formatNumber(within7d.total),
3076
+ packs: String(within7d.packs)
3077
+ });
3078
+ soonest = within7d.earliestExpiresAtMs;
3079
+ tone = "dsm-trae-expiry-week";
3080
+ } else if (expired.packs > 0) {
3081
+ message = t("row.expiryWarningLapsed", {
3082
+ total: formatNumber(expired.total),
3083
+ packs: String(expired.packs)
3084
+ });
3085
+ tone = "dsm-trae-expiry-lapsed";
3086
+ } else return null;
3087
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3088
+ className: `dsm-trae-expiry ${tone}`,
3089
+ role: "status",
3090
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message }), soonest === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3091
+ className: "dsm-trae-expiry-detail",
3092
+ children: t("row.expiryEarliest", { time: formatDateTime(soonest) })
3093
+ })]
3094
+ });
3095
+ }
3096
+ /** The CN check-in cell for one row: an outcome once there is nothing left to do. */
3097
+ function ClaimCell({ claim, onClaim, t }) {
3098
+ if (!claim.claimable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3099
+ className: "dsm-trae-account-checkin-state",
3100
+ children: claim.settled === "claimed" ? t("row.poolCheckedIn") : t("row.poolDeviceUsed")
3101
+ });
3102
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3103
+ type: "button",
3104
+ className: "dsm-trae-account-checkin",
3105
+ disabled: claim.disabled,
3106
+ title: claim.title,
3107
+ onClick: onClaim,
3108
+ children: claim.label
3109
+ });
3110
+ }
3111
+ /**
3112
+ * The account section: preferences, the roster, the pool, and every notice about
3113
+ * the credential.
3114
+ *
3115
+ * Everything here commits ON THE CLICK (the preference boxes write themselves,
3116
+ * the check-in claims immediately, the pool saves through its own footer). That
3117
+ * is precisely why this section has no 「保存」 of its own — the model catalog,
3118
+ * which does, lives in its own section.
3119
+ */
3120
+ function TraeAccountPanel({ t, settingsScope, panel, status, statusLabel, credentialRegionIsAi, regionOn, showPointsInMainUi, onToggleShowPoints, creditPosition, onCreditPositionChange, savingPreferences, creditsLabelId, creditsHintId, rows, busy, rescanNote, onRescan, switchingAccount, onSwitchAccount, onClaim, onTransferChanged, poolDetails, poolClaims, models, deviceClaimed, onPoolChanged, onPoolRefresh, creditsUnavailable, creditsExpiry, alternatives, onSwitchToAlternative }) {
3121
+ const writable = settingsScope?.getSnapshot().writable === true;
3122
+ const signedIn = status.status === "signed-in";
3123
+ const accounts = signedIn ? status.accounts : [];
3124
+ const pool = signedIn ? status.pool : void 0;
3125
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3127
+ className: "dsm-trae-prefs",
3128
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3129
+ className: "dsm-trae-pref",
3130
+ title: t(credentialRegionIsAi ? "row.showPointsInMainUiHintAi" : "row.showPointsInMainUiHint"),
3131
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3132
+ type: "checkbox",
3133
+ className: "dsm-trae-pref-check",
3134
+ checked: showPointsInMainUi,
3135
+ disabled: !writable || savingPreferences,
3136
+ "aria-describedby": creditsHintId,
3137
+ onChange: (event) => {
3138
+ onToggleShowPoints(event.currentTarget.checked);
3139
+ }
3140
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3141
+ className: "dsm-trae-pref-copy",
3142
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3143
+ className: "dsm-trae-pref-label",
3144
+ id: creditsLabelId,
3145
+ children: t("row.showPointsInMainUi")
3146
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3147
+ className: "dsm-trae-pref-hint",
3148
+ id: creditsHintId,
3149
+ children: t(credentialRegionIsAi ? "row.showPointsInMainUiHintAi" : "row.showPointsInMainUiHint")
3150
+ })]
3151
+ })]
3152
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
3153
+ className: "dsm-trae-pref dsm-trae-credit-position-pref",
3154
+ title: t("row.creditPositionHint"),
3155
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3156
+ className: "dsm-trae-pref-copy",
3157
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3158
+ className: "dsm-trae-pref-lead",
3159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3160
+ className: "dsm-trae-pref-label",
3161
+ children: t("row.creditPositionLabel")
3162
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3163
+ className: "dsm-trae-credit-position",
3164
+ value: regionOn ? creditPosition : "composer",
3165
+ disabled: !regionOn || !writable || savingPreferences,
3166
+ "aria-label": t("row.creditPositionAria", { region: panel === "cn" ? t("row.tabCn") : panel === "enterprise" ? t("row.tabEnterprise") : t("row.tabAi") }),
3167
+ onChange: (event) => {
3168
+ onCreditPositionChange(event.currentTarget.value === "stats" ? "stats" : "composer");
3169
+ },
3170
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3171
+ value: "composer",
3172
+ children: t("row.creditPositionComposer")
3173
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3174
+ value: "stats",
3175
+ children: t("row.creditPositionStats")
3176
+ })]
3177
+ })]
3178
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3179
+ className: "dsm-trae-pref-hint",
3180
+ children: t("row.creditPositionHint")
3181
+ })]
3182
+ })
3183
+ })]
3184
+ }),
3185
+ pool !== void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3186
+ className: "dsm-trae-accounts",
3187
+ "aria-label": t("row.accountsTitle"),
3188
+ children: [
3189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3190
+ className: "dsm-trae-accounts-head",
3191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3192
+ className: "dsm-trae-accounts-title",
3193
+ children: t("row.accountsTitle")
3194
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3195
+ className: "dsm-trae-accounts-hint",
3196
+ children: t("row.accountsHint")
3197
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3198
+ type: "button",
3199
+ className: "dsm-btn dsm-btn-outline",
3200
+ disabled: busy,
3201
+ onClick: onRescan,
3202
+ children: busy ? t("row.accountsScanning") : t("row.refreshTokens")
3203
+ })]
3204
+ }),
3205
+ !signedIn || accounts.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3206
+ className: "dsm-trae-usage-account",
3207
+ role: "status",
3208
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3209
+ className: "dsm-trae-usage-account-copy",
3210
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3211
+ className: "dsm-trae-usage-status",
3212
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3213
+ "aria-hidden": "true",
3214
+ className: "dsm-trae-usage-dot",
3215
+ style: dotStyle(status.status)
3216
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: statusLabel })]
3217
+ }), signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3218
+ className: "dsm-trae-usage-expiry",
3219
+ children: t("row.tokenExpiry", { expiresAt: formatDateTime(status.tokenExpiresAtMs) })
3220
+ }) : null]
3221
+ })
3222
+ }) : null,
3223
+ status.status !== "error" && rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3224
+ className: `dsm-trae-account-table${credentialRegionIsAi ? "" : " dsm-trae-account-table-checkin"}`,
3225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3226
+ className: "dsm-trae-account-row dsm-trae-account-row-head",
3227
+ role: "row",
3228
+ children: [
3229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnAccount") }),
3230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnValue") }),
3231
+ credentialRegionIsAi ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnCheckin") }),
3232
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})
3233
+ ]
3234
+ }), rows.map((row) => {
3235
+ const warning = refreshTokenWarning(row.refreshExpiresAtMs, Date.now(), t, formatDateTime);
3236
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3237
+ className: `dsm-trae-account-row${row.current ? " dsm-trae-account-row-current" : ""}`,
3238
+ role: "row",
3239
+ children: [
3240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3241
+ className: "dsm-trae-account-who",
3242
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3243
+ "aria-hidden": "true",
3244
+ className: "dsm-trae-usage-dot",
3245
+ style: dotStyle(row.selected ? "signed-in" : "signed-out")
3246
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3247
+ className: "dsm-trae-account-name",
3248
+ children: [
3249
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: row.accountName }),
3250
+ row.current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3251
+ className: "dsm-trae-account-badge",
3252
+ children: t("row.accountCurrentBadge")
3253
+ }) : null,
3254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3255
+ className: "dsm-trae-account-meta",
3256
+ children: row.meta
3257
+ }),
3258
+ warning === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3259
+ className: `dsm-trae-refresh-warn dsm-trae-refresh-warn-${warning.tone}`,
3260
+ role: "status",
3261
+ children: warning.text
3262
+ }),
3263
+ row.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3264
+ className: "dsm-trae-account-note",
3265
+ role: "alert",
3266
+ children: row.error
3267
+ })
3268
+ ]
3269
+ })]
3270
+ }),
3271
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3272
+ className: "dsm-trae-account-worth",
3273
+ children: row.value
3274
+ }),
3275
+ credentialRegionIsAi ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3276
+ className: "dsm-trae-account-checkin-cell",
3277
+ children: row.claim === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClaimCell, {
3278
+ claim: row.claim,
3279
+ t,
3280
+ onClaim: () => {
3281
+ onClaim(row.id);
3282
+ }
3283
+ })
3284
+ }),
3285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3286
+ className: "dsm-trae-account-actions",
3287
+ children: row.current ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3288
+ type: "button",
3289
+ className: "dsm-btn dsm-btn-outline dsm-trae-account-switch",
3290
+ disabled: switchingAccount || !writable,
3291
+ onClick: () => {
3292
+ onSwitchAccount(row.id);
3293
+ },
3294
+ children: switchingAccount ? t("row.accountSwitching") : t("row.accountSwitch")
3295
+ })
3296
+ })
3297
+ ]
3298
+ }, row.id);
3299
+ })]
3300
+ }) : null,
3301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
3302
+ t,
3303
+ region: panel === "ai" ? "ai" : "cn",
3304
+ accounts: accounts.map((account) => ({
3305
+ id: account.id,
3306
+ accountName: account.accountName
3307
+ })),
3308
+ onChanged: onTransferChanged
3309
+ })
3310
+ ]
3311
+ }),
3312
+ signedIn && creditsUnavailable === "enterprise" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3313
+ className: "dsm-trae-credits-unavailable",
3314
+ role: "note",
3315
+ children: t("row.enterpriseCreditsNotice")
3316
+ }) : null,
3317
+ signedIn && alternatives !== void 0 && alternatives.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3318
+ className: "dsm-trae-usage-alternative",
3319
+ role: "status",
3320
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3321
+ className: "dsm-trae-usage-alternative-text",
3322
+ children: [
3323
+ t("row.creditsExhausted", { account: status.accountName }),
3324
+ " ",
3325
+ t("row.creditsAlternative", { accounts: alternatives.map((alt) => `${alt.accountName}(${t(`row.edition${alt.edition === "cn" ? "Cn" : alt.edition === "sg" ? "Sg" : alt.edition === "solo" ? "Solo" : "SoloSg"}`)}:${formatNumber(alt.workAvailable + alt.generalAvailable)})`).join("、") })
3326
+ ]
3327
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3328
+ type: "button",
3329
+ className: "dsm-trae-usage-switch-button",
3330
+ disabled: switchingAccount || !writable,
3331
+ onClick: () => {
3332
+ const [target] = alternatives;
3333
+ if (target !== void 0) onSwitchToAlternative(target.id);
3334
+ },
3335
+ children: t("row.creditsAlternativeSwitch")
3336
+ })]
3337
+ }) : null,
3338
+ signedIn && creditsExpiry !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExpiryNotice, {
3339
+ expiry: creditsExpiry,
3340
+ t
3341
+ }) : null,
3342
+ rescanNote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3343
+ className: "dsm-trae-usage-note",
3344
+ role: "status",
3345
+ children: rescanNote
3346
+ }),
3347
+ signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3348
+ pool !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPool, {
3349
+ panel,
3350
+ pool,
3351
+ models,
3352
+ t,
3353
+ settingsScope,
3354
+ topActions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3355
+ type: "button",
3356
+ className: "dsm-btn dsm-btn-outline",
3357
+ disabled: busy,
3358
+ onClick: onRescan,
3359
+ children: busy ? t("row.accountsScanning") : t("row.poolRescan")
3360
+ }),
3361
+ tools: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3362
+ className: "dsm-trae-pool-tool-row",
3363
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3364
+ className: "dsm-trae-pool-tool-label",
3365
+ children: t("row.poolToolsLabel")
3366
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
3367
+ t,
3368
+ hideLabel: true,
3369
+ region: panel === "ai" ? "ai" : "cn",
3370
+ accounts: accounts.map((account) => ({
3371
+ id: account.id,
3372
+ accountName: account.accountName
3373
+ })),
3374
+ onChanged: onTransferChanged
3375
+ })]
3376
+ }),
3377
+ details: poolDetails,
3378
+ claims: pool.checkinSupported === true ? poolClaims : void 0,
3379
+ onClaim,
3380
+ deviceClaimed,
3381
+ onBind: onSwitchAccount,
3382
+ bindingBusy: switchingAccount,
3383
+ onChanged: onPoolChanged,
3384
+ onRefresh: onPoolRefresh
3385
+ }) : null,
3386
+ status.checkin !== void 0 && !status.checkin.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3387
+ className: "dsm-trae-usage-error",
3388
+ children: t("row.checkinDisabled")
3389
+ }) : null,
3390
+ status.checkinError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3391
+ className: "dsm-trae-usage-error",
3392
+ children: t("row.checkinError", { message: status.checkinError })
3393
+ })
3394
+ ] }) : null
3395
+ ] });
3396
+ }
3397
+ //#endregion
2803
3398
  //#region src/client/plan-text.ts
2804
3399
  /**
2805
3400
  * The international region's "what plan is this account on" text.
@@ -2895,55 +3490,6 @@ window.__ModuleLoader__.load({
2895
3490
  document.head.appendChild(styleTag);
2896
3491
  }
2897
3492
  }
2898
- function formatNumber(value) {
2899
- return new Intl.NumberFormat(void 0, {
2900
- minimumFractionDigits: 0,
2901
- maximumFractionDigits: 0
2902
- }).format(value);
2903
- }
2904
- function formatDateTime(value) {
2905
- return new Intl.DateTimeFormat(void 0, {
2906
- year: "numeric",
2907
- month: "2-digit",
2908
- day: "2-digit",
2909
- hour: "2-digit",
2910
- minute: "2-digit"
2911
- }).format(new Date(value));
2912
- }
2913
- /** How close a refresh token must be to lapsing before the card warns, in days. */
2914
- const REFRESH_WARN_DAYS = 30;
2915
- /**
2916
- * The refresh-token warning for one account, or undefined when there is nothing
2917
- * worth saying.
2918
- *
2919
- * The access token renews itself every few minutes, so its own expiry answers
2920
- * "when does this need attention" with "never, until the refresh token lapses".
2921
- * The refresh token is therefore the only honest countdown on this card, and it
2922
- * has to be shown BEFORE it runs out: once it lapses the plugin cannot renew at
2923
- * all and the only recovery is signing in to Trae again, which a user who has
2924
- * been reading "auto-renewal" for months has no reason to expect.
2925
- *
2926
- * Three tones, matching the credit-expiry notice: `lapsed` (renewal already
2927
- * dead), `soon` (inside the warning window), and nothing otherwise — a healthy
2928
- * token must not add a line to every account row forever.
2929
- */
2930
- function refreshTokenWarning(expiresAtMs, nowMs, t, formatTime) {
2931
- if (expiresAtMs === void 0) return void 0;
2932
- const text = formatTime(expiresAtMs);
2933
- if (expiresAtMs <= nowMs) return {
2934
- tone: "lapsed",
2935
- text: t("row.refreshTokenLapsed", { expiresAt: text })
2936
- };
2937
- const days = Math.ceil((expiresAtMs - nowMs) / 864e5);
2938
- if (days > REFRESH_WARN_DAYS) return void 0;
2939
- return {
2940
- tone: "soon",
2941
- text: t("row.refreshTokenExpiring", {
2942
- expiresAt: text,
2943
- days
2944
- })
2945
- };
2946
- }
2947
3493
  function formatCapacity(value, unknown) {
2948
3494
  if (value === void 0) return unknown;
2949
3495
  if (value >= 1e6 && value % 1e6 === 0) return `${value / 1e6}M`;
@@ -2973,54 +3519,70 @@ window.__ModuleLoader__.load({
2973
3519
  children: t(PROBE_LABELS[probe.verdict])
2974
3520
  });
2975
3521
  }
2976
- function dotStyle(status) {
2977
- return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
3522
+ /**
3523
+ * How each promotion kind is phrased.
3524
+ *
3525
+ * The wording is PLUGIN copy, not upstream's: Trae publishes a rule — a pay
3526
+ * ratio plus optional cheap-hours windows — and never a label (contrast
3527
+ * WorkBuddy's `badge.label`, see docs/PROMO_BADGE.md §3). Two things
3528
+ * follow, and both are deliberate:
3529
+ *
3530
+ * - only `limited_discount` says 限时 / "limited"; upstream gives no end time
3531
+ * for the other kinds, so claiming one would be inventing urgency;
3532
+ * - the number shown is upstream's own pay ratio (`discount: 50` ⇒ 五折 /
3533
+ * "50% off"), never a re-derived price.
3534
+ */
3535
+ const PROMO_LABELS = {
3536
+ limited: "row.promoLimited",
3537
+ subsidy: "row.promoSubsidy",
3538
+ offPeak: "row.promoOffPeak"
3539
+ };
3540
+ /** Pay ratio as the "折" figure a Chinese price tag uses: 50 → "5", 25 → "2.5". */
3541
+ function promoZhe(discountPercent) {
3542
+ const zhe = Math.round(discountPercent * 10) / 100;
3543
+ return Number.isInteger(zhe) ? String(zhe) : zhe.toFixed(1);
2978
3544
  }
2979
3545
  /**
2980
- * A warning that credit is about to lapse.
3546
+ * The model row's promotion capsule, or nothing.
2981
3547
  *
2982
- * The balance alone cannot express this: a pack lapsing in an hour and one
2983
- * lapsing next month both render as "150", so a user watching only the total
2984
- * loses the first one silently. The notice names the amount, the pack count and
2985
- * the soonest expiry, and escalates 7d → 24h → already-lapsed.
3548
+ * Judged HERE, at paint time, not when the directory was parsed: the rule's
3549
+ * windows are wall clocks in an upstream-named zone (00:00–09:00 on weekdays,
3550
+ * …), so a verdict frozen into a saved catalog would still be shouting about
3551
+ * last night's discount tomorrow afternoon. The card re-renders on its poll, so
3552
+ * the capsule appears and disappears with the window.
2986
3553
  *
2987
- * Renders nothing when the upstream reported no end times at all — absence of
2988
- * expiry data is not evidence that nothing lapses, so the component stays
2989
- * silent rather than reassuring.
3554
+ * One capsule at most, and it sits BESIDE the name rather than inside it: the
3555
+ * name is ellipsis-truncated, so a badge placed inside would be clipped away
3556
+ * exactly on the long model names.
2990
3557
  */
2991
- function ExpiryNotice({ expiry, t, formatNumber: fmt, formatDateTime: fmtTime }) {
2992
- const { expired, within24h, within7d } = expiry;
2993
- let message;
2994
- let soonest;
2995
- let tone;
2996
- if (within24h.packs > 0) {
2997
- message = t("row.expiryWarningSoon", {
2998
- total: fmt(within24h.total),
2999
- packs: String(within24h.packs)
3000
- });
3001
- soonest = within24h.earliestExpiresAtMs;
3002
- tone = "dsm-trae-expiry-soon";
3003
- } else if (within7d.packs > 0) {
3004
- message = t("row.expiryWarningWeek", {
3005
- total: fmt(within7d.total),
3006
- packs: String(within7d.packs)
3007
- });
3008
- soonest = within7d.earliestExpiresAtMs;
3009
- tone = "dsm-trae-expiry-week";
3010
- } else if (expired.packs > 0) {
3011
- message = t("row.expiryWarningLapsed", {
3012
- total: fmt(expired.total),
3013
- packs: String(expired.packs)
3014
- });
3015
- tone = "dsm-trae-expiry-lapsed";
3016
- } else return null;
3017
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3018
- className: `dsm-trae-expiry ${tone}`,
3019
- role: "status",
3020
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message }), soonest === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3021
- className: "dsm-trae-expiry-detail",
3022
- children: t("row.expiryEarliest", { time: fmtTime(soonest) })
3023
- })]
3558
+ function promoBadge(model, t, now) {
3559
+ const badge = selectTraePromoBadge(model.activityDiscount, now);
3560
+ if (badge === void 0) return null;
3561
+ const zhe = promoZhe(badge.discountPercent);
3562
+ const hints = [];
3563
+ if (badge.beforeRate !== void 0 && badge.rate !== void 0) hints.push(t("row.promoHintRate", {
3564
+ before: String(badge.beforeRate),
3565
+ rate: String(badge.rate)
3566
+ }));
3567
+ if (badge.fromClock !== void 0 && badge.untilClock !== void 0 && badge.timeZone !== void 0) hints.push(t("row.promoHintWindow", {
3568
+ from: badge.fromClock,
3569
+ until: badge.untilClock,
3570
+ zone: badge.timeZone
3571
+ }));
3572
+ if (badge.memberDiscountPercent !== void 0) hints.push(t("row.promoHintMember", {
3573
+ zhe: promoZhe(badge.memberDiscountPercent),
3574
+ percent: String(100 - badge.memberDiscountPercent)
3575
+ }));
3576
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3577
+ className: `dsm-trae-model-promo dsm-trae-model-promo-${badge.kind}`,
3578
+ title: hints.length === 0 ? t(PROMO_LABELS[badge.kind], {
3579
+ zhe,
3580
+ percent: String(badge.percentOff)
3581
+ }) : hints.join(" · "),
3582
+ children: t(PROMO_LABELS[badge.kind], {
3583
+ zhe,
3584
+ percent: String(badge.percentOff)
3585
+ })
3024
3586
  });
3025
3587
  }
3026
3588
  /** Render Trae sign-in state and the total usage summary as one expandable card. */
@@ -3030,6 +3592,27 @@ window.__ModuleLoader__.load({
3030
3592
  /** The region whose tab is on screen; each tab is its own provider stack. */
3031
3593
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
3032
3594
  /**
3595
+ * Which of the card's three sections is on screen (learned from the sibling
3596
+ * `dsh-connect-workbuddy` card, whose settings page split for the same
3597
+ * reason).
3598
+ *
3599
+ * The card had grown into one long column mixing two unrelated jobs —
3600
+ * operating an account (who serves, how much is left, the check-in, the pool)
3601
+ * and editing the catalog (which models, image input, context budget) — and
3602
+ * the two do not even commit the same way: an account change lands on the
3603
+ * click, a catalog change is a draft until 「保存」. One column therefore put
3604
+ * the save button screens away from what it saves.
3605
+ *
3606
+ * The region row above stays the SCOPE (which provider this whole card is
3607
+ * about); these tabs are the sections of that one scope. Only the ACTIVE
3608
+ * section is visible, and the others stay MOUNTED through the `hidden`
3609
+ * attribute, so switching back never refetches, a half-filled model draft
3610
+ * survives the trip, and the pool keeps its log. `hidden` also takes the
3611
+ * inactive panel out of the accessibility tree, which is exactly what
3612
+ * `role="tabpanel"` wants.
3613
+ */
3614
+ const [panel, setPanel] = (0, react.useState)("accounts");
3615
+ /**
3033
3616
  * Last-known usage per region, so tab dots survive tab switches.
3034
3617
  *
3035
3618
  * A region is ABSENT until something has been read for it (issue #77). It used
@@ -3377,6 +3960,16 @@ window.__ModuleLoader__.load({
3377
3960
  accounts: []
3378
3961
  };
3379
3962
  /**
3963
+ * Whether the ACTIVE panel has a usable sign-in.
3964
+ *
3965
+ * The section tabs render only here: a signed-out card is one screen of
3966
+ * instructions ("sign in once in the Trae app" plus the probed paths), and
3967
+ * splitting that into panels would hide the only thing it has to say behind a
3968
+ * tab the user cannot see the point of. The sibling WorkBuddy card made the
3969
+ * same call for the same reason.
3970
+ */
3971
+ const signedIn = status.status === "signed-in";
3972
+ /**
3380
3973
  * Clear in-flight claim state when the tab changes. `activeRegion` is a single
3381
3974
  * value across both tabs, so a switch must not leave one row's claim spinning
3382
3975
  * under the other tab.
@@ -3804,6 +4397,15 @@ window.__ModuleLoader__.load({
3804
4397
  const savedImageIds = new Set(slotImageIds ?? legacyImageIds);
3805
4398
  const draft = drafts[activeRegion];
3806
4399
  const visibleModels = draft?.models ?? (status.status === "signed-in" ? status.models : []);
4400
+ /**
4401
+ * One clock reading for the whole model list.
4402
+ *
4403
+ * Promotion badges are judged against it (see `promoBadge`): reading the clock
4404
+ * once per render keeps every row's verdict consistent with the others, and
4405
+ * because the card re-renders on its poll, a badge still appears and vanishes
4406
+ * with its window instead of sticking until the next manual refresh.
4407
+ */
4408
+ const nowForPromo = /* @__PURE__ */ new Date();
3807
4409
  const savedEnabledIds = status.status === "signed-in" ? new Set(status.enabledModelIds) : /* @__PURE__ */ new Set();
3808
4410
  const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
3809
4411
  const activeImageIds = draft?.imageIds ?? savedImageIds;
@@ -4059,23 +4661,63 @@ window.__ModuleLoader__.load({
4059
4661
  settled: claimed ? "claimed" : deviceSpent ? "device" : void 0
4060
4662
  };
4061
4663
  };
4062
- const checkinControl = (account, state) => {
4063
- const view = claimViewOf(account.id, state);
4064
- if (!view.claimable) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4065
- className: "dsm-trae-account-checkin-state",
4066
- children: view.settled === "claimed" ? t("row.poolCheckedIn") : t("row.poolDeviceUsed")
4067
- });
4068
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4069
- type: "button",
4070
- className: "dsm-trae-account-checkin",
4071
- disabled: view.disabled,
4072
- title: view.title,
4073
- onClick: () => {
4074
- claimFor(account.id);
4075
- },
4076
- children: view.label
4077
- });
4078
- };
4664
+ /**
4665
+ * The account roster, ALREADY JUDGED — one row per locally detected sign-in.
4666
+ *
4667
+ * Every verdict the table draws is decided here rather than in the section
4668
+ * component, because each one is a rule with a write or a Host answer behind
4669
+ * it: which account is serving (the document's own `accountId`, or the
4670
+ * roster's `selected` flag when signed out), what an account is worth
4671
+ * (`rowValue`, judged by CREDENTIAL region so the enterprise panel reads CN
4672
+ * balances), and whether a row may claim (`claimViewOf`, which knows the
4673
+ * device-wide check-in rule). A section that re-derived any of them could
4674
+ * disagree with the pool table below it, which reads the same three answers.
4675
+ */
4676
+ const accountRows = status.status === "error" ? [] : (status.accounts ?? []).map((account) => {
4677
+ const current = status.status === "signed-in" ? account.id === status.accountId : account.selected === true;
4678
+ const state = accountStatusByRegion[activeRegion][account.id];
4679
+ const error = claimErrorByRegion[activeRegion][account.id];
4680
+ return {
4681
+ id: account.id,
4682
+ accountName: account.accountName,
4683
+ current,
4684
+ selected: account.selected === true,
4685
+ meta: `${editionLabel(account.edition)} · ${sourceLabel(account.source)}`,
4686
+ value: rowValue(state),
4687
+ refreshExpiresAtMs: account.refreshExpiresAtMs,
4688
+ error: error === void 0 ? void 0 : t("row.checkinError", { message: error }),
4689
+ claim: isCnPool ? (() => {
4690
+ const view = claimViewOf(account.id, state);
4691
+ return {
4692
+ label: view.label,
4693
+ title: view.title,
4694
+ disabled: view.disabled,
4695
+ busy: view.busy,
4696
+ claimable: view.claimable,
4697
+ settled: view.settled
4698
+ };
4699
+ })() : void 0
4700
+ };
4701
+ });
4702
+ /**
4703
+ * The pool's per-row details, joined from the roster above by id.
4704
+ *
4705
+ * The pool document carries the accounts but not their edition, source or
4706
+ * refresh-token countdown, so those three are spliced in here — by the same
4707
+ * `editionLabel` / `sourceLabel` / `refreshTokenWarning` the roster uses, so
4708
+ * one account cannot read "国内版 · 桌面端" in one table and something else in
4709
+ * the next.
4710
+ */
4711
+ const poolDetails = Object.fromEntries((status.status === "signed-in" ? status.accounts : []).map((account) => {
4712
+ const warn = refreshTokenWarning(account.refreshExpiresAtMs, Date.now(), t, formatDateTime);
4713
+ return [account.id, {
4714
+ meta: `${editionLabel(account.edition)} · ${sourceLabel(account.source)}`,
4715
+ ...warn === void 0 ? {} : {
4716
+ warn: warn.text,
4717
+ warnTone: warn.tone
4718
+ }
4719
+ }];
4720
+ }));
4079
4721
  const label = loading ? t("row.accountsLoading") : status.status === "signed-in" ? t("row.signedIn", { accountName: status.accountName }) : status.status === "error" ? t("row.requestFailed") : t("row.signedOut");
4080
4722
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
4081
4723
  className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
@@ -4169,324 +4811,127 @@ window.__ModuleLoader__.load({
4169
4811
  role: "status",
4170
4812
  children: t("row.tabOffNotice")
4171
4813
  }),
4172
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4173
- className: "dsm-trae-prefs",
4174
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4175
- className: "dsm-trae-pref",
4176
- title: t(isCnPool ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi"),
4177
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4178
- type: "checkbox",
4179
- className: "dsm-trae-pref-check",
4180
- checked: settingsEdits.showPointsInMainUi,
4181
- disabled: settingsScope?.getSnapshot().writable !== true || savingSettings,
4182
- "aria-describedby": creditsHintId,
4183
- onChange: (event) => {
4184
- writePreference("showPointsInMainUi", event.currentTarget.checked);
4185
- }
4186
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4187
- className: "dsm-trae-pref-copy",
4188
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4189
- className: "dsm-trae-pref-label",
4190
- id: creditsLabelId,
4191
- children: t("row.showPointsInMainUi")
4192
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4193
- className: "dsm-trae-pref-hint",
4194
- id: creditsHintId,
4195
- children: t(isCnPool ? "row.showPointsInMainUiHint" : "row.showPointsInMainUiHintAi")
4196
- })]
4197
- })]
4198
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4199
- className: "dsm-trae-pref dsm-trae-credit-position-pref",
4200
- title: t("row.creditPositionHint"),
4201
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4202
- className: "dsm-trae-pref-copy",
4203
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4204
- className: "dsm-trae-pref-lead",
4205
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4206
- className: "dsm-trae-pref-label",
4207
- children: t("row.creditPositionLabel")
4208
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4209
- className: "dsm-trae-credit-position",
4210
- value: activeRegionOn ? creditPositionOn(activeRegion) : "composer",
4211
- disabled: !activeRegionOn || settingsScope?.getSnapshot().writable !== true || savingSettings,
4212
- "aria-label": t("row.creditPositionAria", { region: activeRegion === "cn" ? t("row.tabCn") : activeRegion === "enterprise" ? t("row.tabEnterprise") : t("row.tabAi") }),
4213
- onChange: (event) => {
4214
- changeCreditPosition(activeRegion, event.currentTarget.value === "stats" ? "stats" : "composer");
4215
- },
4216
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4217
- value: "composer",
4218
- children: t("row.creditPositionComposer")
4219
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4220
- value: "stats",
4221
- children: t("row.creditPositionStats")
4222
- })]
4223
- })]
4224
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4225
- className: "dsm-trae-pref-hint",
4226
- children: t("row.creditPositionHint")
4227
- })]
4228
- })
4229
- })]
4230
- }),
4231
- status.status === "signed-in" && status.pool !== void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4232
- className: "dsm-trae-accounts",
4233
- "aria-label": t("row.accountsTitle"),
4814
+ signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4815
+ className: "dsm-trae-panels",
4816
+ role: "tablist",
4817
+ "aria-label": t("row.panelAria"),
4234
4818
  children: [
4235
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4236
- className: "dsm-trae-accounts-head",
4237
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
4238
- className: "dsm-trae-accounts-title",
4239
- children: t("row.accountsTitle")
4240
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4241
- className: "dsm-trae-accounts-hint",
4242
- children: t("row.accountsHint")
4243
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4244
- type: "button",
4245
- className: "dsm-btn dsm-btn-outline",
4246
- disabled: busy,
4247
- onClick: () => {
4248
- rescanAccounts();
4249
- },
4250
- children: busy ? t("row.accountsScanning") : t("row.refreshTokens")
4251
- })]
4819
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4820
+ type: "button",
4821
+ role: "tab",
4822
+ id: "dsm-trae-tab-accounts",
4823
+ "aria-controls": "dsm-trae-panel-accounts",
4824
+ "aria-selected": panel === "accounts",
4825
+ className: `dsm-trae-panel-tab${panel === "accounts" ? " dsm-trae-panel-tab-active" : ""}`,
4826
+ onClick: () => {
4827
+ setPanel("accounts");
4828
+ },
4829
+ children: t("row.panelAccounts")
4252
4830
  }),
4253
- status.status !== "signed-in" || status.accounts.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4254
- className: "dsm-trae-usage-account",
4255
- role: "status",
4256
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4257
- className: "dsm-trae-usage-account-copy",
4258
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4259
- className: "dsm-trae-usage-status",
4260
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4261
- "aria-hidden": "true",
4262
- className: "dsm-trae-usage-dot",
4263
- style: dotStyle(status.status)
4264
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
4265
- }), status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4266
- className: "dsm-trae-usage-expiry",
4267
- children: t("row.tokenExpiry", { expiresAt: formatDateTime(status.tokenExpiresAtMs) })
4268
- }) : null]
4269
- })
4270
- }) : null,
4271
- status.status !== "error" && status.accounts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4272
- className: `dsm-trae-account-table${isCnPool ? " dsm-trae-account-table-checkin" : ""}`,
4273
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4274
- className: "dsm-trae-account-row dsm-trae-account-row-head",
4275
- role: "row",
4276
- children: [
4277
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnAccount") }),
4278
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnValue") }),
4279
- isCnPool ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.accountColumnCheckin") }) : null,
4280
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {})
4281
- ]
4282
- }), status.accounts.map((account) => {
4283
- const current = status.status === "signed-in" ? account.id === status.accountId : account.selected === true;
4284
- const state = accountStatusByRegion[activeRegion][account.id];
4285
- const error = claimErrorByRegion[activeRegion][account.id];
4286
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4287
- className: `dsm-trae-account-row${current ? " dsm-trae-account-row-current" : ""}`,
4288
- role: "row",
4289
- children: [
4290
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4291
- className: "dsm-trae-account-who",
4292
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4293
- "aria-hidden": "true",
4294
- className: "dsm-trae-usage-dot",
4295
- style: dotStyle(account.selected ? "signed-in" : "signed-out")
4296
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4297
- className: "dsm-trae-account-name",
4298
- children: [
4299
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: account.accountName }),
4300
- current ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4301
- className: "dsm-trae-account-badge",
4302
- children: t("row.accountCurrentBadge")
4303
- }) : null,
4304
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4305
- className: "dsm-trae-account-meta",
4306
- children: [
4307
- editionLabel(account.edition),
4308
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
4309
- "aria-hidden": "true",
4310
- children: "·"
4311
- }),
4312
- sourceLabel(account.source)
4313
- ]
4314
- }),
4315
- (() => {
4316
- const warning = refreshTokenWarning(account.refreshExpiresAtMs, Date.now(), t, formatDateTime);
4317
- return warning === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4318
- className: `dsm-trae-refresh-warn dsm-trae-refresh-warn-${warning.tone}`,
4319
- role: "status",
4320
- children: warning.text
4321
- });
4322
- })(),
4323
- error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4324
- className: "dsm-trae-account-note",
4325
- role: "alert",
4326
- children: t("row.checkinError", { message: error })
4327
- })
4328
- ]
4329
- })]
4330
- }),
4331
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4332
- className: "dsm-trae-account-worth",
4333
- children: rowValue(state)
4334
- }),
4335
- isCnPool ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4336
- className: "dsm-trae-account-checkin-cell",
4337
- children: checkinControl(account, state)
4338
- }) : null,
4339
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4340
- className: "dsm-trae-account-actions",
4341
- children: current ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4342
- type: "button",
4343
- className: "dsm-btn dsm-btn-outline dsm-trae-account-switch",
4344
- disabled: switchingAccount || settingsScope?.getSnapshot()?.writable !== true,
4345
- onClick: () => {
4346
- switchAccount(account.id);
4347
- },
4348
- children: switchingAccount ? t("row.accountSwitching") : t("row.accountSwitch")
4349
- })
4350
- })
4351
- ]
4352
- }, account.id);
4353
- })]
4354
- }) : null,
4355
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
4356
- t,
4357
- region: activeRegion === "ai" ? "ai" : "cn",
4358
- accounts: status.status === "signed-in" ? status.accounts.map((account) => ({
4359
- id: account.id,
4360
- accountName: account.accountName
4361
- })) : [],
4362
- onChanged: async () => {
4363
- const fresh = await refreshUsage(activeRegion);
4364
- if (fresh !== void 0) setStatusByRegion((current) => ({
4365
- ...current,
4366
- [activeRegion]: fresh
4367
- }));
4368
- }
4831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4832
+ type: "button",
4833
+ role: "tab",
4834
+ id: "dsm-trae-tab-models",
4835
+ "aria-controls": "dsm-trae-panel-models",
4836
+ "aria-selected": panel === "models",
4837
+ className: `dsm-trae-panel-tab${panel === "models" ? " dsm-trae-panel-tab-active" : ""}`,
4838
+ onClick: () => {
4839
+ setPanel("models");
4840
+ },
4841
+ children: t("row.panelModels")
4842
+ }),
4843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4844
+ type: "button",
4845
+ role: "tab",
4846
+ id: "dsm-trae-tab-diag",
4847
+ "aria-controls": "dsm-trae-panel-diag",
4848
+ "aria-selected": panel === "diag",
4849
+ className: `dsm-trae-panel-tab${panel === "diag" ? " dsm-trae-panel-tab-active" : ""}`,
4850
+ onClick: () => {
4851
+ setPanel("diag");
4852
+ },
4853
+ children: t("row.panelDiag")
4369
4854
  })
4370
4855
  ]
4371
- }),
4372
- status.status === "signed-in" && status.creditsUnavailable === "enterprise" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4373
- className: "dsm-trae-credits-unavailable",
4374
- role: "note",
4375
- children: t("row.enterpriseCreditsNotice")
4376
- }) : null,
4377
- status.status === "signed-in" && status.alternatives !== void 0 && status.alternatives.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4378
- className: "dsm-trae-usage-alternative",
4379
- role: "status",
4380
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4381
- className: "dsm-trae-usage-alternative-text",
4382
- children: [
4383
- t("row.creditsExhausted", { account: status.accountName }),
4384
- " ",
4385
- t("row.creditsAlternative", { accounts: status.alternatives.map((alt) => `${alt.accountName}(${t(`row.edition${alt.edition === "cn" ? "Cn" : alt.edition === "sg" ? "Sg" : alt.edition === "solo" ? "Solo" : "SoloSg"}`)}:${formatNumber(alt.workAvailable + alt.generalAvailable)})`).join("、") })
4386
- ]
4387
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4388
- type: "button",
4389
- className: "dsm-trae-usage-switch-button",
4390
- disabled: switchingAccount || settingsScope?.getSnapshot()?.writable !== true,
4391
- onClick: () => {
4392
- const target = status.alternatives?.[0];
4393
- if (target !== void 0) switchAccount(target.id);
4394
- },
4395
- children: t("row.creditsAlternativeSwitch")
4396
- })]
4397
4856
  }) : null,
4398
- status.status === "signed-in" && status.credits?.expiry !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExpiryNotice, {
4399
- expiry: status.credits.expiry,
4400
- t,
4401
- formatNumber,
4402
- formatDateTime
4403
- }) : null,
4404
- rescanNote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4405
- className: "dsm-trae-usage-note",
4406
- role: "status",
4407
- children: rescanNote
4408
- }),
4409
- status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4410
- status.pool !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountPool, {
4411
- panel: activeRegion,
4412
- pool: status.pool,
4413
- models: visibleModels,
4857
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4858
+ className: "dsm-trae-panel",
4859
+ role: "tabpanel",
4860
+ id: "dsm-trae-panel-accounts",
4861
+ "aria-labelledby": "dsm-trae-tab-accounts",
4862
+ hidden: signedIn && panel !== "accounts",
4863
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraeAccountPanel, {
4414
4864
  t,
4415
4865
  settingsScope,
4416
- topActions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4417
- type: "button",
4418
- className: "dsm-btn dsm-btn-outline",
4419
- disabled: busy,
4420
- onClick: () => {
4421
- rescanAccounts();
4422
- },
4423
- children: busy ? t("row.accountsScanning") : t("row.poolRescan")
4424
- }),
4425
- tools: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4426
- className: "dsm-trae-pool-tool-row",
4427
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4428
- className: "dsm-trae-pool-tool-label",
4429
- children: t("row.poolToolsLabel")
4430
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
4431
- t,
4432
- hideLabel: true,
4433
- region: activeRegion === "ai" ? "ai" : "cn",
4434
- accounts: status.accounts.map((account) => ({
4435
- id: account.id,
4436
- accountName: account.accountName
4437
- })),
4438
- onChanged: async () => {
4439
- const fresh = await refreshUsage(activeRegion);
4440
- if (fresh !== void 0) setStatusByRegion((current) => ({
4441
- ...current,
4442
- [activeRegion]: fresh
4443
- }));
4444
- }
4445
- })]
4446
- }),
4447
- details: Object.fromEntries(status.accounts.map((account) => {
4448
- const warn = refreshTokenWarning(account.refreshExpiresAtMs, Date.now(), t, formatDateTime);
4449
- return [account.id, {
4450
- meta: `${editionLabel(account.edition)} · ${sourceLabel(account.source)}`,
4451
- ...warn === void 0 ? {} : {
4452
- warn: warn.text,
4453
- warnTone: warn.tone
4454
- }
4455
- }];
4456
- })),
4457
- claims: status.pool.checkinSupported === true ? Object.fromEntries(status.accounts.map((account) => [account.id, claimViewOf(account.id, accountStatusByRegion[activeRegion][account.id])])) : void 0,
4866
+ panel: activeRegion,
4867
+ status,
4868
+ statusLabel: label,
4869
+ credentialRegionIsAi,
4870
+ regionOn: activeRegionOn,
4871
+ showPointsInMainUi: settingsEdits.showPointsInMainUi,
4872
+ onToggleShowPoints: (value) => {
4873
+ writePreference("showPointsInMainUi", value);
4874
+ },
4875
+ creditPosition: creditPositionOn(activeRegion),
4876
+ onCreditPositionChange: (position) => {
4877
+ changeCreditPosition(activeRegion, position);
4878
+ },
4879
+ savingPreferences: savingSettings,
4880
+ creditsLabelId,
4881
+ creditsHintId,
4882
+ rows: accountRows,
4883
+ busy,
4884
+ rescanNote,
4885
+ onRescan: () => {
4886
+ rescanAccounts();
4887
+ },
4888
+ switchingAccount,
4889
+ onSwitchAccount: (accountId) => {
4890
+ switchAccount(accountId);
4891
+ },
4458
4892
  onClaim: (accountId) => {
4459
4893
  claimFor(accountId);
4460
4894
  },
4461
- deviceClaimed: deviceClaimedToday,
4462
- onBind: (accountId) => {
4463
- switchAccount(accountId);
4895
+ onTransferChanged: async () => {
4896
+ const fresh = await refreshUsage(activeRegion);
4897
+ if (fresh !== void 0) setStatusByRegion((current) => ({
4898
+ ...current,
4899
+ [activeRegion]: fresh
4900
+ }));
4464
4901
  },
4465
- bindingBusy: switchingAccount,
4466
- onChanged: async () => {
4902
+ poolDetails,
4903
+ poolClaims: status.status === "signed-in" && status.pool?.checkinSupported === true ? Object.fromEntries(status.accounts.map((account) => [account.id, claimViewOf(account.id, accountStatusByRegion[activeRegion][account.id])])) : void 0,
4904
+ models: visibleModels,
4905
+ deviceClaimed: deviceClaimedToday,
4906
+ onPoolChanged: async () => {
4467
4907
  const fresh = await refreshUsage(activeRegion);
4468
4908
  if (fresh !== void 0) setStatusByRegion((current) => ({
4469
4909
  ...current,
4470
4910
  [activeRegion]: fresh
4471
4911
  }));
4472
4912
  },
4473
- onRefresh: async () => {
4913
+ onPoolRefresh: async () => {
4474
4914
  const fresh = await refreshUsage(activeRegion);
4475
4915
  if (fresh !== void 0) setStatusByRegion((current) => ({
4476
4916
  ...current,
4477
4917
  [activeRegion]: fresh
4478
4918
  }));
4919
+ },
4920
+ creditsUnavailable: status.status === "signed-in" ? status.creditsUnavailable : void 0,
4921
+ creditsExpiry: status.status === "signed-in" ? status.credits?.expiry : void 0,
4922
+ alternatives: status.status === "signed-in" ? status.alternatives : void 0,
4923
+ onSwitchToAlternative: (accountId) => {
4924
+ switchAccount(accountId);
4479
4925
  }
4480
- }) : null,
4481
- status.checkin !== void 0 && !status.checkin.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4482
- className: "dsm-trae-usage-error",
4483
- children: t("row.checkinDisabled")
4484
- }) : null,
4485
- status.checkinError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4486
- className: "dsm-trae-usage-error",
4487
- children: t("row.checkinError", { message: status.checkinError })
4488
- }),
4489
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4926
+ })
4927
+ }),
4928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4929
+ className: "dsm-trae-panel",
4930
+ role: "tabpanel",
4931
+ id: "dsm-trae-panel-models",
4932
+ "aria-labelledby": "dsm-trae-tab-models",
4933
+ hidden: !signedIn || panel !== "models",
4934
+ children: signedIn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4490
4935
  className: "dsm-trae-models",
4491
4936
  "aria-label": t("row.modelsTitle"),
4492
4937
  children: [
@@ -4567,9 +5012,9 @@ window.__ModuleLoader__.load({
4567
5012
  onChange: () => {
4568
5013
  toggleModel(model.id);
4569
5014
  }
4570
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5015
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4571
5016
  className: "dsm-trae-model-copy",
4572
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5017
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4573
5018
  className: "dsm-trae-model-name",
4574
5019
  children: [
4575
5020
  model.name,
@@ -4579,7 +5024,7 @@ window.__ModuleLoader__.load({
4579
5024
  }),
4580
5025
  probeBadge(probeByRegion[activeRegion][model.id], t)
4581
5026
  ]
4582
- })
5027
+ }), promoBadge(model, t, nowForPromo)]
4583
5028
  })]
4584
5029
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4585
5030
  className: "dsm-trae-model-options",
@@ -4680,8 +5125,143 @@ window.__ModuleLoader__.load({
4680
5125
  })]
4681
5126
  })
4682
5127
  ]
4683
- })
4684
- ] }) : null,
5128
+ }) }) : null
5129
+ }),
5130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5131
+ className: "dsm-trae-panel",
5132
+ role: "tabpanel",
5133
+ id: "dsm-trae-panel-diag",
5134
+ "aria-labelledby": "dsm-trae-tab-diag",
5135
+ hidden: !signedIn || panel !== "diag",
5136
+ children: status.status !== "signed-in" || panel !== "diag" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5137
+ className: "dsm-trae-diag",
5138
+ children: [
5139
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5140
+ className: "dsm-trae-diag-row",
5141
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5142
+ className: "dsm-trae-diag-label",
5143
+ children: t("row.diagRegion")
5144
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5145
+ className: "dsm-trae-diag-value",
5146
+ children: activeRegion === "cn" ? t("row.tabCn") : activeRegion === "enterprise" ? t("row.tabEnterprise") : t("row.tabAi")
5147
+ })]
5148
+ }),
5149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5150
+ className: "dsm-trae-diag-row",
5151
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5152
+ className: "dsm-trae-diag-label",
5153
+ children: t("row.diagProvider")
5154
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5155
+ className: "dsm-trae-diag-value dsm-trae-diag-mono",
5156
+ children: providerOfPanel(activeRegion)
5157
+ })]
5158
+ }),
5159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5160
+ className: "dsm-trae-diag-row",
5161
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5162
+ className: "dsm-trae-diag-label",
5163
+ children: t("row.diagAccount")
5164
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5165
+ className: "dsm-trae-diag-value",
5166
+ children: status.accountName
5167
+ })]
5168
+ }),
5169
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5170
+ className: "dsm-trae-diag-row",
5171
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5172
+ className: "dsm-trae-diag-label",
5173
+ children: t("row.diagToken")
5174
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5175
+ className: "dsm-trae-diag-value dsm-trae-diag-mono",
5176
+ children: formatDateTime(status.tokenExpiresAtMs)
5177
+ })]
5178
+ }),
5179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5180
+ className: "dsm-trae-diag-row",
5181
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5182
+ className: "dsm-trae-diag-label",
5183
+ children: t("row.diagRefresh")
5184
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5185
+ className: "dsm-trae-diag-value dsm-trae-diag-mono",
5186
+ children: status.refreshExpiresAtMs === void 0 ? t("row.diagRefreshNone") : formatDateTime(status.refreshExpiresAtMs)
5187
+ })]
5188
+ }),
5189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5190
+ className: "dsm-trae-diag-row",
5191
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5192
+ className: "dsm-trae-diag-label",
5193
+ children: t("row.diagCatalog")
5194
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5195
+ className: "dsm-trae-diag-value",
5196
+ children: formatNumber(status.models.length)
5197
+ })]
5198
+ }),
5199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5200
+ className: "dsm-trae-diag-row",
5201
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5202
+ className: "dsm-trae-diag-label",
5203
+ children: t("row.diagEnabled")
5204
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5205
+ className: "dsm-trae-diag-value",
5206
+ children: formatNumber(status.enabledModelIds.length)
5207
+ })]
5208
+ }),
5209
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5210
+ className: "dsm-trae-diag-row",
5211
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5212
+ className: "dsm-trae-diag-label",
5213
+ children: t("row.diagPool")
5214
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5215
+ className: "dsm-trae-diag-value",
5216
+ children: status.pool === void 0 ? t("row.diagPoolOff") : status.pool.enabled ? t("row.diagPoolOn", { count: status.pool.effectiveMemberAccountIds?.length ?? status.pool.accounts.length }) : t("row.diagPoolOff")
5217
+ })]
5218
+ }),
5219
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5220
+ className: "dsm-trae-diag-row",
5221
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5222
+ className: "dsm-trae-diag-label",
5223
+ children: t("row.diagCheckin")
5224
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5225
+ className: "dsm-trae-diag-value",
5226
+ children: status.checkin !== void 0 || status.pool?.checkinSupported === true ? t("row.diagCheckinOn") : t("row.diagCheckinOff")
5227
+ })]
5228
+ }),
5229
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5230
+ className: "dsm-trae-diag-row",
5231
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5232
+ className: "dsm-trae-diag-label",
5233
+ children: t("row.diagRaw")
5234
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5235
+ className: "dsm-trae-diag-value dsm-trae-diag-mono",
5236
+ children: status.rawChat === void 0 ? t("row.diagRawOff") : status.rawChat.state
5237
+ })]
5238
+ }),
5239
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5240
+ className: "dsm-trae-diag-row",
5241
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5242
+ className: "dsm-trae-diag-label",
5243
+ children: t("row.diagFailures")
5244
+ }), (() => {
5245
+ const failures = [];
5246
+ if (status.creditsError !== void 0) failures.push(status.creditsError);
5247
+ if (status.payStatusError !== void 0) failures.push(status.payStatusError);
5248
+ if (status.checkinError !== void 0) failures.push(status.checkinError);
5249
+ if (writeError !== void 0) failures.push(writeError);
5250
+ return failures.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5251
+ className: "dsm-trae-diag-value",
5252
+ children: t("row.diagNoFailures")
5253
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5254
+ className: "dsm-trae-diag-value dsm-trae-diag-bad dsm-trae-diag-mono",
5255
+ children: failures.join(" · ")
5256
+ });
5257
+ })()]
5258
+ })
5259
+ ]
5260
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5261
+ className: "dsm-trae-diag-hint",
5262
+ children: t("row.diagHint")
5263
+ })] })
5264
+ }),
4685
5265
  status.status === "signed-out" && loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4686
5266
  className: "dsm-trae-usage-text",
4687
5267
  role: "status",
@@ -5431,6 +6011,29 @@ window.__ModuleLoader__.load({
5431
6011
  "row.tabSwitchHint": "Turns this provider off: its models leave the DSH picker. Account and settings are kept.",
5432
6012
  "row.tabSwitchAria": "Offer the {region} provider to DSH",
5433
6013
  "row.tabOffNotice": "This provider is off: its models are not offered to DSH. Account and settings are kept.",
6014
+ "row.panelAria": "Card sections",
6015
+ "row.panelAccounts": "Account",
6016
+ "row.panelModels": "Models",
6017
+ "row.panelDiag": "Diagnostics",
6018
+ "row.diagRegion": "Panel",
6019
+ "row.diagProvider": "Provider id",
6020
+ "row.diagAccount": "Serving account",
6021
+ "row.diagToken": "Access token expires",
6022
+ "row.diagRefresh": "Re-sign-in deadline",
6023
+ "row.diagRefreshNone": "this credential carries none",
6024
+ "row.diagCatalog": "Models in catalog",
6025
+ "row.diagEnabled": "Models enabled",
6026
+ "row.diagPool": "Account pool",
6027
+ "row.diagPoolOn": "on · {count} account(s)",
6028
+ "row.diagPoolOff": "off",
6029
+ "row.diagCheckin": "Daily check-in",
6030
+ "row.diagCheckinOn": "supported",
6031
+ "row.diagCheckinOff": "not offered here",
6032
+ "row.diagRaw": "Raw chat channel",
6033
+ "row.diagRawOff": "not checked",
6034
+ "row.diagFailures": "Last reported failure",
6035
+ "row.diagNoFailures": "none",
6036
+ "row.diagHint": "Everything above is read-only and can be quoted verbatim into a bug report. The values are this panel’s own last refresh, not a summary of it.",
5434
6037
  "row.settingsWriteFailed": "The setting was not saved: {message}",
5435
6038
  "row.signedOut": "Not signed in",
5436
6039
  "row.signedOutHint": "Sign in once in the Trae desktop app; this plugin follows that sign-in automatically.",
@@ -5574,6 +6177,12 @@ window.__ModuleLoader__.load({
5574
6177
  "row.probeRateLimited": "throttled",
5575
6178
  "row.probeAuth": "sign-in rejected",
5576
6179
  "row.probeUnknown": "not determined",
6180
+ "row.promoLimited": "Limited {percent}% off",
6181
+ "row.promoSubsidy": "{percent}% off",
6182
+ "row.promoOffPeak": "Off-peak {percent}% off",
6183
+ "row.promoHintRate": "was {before} credits/Mtoken, now {rate}",
6184
+ "row.promoHintWindow": "off-peak hours {from}–{until} ({zone})",
6185
+ "row.promoHintMember": "subscription price is a further {percent}% off",
5577
6186
  "row.modelEnabled": "Enabled",
5578
6187
  "row.discard": "Discard changes",
5579
6188
  "row.save": "Save",
@@ -5751,6 +6360,29 @@ window.__ModuleLoader__.load({
5751
6360
  "row.tabSwitchHint": "关闭该供应商:其模型不再提供给 DSH;账号与设置保留,可随时恢复。",
5752
6361
  "row.tabSwitchAria": "是否向 DSH 提供{region}供应商",
5753
6362
  "row.tabOffNotice": "该供应商已关闭,模型不会提供给 DSH。账号与设置均已保留。",
6363
+ "row.panelAria": "卡片分栏",
6364
+ "row.panelAccounts": "账号",
6365
+ "row.panelModels": "模型",
6366
+ "row.panelDiag": "诊断",
6367
+ "row.diagRegion": "面板",
6368
+ "row.diagProvider": "供应商 id",
6369
+ "row.diagAccount": "服务账号",
6370
+ "row.diagToken": "访问令牌到期",
6371
+ "row.diagRefresh": "重新登录最后期限",
6372
+ "row.diagRefreshNone": "该凭据不携带",
6373
+ "row.diagCatalog": "目录模型数",
6374
+ "row.diagEnabled": "已启用模型数",
6375
+ "row.diagPool": "账号池",
6376
+ "row.diagPoolOn": "已开启 · {count} 个账号",
6377
+ "row.diagPoolOff": "未开启",
6378
+ "row.diagCheckin": "每日签到",
6379
+ "row.diagCheckinOn": "支持",
6380
+ "row.diagCheckinOff": "本面板不提供",
6381
+ "row.diagRaw": "Raw 会话通道",
6382
+ "row.diagRawOff": "未检测",
6383
+ "row.diagFailures": "最近一次报错",
6384
+ "row.diagNoFailures": "无",
6385
+ "row.diagHint": "以上全部只读,可整段原样贴进反馈。数值来自本面板最近一次刷新,不是概括。",
5754
6386
  "row.settingsWriteFailed": "设置未能保存:{message}",
5755
6387
  "row.signedOut": "未登录",
5756
6388
  "row.signedOutHint": "在 Trae 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
@@ -5894,6 +6526,12 @@ window.__ModuleLoader__.load({
5894
6526
  "row.probeRateLimited": "被限流",
5895
6527
  "row.probeAuth": "登录凭据被拒",
5896
6528
  "row.probeUnknown": "本次未测出",
6529
+ "row.promoLimited": "限时 {zhe} 折",
6530
+ "row.promoSubsidy": "{zhe} 折",
6531
+ "row.promoOffPeak": "闲时 {zhe} 折",
6532
+ "row.promoHintRate": "原价 {before} 积分/百万 token,现价 {rate}",
6533
+ "row.promoHintWindow": "闲时时段 {from}–{until}({zone})",
6534
+ "row.promoHintMember": "会员价另有 {zhe} 折",
5897
6535
  "row.modelEnabled": "启用模型",
5898
6536
  "row.discard": "放弃修改",
5899
6537
  "row.save": "保存",