dsh-connect-trae 2.10.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +461 -0
- package/README.en.md +4 -1
- package/README.md +4 -1
- package/lib/client.js +719 -287
- package/lib/index.d.ts +12 -20
- package/lib/index.js +75 -8
- package/package.json +2 -1
package/lib/client.js
CHANGED
|
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react_dom = require("react-dom");
|
|
9
10
|
//#region src/status-paths.ts
|
|
10
11
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
11
12
|
const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
|
|
@@ -21,6 +22,17 @@ window.__ModuleLoader__.load({
|
|
|
21
22
|
/** Plugin-owned local account rescan endpoint. */
|
|
22
23
|
const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
|
|
23
24
|
/**
|
|
25
|
+
* General balance of EVERY account in a region, for the composer panel's table.
|
|
26
|
+
*
|
|
27
|
+
* Separate from the usage route on purpose: the usage document backs the
|
|
28
|
+
* 5-minute readout, and multiplying its upstream reads by the account count
|
|
29
|
+
* would charge a healthy account for something it never looks at — the same
|
|
30
|
+
* principle that keeps `creditAlternatives` firing only when a balance hits
|
|
31
|
+
* zero. This one is paid when the panel is actually opened, which is the only
|
|
32
|
+
* place the table is shown.
|
|
33
|
+
*/
|
|
34
|
+
const TRAE_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-trae/account-credits";
|
|
35
|
+
/**
|
|
24
36
|
* Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
|
|
25
37
|
* route in this plugin that changes upstream account state.
|
|
26
38
|
*/
|
|
@@ -73,6 +85,24 @@ window.__ModuleLoader__.load({
|
|
|
73
85
|
return out;
|
|
74
86
|
}
|
|
75
87
|
/**
|
|
88
|
+
* Read the per-region account selections out of a settings value.
|
|
89
|
+
*
|
|
90
|
+
* SHARED because `accounts` now has TWO writers: the card, and the composer
|
|
91
|
+
* panel's account table (2.14.0). Two writers of one field must use one merge
|
|
92
|
+
* rule, or they drift — and the rule is not obvious.
|
|
93
|
+
*
|
|
94
|
+
* Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
|
|
95
|
+
* live reference, which passes the `typeof === 'object'` check and would be
|
|
96
|
+
* returned as if it were the map. Every lookup on it is then `undefined`, and
|
|
97
|
+
* the merge (`{ ...configured, [region]: id }`) would spread a reference into
|
|
98
|
+
* `{get: <function>}` — dropping the other region's selection and leaking a
|
|
99
|
+
* function into the settings document.
|
|
100
|
+
*/
|
|
101
|
+
function configuredAccountsOf(configured) {
|
|
102
|
+
const accounts = unwrapVolatileDeep(configured)?.accounts;
|
|
103
|
+
return typeof accounts === "object" && accounts !== null ? accounts : {};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
76
106
|
* Build the next `regions` settings value for the card's save. The write
|
|
77
107
|
* targets ONLY the signed-in account's region slot; every other region's slot
|
|
78
108
|
* is carried over untouched, so switching accounts never clobbers the other
|
|
@@ -203,6 +233,10 @@ window.__ModuleLoader__.load({
|
|
|
203
233
|
.dsm-btn-outline:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed);background:rgba(255,255,255,.04)}
|
|
204
234
|
.dsm-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
205
235
|
.dsm-btn-primary:hover:not(:disabled){opacity:.9}
|
|
236
|
+
.dsm-trae-mainui-switch{display:flex;justify-content:flex-end;padding:0 2px 8px}
|
|
237
|
+
.dsm-trae-mainui-switch-label{display:inline-flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0);white-space:nowrap;cursor:pointer}
|
|
238
|
+
.dsm-trae-mainui-switch-label input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
239
|
+
.dsm-trae-mainui-switch-label:has(input:disabled){opacity:.6;cursor:default}
|
|
206
240
|
.dsm-trae-usage{display:flex;flex-direction:column;gap:16px;margin:0;padding:16px 0 4px}
|
|
207
241
|
.dsm-trae-tabs{display:flex;gap:6px;padding:4px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;background:var(--dsw-alias-bg-layer-3,#2a2c33)}
|
|
208
242
|
.dsm-trae-tab-cell{display:flex;align-items:center;gap:2px;flex:1;min-width:0}
|
|
@@ -267,10 +301,6 @@ window.__ModuleLoader__.load({
|
|
|
267
301
|
row at the top and the copy beside the model actions (2.9.3). Without it the
|
|
268
302
|
checkbox rendered unstyled and read as absent — the whole reason the switch
|
|
269
303
|
looked missing. */
|
|
270
|
-
.dsm-trae-mainui-switch{padding:2px 0 0}
|
|
271
|
-
.dsm-trae-mainui-switch-label{display:flex;align-items:center;gap:9px;font-size:13px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0);cursor:pointer;white-space:nowrap}
|
|
272
|
-
.dsm-trae-mainui-switch-label input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
273
|
-
.dsm-trae-mainui-switch-label:has(input:disabled){opacity:.6;cursor:default}
|
|
274
304
|
.dsm-trae-models-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
275
305
|
}
|
|
276
306
|
.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)}
|
|
@@ -320,22 +350,127 @@ window.__ModuleLoader__.load({
|
|
|
320
350
|
.dsm-trae-checkin-note{margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
321
351
|
`;
|
|
322
352
|
/**
|
|
323
|
-
*
|
|
353
|
+
* Composer-row readout styles, injected by the readout itself.
|
|
354
|
+
*
|
|
355
|
+
* SEPARATE from `TRAE_CARD_CSS`, and that is not tidiness: the card's CSS is
|
|
356
|
+
* only injected when the settings card module loads, so putting the composer's
|
|
357
|
+
* rules in it means the readout renders UNSTYLED in the composer toolbar —
|
|
358
|
+
* which is exactly what shipped in 2.13.0–2.13.4. The trigger fell back to the
|
|
359
|
+
* browser's default button box, and the panel, without `position: fixed`, had
|
|
360
|
+
* its inline left/top ignored and rendered nowhere useful.
|
|
324
361
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
362
|
+
* `tests/composer-styles.spec.tsx` now asserts that every class this component
|
|
363
|
+
* renders has a rule here, so a rule cannot go missing without a test failing.
|
|
327
364
|
*/
|
|
328
|
-
const
|
|
329
|
-
.
|
|
330
|
-
.
|
|
331
|
-
.dsm-trae-
|
|
332
|
-
.dsm-trae-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.
|
|
336
|
-
|
|
337
|
-
.
|
|
338
|
-
|
|
365
|
+
const COMPOSER_POINTS_CSS = `
|
|
366
|
+
/* The readout is plain composer text — no border, no fill at rest. The padding
|
|
367
|
+
is only the click target, invisible until the pointer arrives. */
|
|
368
|
+
.dsm-trae-composer-points{display:inline-flex;align-items:center;min-width:0}
|
|
369
|
+
.dsm-trae-composer-points-trigger{
|
|
370
|
+
appearance:none;display:inline-flex;align-items:center;align-self:stretch;
|
|
371
|
+
padding:2px 6px;border:0;border-radius:6px;background:transparent;
|
|
372
|
+
/* EXPLICIT, not inherit. The composer row's own font and body colour are
|
|
373
|
+
noticeably larger and darker than the neighbouring controls — a credit
|
|
374
|
+
readout is secondary information and must not shout. 12px matches this
|
|
375
|
+
plugin's original line; label-tertiary matches the shell's own composer-row
|
|
376
|
+
control (ContextMeter), which is the sibling this reads alongside. */
|
|
377
|
+
font-size:12px;line-height:18px;
|
|
378
|
+
color:var(--dsw-alias-label-tertiary,#999);
|
|
379
|
+
white-space:nowrap;cursor:pointer;
|
|
380
|
+
font-variant-numeric:tabular-nums;
|
|
381
|
+
}
|
|
382
|
+
.dsm-trae-composer-points-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14))}
|
|
383
|
+
.dsm-trae-composer-points-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
384
|
+
/* The panel is portaled to document.body and positioned from the trigger, so it
|
|
385
|
+
MUST be fixed — otherwise the left/top it measures are ignored and it lands in
|
|
386
|
+
document flow. All material comes from shell tokens so it follows the theme. */
|
|
387
|
+
.dsm-trae-composer-panel{
|
|
388
|
+
position:fixed;z-index:1100;box-sizing:border-box;
|
|
389
|
+
width:min(248px,calc(100vw - 24px));padding:12px;
|
|
390
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.28));
|
|
391
|
+
border-radius:var(--dsw-radius-lg,14px);
|
|
392
|
+
background:var(--dsw-specific-menu,Canvas);
|
|
393
|
+
box-shadow:var(--dsw-elevation-prominent,0 8px 32px rgba(0,0,0,.22));
|
|
394
|
+
backdrop-filter:var(--dsw-menu-backdrop-filter,none);
|
|
395
|
+
font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#444);
|
|
396
|
+
}
|
|
397
|
+
.dsm-trae-composer-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
|
|
398
|
+
.dsm-trae-composer-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,CanvasText)}
|
|
399
|
+
/* Self-contained: the dsm-btn primitive lives in TRAE_CARD_CSS, which the
|
|
400
|
+
composer never loads, so the refresh button must style itself. */
|
|
401
|
+
.dsm-trae-composer-panel-refresh{
|
|
402
|
+
appearance:none;flex:none;margin-left:auto;padding:3px 10px;
|
|
403
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.34));
|
|
404
|
+
border-radius:7px;background:transparent;
|
|
405
|
+
color:var(--dsw-alias-label-secondary,#444);
|
|
406
|
+
font:inherit;font-size:11px;line-height:16px;cursor:pointer;
|
|
407
|
+
}
|
|
408
|
+
.dsm-trae-composer-panel-refresh:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,rgba(127,127,127,.6));color:var(--dsw-alias-label-primary,CanvasText)}
|
|
409
|
+
.dsm-trae-composer-panel-refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
410
|
+
.dsm-trae-composer-panel-refresh:disabled{opacity:.5;cursor:default}
|
|
411
|
+
/* The panel is a TABLE: one row per account in the region, one figure (the
|
|
412
|
+
general balance) per row, and a row click switches accounts.
|
|
413
|
+
|
|
414
|
+
Rhythm is deliberate: the header sits on a hairline, the switch button owns
|
|
415
|
+
ALL horizontal padding (and pulls back by the same amount so its label lines
|
|
416
|
+
up with the header), and every row is the same height. A misaligned first
|
|
417
|
+
column is what makes a two-column popover read as broken. */
|
|
418
|
+
.dsm-trae-composer-panel-table{
|
|
419
|
+
width:100%;border-collapse:collapse;
|
|
420
|
+
font-size:12px;line-height:18px;
|
|
421
|
+
}
|
|
422
|
+
.dsm-trae-composer-panel-table thead th{
|
|
423
|
+
padding:0 0 7px;font-weight:500;text-align:left;font-size:11px;
|
|
424
|
+
color:var(--dsw-alias-label-tertiary,GrayText);
|
|
425
|
+
border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));
|
|
426
|
+
}
|
|
427
|
+
.dsm-trae-composer-panel-num{
|
|
428
|
+
text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;
|
|
429
|
+
}
|
|
430
|
+
.dsm-trae-composer-panel-table thead th.dsm-trae-composer-panel-num{padding-right:0}
|
|
431
|
+
.dsm-trae-composer-panel-table tbody th{
|
|
432
|
+
padding:0;font-weight:400;text-align:left;vertical-align:middle;
|
|
433
|
+
}
|
|
434
|
+
.dsm-trae-composer-panel-table tbody td{
|
|
435
|
+
padding:4px 0;vertical-align:middle;
|
|
436
|
+
color:var(--dsw-alias-label-primary,CanvasText);
|
|
437
|
+
}
|
|
438
|
+
/* The current account reads as current without shouting: a filled dot and a
|
|
439
|
+
slightly heavier weight. Disabled — pressing it would write a selection that
|
|
440
|
+
has not changed. */
|
|
441
|
+
.dsm-trae-composer-panel-table tbody tr.dsm-trae-composer-panel-row-current td{font-weight:600}
|
|
442
|
+
.dsm-trae-composer-panel-switch{
|
|
443
|
+
appearance:none;display:flex;align-items:center;gap:7px;
|
|
444
|
+
width:100%;padding:4px 6px;margin-left:-6px;
|
|
445
|
+
border:0;border-radius:6px;background:transparent;
|
|
446
|
+
color:inherit;font:inherit;font-size:12px;text-align:left;cursor:pointer;
|
|
447
|
+
white-space:nowrap;overflow:hidden;
|
|
448
|
+
}
|
|
449
|
+
.dsm-trae-composer-panel-switch>span:last-child{overflow:hidden;text-overflow:ellipsis}
|
|
450
|
+
/* Hover belongs to the switch, not the row: the number cell is not clickable,
|
|
451
|
+
so lighting it up would promise an action it cannot perform. */
|
|
452
|
+
.dsm-trae-composer-panel-switch:hover:not(:disabled){
|
|
453
|
+
background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));
|
|
454
|
+
}
|
|
455
|
+
.dsm-trae-composer-panel-switch:focus-visible{
|
|
456
|
+
outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:-1px;
|
|
457
|
+
}
|
|
458
|
+
.dsm-trae-composer-panel-switch:disabled{cursor:default}
|
|
459
|
+
.dsm-trae-composer-panel-dot{
|
|
460
|
+
flex:none;width:7px;height:7px;border-radius:50%;
|
|
461
|
+
background:var(--dsw-alias-label-dimmed,rgba(127,127,127,.5));
|
|
462
|
+
}
|
|
463
|
+
.dsm-trae-composer-panel-table tr.dsm-trae-composer-panel-row-current .dsm-trae-composer-panel-dot{
|
|
464
|
+
background:var(--dsw-state-success,#22a06b);
|
|
465
|
+
}
|
|
466
|
+
.dsm-trae-composer-panel-foot{
|
|
467
|
+
display:flex;justify-content:space-between;gap:8px;
|
|
468
|
+
margin-top:9px;padding-top:7px;
|
|
469
|
+
border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));
|
|
470
|
+
font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,GrayText);
|
|
471
|
+
}
|
|
472
|
+
.dsm-trae-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
|
|
473
|
+
.dsm-trae-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
|
|
339
474
|
`;
|
|
340
475
|
//#endregion
|
|
341
476
|
//#region src/client/TraeUsageCard.tsx
|
|
@@ -348,20 +483,6 @@ window.__ModuleLoader__.load({
|
|
|
348
483
|
const POLL_INTERVAL_MS = 6e4;
|
|
349
484
|
const TRAE_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-trae";
|
|
350
485
|
/**
|
|
351
|
-
* Read the per-region account selections out of the settings snapshot.
|
|
352
|
-
*
|
|
353
|
-
* Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
|
|
354
|
-
* live reference, which passes the `typeof === 'object'` check below and would
|
|
355
|
-
* be returned as if it were the map. Every lookup on it is then `undefined`,
|
|
356
|
-
* and the card's merge (`{ ...configured, [region]: id }`) would spread a
|
|
357
|
-
* reference into `{get: <function>}` — dropping the other region's selection
|
|
358
|
-
* and leaking a function into the settings document.
|
|
359
|
-
*/
|
|
360
|
-
function configuredAccountsOf(configured) {
|
|
361
|
-
const accounts = unwrapVolatileDeep(configured)?.accounts;
|
|
362
|
-
return typeof accounts === "object" && accounts !== null ? accounts : {};
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
486
|
* Write one settings field and confirm the value actually landed.
|
|
366
487
|
*
|
|
367
488
|
* `configForms.set()` resolving is NOT proof that anything was stored: a write
|
|
@@ -401,7 +522,7 @@ window.__ModuleLoader__.load({
|
|
|
401
522
|
document.head.appendChild(styleTag);
|
|
402
523
|
}
|
|
403
524
|
}
|
|
404
|
-
function formatNumber
|
|
525
|
+
function formatNumber(value) {
|
|
405
526
|
return new Intl.NumberFormat(void 0, {
|
|
406
527
|
minimumFractionDigits: 0,
|
|
407
528
|
maximumFractionDigits: 0
|
|
@@ -420,7 +541,7 @@ window.__ModuleLoader__.load({
|
|
|
420
541
|
if (value === void 0) return unknown;
|
|
421
542
|
if (value >= 1e6 && value % 1e6 === 0) return `${value / 1e6}M`;
|
|
422
543
|
if (value >= 1e3 && value % 1e3 === 0) return `${value / 1e3}K`;
|
|
423
|
-
return formatNumber
|
|
544
|
+
return formatNumber(value);
|
|
424
545
|
}
|
|
425
546
|
/**
|
|
426
547
|
* The subscription row's verdict.
|
|
@@ -512,24 +633,6 @@ window.__ModuleLoader__.load({
|
|
|
512
633
|
*/
|
|
513
634
|
const [claiming, setClaiming] = (0, react.useState)(false);
|
|
514
635
|
/** Last claim refusal, shown until the next successful refresh. */
|
|
515
|
-
/**
|
|
516
|
-
* Toggle the global "show credits at the DSH sidebar foot" switch. Unlike the
|
|
517
|
-
* per-region provider switch this is one top-level boolean shared by both
|
|
518
|
-
* regions; only that field is written, and the read-back must show the new
|
|
519
|
-
* value before success is reported.
|
|
520
|
-
*/
|
|
521
|
-
const toggleShowPointsInMainUi = async (enabled) => {
|
|
522
|
-
if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
|
|
523
|
-
setTogglingMainUi(true);
|
|
524
|
-
setWriteError(void 0);
|
|
525
|
-
try {
|
|
526
|
-
await writeSettingsField(settingsScope, "showPointsInMainUi", enabled, (readBack) => unwrapVolatileDeep(readBack)?.showPointsInMainUi === enabled);
|
|
527
|
-
} catch (error) {
|
|
528
|
-
if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
529
|
-
} finally {
|
|
530
|
-
if (mounted.current) setTogglingMainUi(false);
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
636
|
const [claimError, setClaimError] = (0, react.useState)(void 0);
|
|
534
637
|
/**
|
|
535
638
|
* Outcome of the last "refresh accounts" press (issue #25).
|
|
@@ -540,7 +643,6 @@ window.__ModuleLoader__.load({
|
|
|
540
643
|
* failure. Reporting the outcome is the difference between "it did nothing"
|
|
541
644
|
* and "it found the same 2 accounts, still on X".
|
|
542
645
|
*/
|
|
543
|
-
const [togglingMainUi, setTogglingMainUi] = (0, react.useState)(false);
|
|
544
646
|
const [rescanNote, setRescanNote] = (0, react.useState)(void 0);
|
|
545
647
|
/**
|
|
546
648
|
* Whether this MACHINE has spent today's check-in while the selected account
|
|
@@ -556,6 +658,8 @@ window.__ModuleLoader__.load({
|
|
|
556
658
|
* the control would flip and then quietly snap back on the next snapshot.
|
|
557
659
|
*/
|
|
558
660
|
const [writeError, setWriteError] = (0, react.useState)(void 0);
|
|
661
|
+
/** True while the composer-credit switch write is in flight. */
|
|
662
|
+
const [togglingCredits, setTogglingCredits] = (0, react.useState)(false);
|
|
559
663
|
const mounted = (0, react.useRef)(true);
|
|
560
664
|
(0, react.useEffect)(() => {
|
|
561
665
|
mounted.current = true;
|
|
@@ -563,6 +667,24 @@ window.__ModuleLoader__.load({
|
|
|
563
667
|
mounted.current = false;
|
|
564
668
|
};
|
|
565
669
|
}, []);
|
|
670
|
+
/**
|
|
671
|
+
* Toggle the composer credit readout (`showPointsInMainUi`). The card's
|
|
672
|
+
* checkbox is the ONLY writer of this field — the readout itself carries no
|
|
673
|
+
* control — so there is exactly one path that can change it, and the write is
|
|
674
|
+
* read-back-checked like every other settings write in this card.
|
|
675
|
+
*/
|
|
676
|
+
const toggleShowPointsInMainUi = async (enabled) => {
|
|
677
|
+
if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
|
|
678
|
+
setTogglingCredits(true);
|
|
679
|
+
setWriteError(void 0);
|
|
680
|
+
try {
|
|
681
|
+
await writeSettingsField(settingsScope, "showPointsInMainUi", enabled, (readBack) => unwrapVolatileDeep(readBack)?.showPointsInMainUi === enabled);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
684
|
+
} finally {
|
|
685
|
+
if (mounted.current) setTogglingCredits(false);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
566
688
|
(0, react.useEffect)(() => settingsScope?.subscribe(() => {
|
|
567
689
|
setSettingsRevision((value) => value + 1);
|
|
568
690
|
}), [settingsScope]);
|
|
@@ -1039,21 +1161,6 @@ window.__ModuleLoader__.load({
|
|
|
1039
1161
|
children: open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1040
1162
|
className: "dsm-trae-usage",
|
|
1041
1163
|
children: [
|
|
1042
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1043
|
-
className: "dsm-trae-mainui-switch",
|
|
1044
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1045
|
-
className: "dsm-trae-mainui-switch-label",
|
|
1046
|
-
title: t("row.showPointsInMainUiHint"),
|
|
1047
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1048
|
-
type: "checkbox",
|
|
1049
|
-
checked: settingsValue?.showPointsInMainUi === true,
|
|
1050
|
-
disabled: settingsScope?.getSnapshot().writable !== true || togglingMainUi,
|
|
1051
|
-
onChange: (event) => {
|
|
1052
|
-
toggleShowPointsInMainUi(event.currentTarget.checked);
|
|
1053
|
-
}
|
|
1054
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.showPointsInMainUi") })]
|
|
1055
|
-
})
|
|
1056
|
-
}),
|
|
1057
1164
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1058
1165
|
className: "dsm-trae-tabs",
|
|
1059
1166
|
role: "tablist",
|
|
@@ -1107,6 +1214,21 @@ window.__ModuleLoader__.load({
|
|
|
1107
1214
|
role: "status",
|
|
1108
1215
|
children: t("row.tabOffNotice")
|
|
1109
1216
|
}),
|
|
1217
|
+
activeRegion !== "cn" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1218
|
+
className: "dsm-trae-mainui-switch",
|
|
1219
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1220
|
+
className: "dsm-trae-mainui-switch-label",
|
|
1221
|
+
title: t("row.showPointsInMainUiHint"),
|
|
1222
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1223
|
+
type: "checkbox",
|
|
1224
|
+
checked: settingsValue?.showPointsInMainUi === true,
|
|
1225
|
+
disabled: settingsScope?.getSnapshot().writable !== true || togglingCredits,
|
|
1226
|
+
onChange: (event) => {
|
|
1227
|
+
toggleShowPointsInMainUi(event.currentTarget.checked);
|
|
1228
|
+
}
|
|
1229
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.showPointsInMainUi") })]
|
|
1230
|
+
})
|
|
1231
|
+
}),
|
|
1110
1232
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1111
1233
|
className: "dsm-trae-usage-account",
|
|
1112
1234
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -1166,7 +1288,7 @@ window.__ModuleLoader__.load({
|
|
|
1166
1288
|
children: [
|
|
1167
1289
|
t("row.creditsExhausted", { account: status.accountName }),
|
|
1168
1290
|
" ",
|
|
1169
|
-
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
|
|
1291
|
+
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("、") })
|
|
1170
1292
|
]
|
|
1171
1293
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1172
1294
|
type: "button",
|
|
@@ -1244,7 +1366,7 @@ window.__ModuleLoader__.load({
|
|
|
1244
1366
|
}),
|
|
1245
1367
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1246
1368
|
className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-work",
|
|
1247
|
-
children: formatNumber
|
|
1369
|
+
children: formatNumber(status.credits.workAvailable)
|
|
1248
1370
|
}),
|
|
1249
1371
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1250
1372
|
className: "dsm-trae-usage-stat-hint",
|
|
@@ -1266,7 +1388,7 @@ window.__ModuleLoader__.load({
|
|
|
1266
1388
|
}),
|
|
1267
1389
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1268
1390
|
className: "dsm-trae-usage-stat-value dsm-trae-usage-stat-value-general",
|
|
1269
|
-
children: formatNumber
|
|
1391
|
+
children: formatNumber(status.credits.generalAvailable)
|
|
1270
1392
|
}),
|
|
1271
1393
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1272
1394
|
className: "dsm-trae-usage-stat-hint",
|
|
@@ -1283,8 +1405,8 @@ window.__ModuleLoader__.load({
|
|
|
1283
1405
|
status.checkin === void 0 ? null : (() => {
|
|
1284
1406
|
const accountClaimed = status.checkin.checkedIn;
|
|
1285
1407
|
const deviceSpent = !accountClaimed && (status.checkin.didCheckedIn || deviceSpentNotice);
|
|
1286
|
-
const reward = formatNumber
|
|
1287
|
-
const bonus = status.checkin.extraCredits === void 0 ? "" : ` + ${formatNumber
|
|
1408
|
+
const reward = formatNumber(status.checkin.credits);
|
|
1409
|
+
const bonus = status.checkin.extraCredits === void 0 ? "" : ` + ${formatNumber(status.checkin.extraCredits)}`;
|
|
1288
1410
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1289
1411
|
className: "dsm-trae-checkin",
|
|
1290
1412
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -1338,42 +1460,27 @@ window.__ModuleLoader__.load({
|
|
|
1338
1460
|
children: t("row.modelsSummary", { count: activeEnabledIds.size })
|
|
1339
1461
|
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1340
1462
|
className: "dsm-trae-models-actions",
|
|
1341
|
-
children: [
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
},
|
|
1361
|
-
children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
|
|
1362
|
-
}),
|
|
1363
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1364
|
-
type: "button",
|
|
1365
|
-
className: "dsm-btn dsm-btn-outline",
|
|
1366
|
-
disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
|
|
1367
|
-
title: t("row.modelsTestHint"),
|
|
1368
|
-
onClick: () => {
|
|
1369
|
-
testEnabledModels();
|
|
1370
|
-
},
|
|
1371
|
-
children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
|
|
1372
|
-
done: testing.done,
|
|
1373
|
-
total: testing.total
|
|
1374
|
-
})
|
|
1463
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1464
|
+
type: "button",
|
|
1465
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1466
|
+
disabled: busy,
|
|
1467
|
+
onClick: () => {
|
|
1468
|
+
refreshModels();
|
|
1469
|
+
},
|
|
1470
|
+
children: busy ? t("row.modelsRefreshing") : t("row.modelsRefresh")
|
|
1471
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1472
|
+
type: "button",
|
|
1473
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1474
|
+
disabled: busy || testing !== void 0 || activeEnabledIds.size === 0,
|
|
1475
|
+
title: t("row.modelsTestHint"),
|
|
1476
|
+
onClick: () => {
|
|
1477
|
+
testEnabledModels();
|
|
1478
|
+
},
|
|
1479
|
+
children: testing === void 0 ? t("row.modelsTestEnabled") : t("row.modelsTesting", {
|
|
1480
|
+
done: testing.done,
|
|
1481
|
+
total: testing.total
|
|
1375
1482
|
})
|
|
1376
|
-
]
|
|
1483
|
+
})]
|
|
1377
1484
|
})]
|
|
1378
1485
|
}),
|
|
1379
1486
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1540,233 +1647,544 @@ window.__ModuleLoader__.load({
|
|
|
1540
1647
|
});
|
|
1541
1648
|
}
|
|
1542
1649
|
//#endregion
|
|
1543
|
-
//#region src/client/
|
|
1650
|
+
//#region src/client/popover.ts
|
|
1544
1651
|
/**
|
|
1545
|
-
*
|
|
1546
|
-
*
|
|
1652
|
+
* The two shell behaviours the composer credit panel needs: hanging the panel
|
|
1653
|
+
* off its trigger, and dismissing it on an outside press.
|
|
1654
|
+
*
|
|
1655
|
+
* Implemented locally rather than imported from
|
|
1656
|
+
* `@deepseek-ai/dsh-client-ui-primitives`. The shell DOES register that package
|
|
1657
|
+
* for plugin bundles, so importing it is legal — this is a deliberate choice,
|
|
1658
|
+
* not a workaround: the two behaviours are ~40 lines of DOM geometry, and owning
|
|
1659
|
+
* them keeps this component renderable in the Node test suite without mocking a
|
|
1660
|
+
* package that itself reaches `*.module.css` and further DSH dependencies.
|
|
1547
1661
|
*
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1662
|
+
* NOTE ON A MISDIAGNOSIS, kept because it is the kind of thing that gets
|
|
1663
|
+
* re-litigated: the 2.12.0/2.13.0 client load failure was first attributed to
|
|
1664
|
+
* importing the primitives package. It was not that. The real cause was
|
|
1665
|
+
* `import { createPortal } from 'react-dom'` being BUNDLED rather than
|
|
1666
|
+
* externalized — the inlined development build opens with
|
|
1667
|
+
* `process.env.NODE_ENV` and threw `ReferenceError: process is not defined` in
|
|
1668
|
+
* the browser. See `tsdown.config.ts` for the fix and
|
|
1669
|
+
* `tests/client-runtime-imports.spec.ts` for the rule that now prevents it.
|
|
1553
1670
|
*
|
|
1554
|
-
*
|
|
1555
|
-
*
|
|
1671
|
+
* Behaviour is intentionally identical to the shell's implementations: the same
|
|
1672
|
+
* top/bottom placement, the same viewport clamping, the same re-measure on
|
|
1673
|
+
* scroll (capture, so nested scrollers count), resize and the panel's own size
|
|
1674
|
+
* changes, and the same "a pointerdown outside both the trigger and the panel
|
|
1675
|
+
* closes it" rule.
|
|
1556
1676
|
*/
|
|
1557
1677
|
/**
|
|
1558
|
-
*
|
|
1559
|
-
* this is the gap between subsequent automatic fetches.
|
|
1678
|
+
* Clearance to keep from the top of the viewport.
|
|
1560
1679
|
*
|
|
1561
|
-
*
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1680
|
+
* The shell reserves a strip at the top of the frame (`--dsh-frame-top-clearance`)
|
|
1681
|
+
* for the window chrome, so a panel clamped to `margin` alone can still land
|
|
1682
|
+
* under it. Absent variable or no `data-fullscreen` distinction is handled the
|
|
1683
|
+
* same way the shell does: fall back to `min`.
|
|
1684
|
+
*/
|
|
1685
|
+
function overlayTopMargin(min) {
|
|
1686
|
+
if (typeof document === "undefined") return min;
|
|
1687
|
+
const root = document.documentElement;
|
|
1688
|
+
const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue("--dsh-frame-top-clearance"));
|
|
1689
|
+
if (Number.isNaN(clearance)) return min;
|
|
1690
|
+
return Math.max(min, (root.hasAttribute("data-fullscreen") ? 0 : clearance) + 20);
|
|
1691
|
+
}
|
|
1692
|
+
/** Keep a `position: fixed` floating element anchored to a trigger. */
|
|
1693
|
+
function useAnchoredPosition(options) {
|
|
1694
|
+
const { open, anchorRef, panelRef, side = "bottom", align = "start", gap, margin } = options;
|
|
1695
|
+
const [position, setPosition] = (0, react.useState)(null);
|
|
1696
|
+
(0, react.useLayoutEffect)(() => {
|
|
1697
|
+
if (!open) {
|
|
1698
|
+
setPosition(null);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const place = () => {
|
|
1702
|
+
const rect = anchorRef.current?.getBoundingClientRect();
|
|
1703
|
+
if (rect === void 0) return;
|
|
1704
|
+
const panel = panelRef.current;
|
|
1705
|
+
const width = panel?.offsetWidth ?? 0;
|
|
1706
|
+
const height = panel?.offsetHeight ?? 0;
|
|
1707
|
+
let left = align === "end" ? rect.right - width : rect.left;
|
|
1708
|
+
let top = side === "top" ? rect.top - gap - height : rect.bottom + gap;
|
|
1709
|
+
if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
|
|
1710
|
+
if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin);
|
|
1711
|
+
setPosition({
|
|
1712
|
+
left,
|
|
1713
|
+
top
|
|
1714
|
+
});
|
|
1715
|
+
};
|
|
1716
|
+
place();
|
|
1717
|
+
window.addEventListener("scroll", place, true);
|
|
1718
|
+
window.addEventListener("resize", place);
|
|
1719
|
+
const panel = panelRef.current;
|
|
1720
|
+
let observer = null;
|
|
1721
|
+
if (typeof ResizeObserver !== "undefined" && panel !== null) {
|
|
1722
|
+
observer = new ResizeObserver(place);
|
|
1723
|
+
observer.observe(panel);
|
|
1724
|
+
}
|
|
1725
|
+
return () => {
|
|
1726
|
+
observer?.disconnect();
|
|
1727
|
+
window.removeEventListener("scroll", place, true);
|
|
1728
|
+
window.removeEventListener("resize", place);
|
|
1729
|
+
};
|
|
1730
|
+
}, [
|
|
1731
|
+
open,
|
|
1732
|
+
anchorRef,
|
|
1733
|
+
panelRef,
|
|
1734
|
+
side,
|
|
1735
|
+
align,
|
|
1736
|
+
gap,
|
|
1737
|
+
margin
|
|
1738
|
+
]);
|
|
1739
|
+
return position;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Close an open popover when a pointerdown lands outside it.
|
|
1743
|
+
*
|
|
1744
|
+
* `portal` counts as inside: the panel is portaled to `document.body`, so it is
|
|
1745
|
+
* not a descendant of `root` and a click on the panel itself would otherwise
|
|
1746
|
+
* read as "outside" and dismiss the popover the user is trying to use.
|
|
1747
|
+
*/
|
|
1748
|
+
function useDismissOnOutsidePointer(root, open, setOpen, portal) {
|
|
1749
|
+
(0, react.useEffect)(() => {
|
|
1750
|
+
if (!open) return void 0;
|
|
1751
|
+
const closeOutside = (event) => {
|
|
1752
|
+
if (event.target instanceof Node && root.current?.contains(event.target) !== true && portal?.current?.contains(event.target) !== true) setOpen(false);
|
|
1753
|
+
};
|
|
1754
|
+
document.addEventListener("pointerdown", closeOutside);
|
|
1755
|
+
return () => {
|
|
1756
|
+
document.removeEventListener("pointerdown", closeOutside);
|
|
1757
|
+
};
|
|
1758
|
+
}, [
|
|
1759
|
+
root,
|
|
1760
|
+
open,
|
|
1761
|
+
setOpen,
|
|
1762
|
+
portal
|
|
1763
|
+
]);
|
|
1764
|
+
}
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region src/client/ComposerPoints.tsx
|
|
1767
|
+
/**
|
|
1768
|
+
* Credit readout for the composer tool row (`conversation.input.left`).
|
|
1769
|
+
*
|
|
1770
|
+
* Shows the bound CN account's general credit balance, but ONLY while the
|
|
1771
|
+
* session's selected model belongs to this plugin. That condition is the whole
|
|
1772
|
+
* point: the sidebar foot is a single shared row, so two connector plugins each
|
|
1773
|
+
* parking a line there end up fighting for the same space. The composer row is
|
|
1774
|
+
* per-session and provider-scoped, so exactly one plugin's credits are relevant
|
|
1775
|
+
* at a time and nothing overlaps.
|
|
1776
|
+
*
|
|
1777
|
+
* The row itself is a bare clickable readout — `Trae CN · 657` — with NO
|
|
1778
|
+
* refresh control. Clicking it opens an anchored panel: a TABLE with one row
|
|
1779
|
+
* per account in the region and that account's general balance, where a row
|
|
1780
|
+
* click switches to it. The manual refresh button lives in the panel too.
|
|
1781
|
+
*
|
|
1782
|
+
* Two deliberate choices in that panel:
|
|
1783
|
+
*
|
|
1784
|
+
* - GENERAL balance only, not Work credits. The table's one figure is the
|
|
1785
|
+
* bucket the SOLO chat actually spends; two columns of numbers in a
|
|
1786
|
+
* five-slot popover is noise, and the card is where the breakdown belongs.
|
|
1787
|
+
* - Per-account figures are fetched ONLY while the panel is open. The
|
|
1788
|
+
* 5-minute readout must not multiply its upstream reads by the account
|
|
1789
|
+
* count — a healthy balance should not pay for a table nobody is looking at.
|
|
1790
|
+
* This is the same principle that keeps `creditAlternatives` firing only
|
|
1791
|
+
* when a balance hits zero.
|
|
1792
|
+
*
|
|
1793
|
+
* The provider comes from the Host's own `modelSelection` projection rather
|
|
1794
|
+
* than from anything this plugin tracks: the projection is what the model
|
|
1795
|
+
* picker writes and what the next request will use, so the readout cannot
|
|
1796
|
+
* disagree with the model actually in effect.
|
|
1797
|
+
*/
|
|
1798
|
+
/**
|
|
1799
|
+
* Inject the readout's styles into the document head, once, on module load.
|
|
1800
|
+
*
|
|
1801
|
+
* Same idempotent pattern as the card's injection, and for the same reason: the
|
|
1802
|
+
* classes are attached to DOM this component owns, and `.dsm-card-*` lives in
|
|
1803
|
+
* `TRAE_CARD_CSS` — which only loads when the settings card does. Before this
|
|
1804
|
+
* existed (2.13.0–2.13.4) the trigger rendered as the browser's default button
|
|
1805
|
+
* box and the panel, with no `position: fixed`, had its measured left/top
|
|
1806
|
+
* ignored and appeared nowhere.
|
|
1565
1807
|
*/
|
|
1566
|
-
const SIDEBAR_POINTS_REFRESH_INTERVAL_MS = 3e5;
|
|
1567
|
-
/** Inject the sidebar-line CSS once. */
|
|
1568
1808
|
if (typeof document !== "undefined") {
|
|
1569
|
-
const cssId = "dsh-connect-trae/
|
|
1809
|
+
const cssId = "dsh-connect-trae/composer-points.css";
|
|
1570
1810
|
if (!document.querySelector(`style[data-plugin-css="${cssId}"]`)) {
|
|
1571
1811
|
const styleTag = document.createElement("style");
|
|
1572
1812
|
styleTag.dataset.plugin = "dsh-connect-trae";
|
|
1573
1813
|
styleTag.dataset.pluginCss = cssId;
|
|
1574
|
-
styleTag.textContent =
|
|
1814
|
+
styleTag.textContent = COMPOSER_POINTS_CSS;
|
|
1575
1815
|
document.head.appendChild(styleTag);
|
|
1576
1816
|
}
|
|
1577
1817
|
}
|
|
1578
|
-
function formatNumber(value) {
|
|
1579
|
-
return new Intl.NumberFormat(void 0, { maximumFractionDigits: 0 }).format(value);
|
|
1580
|
-
}
|
|
1581
|
-
function formatClock(value) {
|
|
1582
|
-
return new Intl.DateTimeFormat(void 0, {
|
|
1583
|
-
hour: "2-digit",
|
|
1584
|
-
minute: "2-digit",
|
|
1585
|
-
second: "2-digit"
|
|
1586
|
-
}).format(new Date(value));
|
|
1587
|
-
}
|
|
1588
1818
|
/**
|
|
1589
|
-
*
|
|
1819
|
+
* Automatic refresh period. Five minutes, matching the sidebar line: a credit
|
|
1820
|
+
* balance moves on the scale of model calls, not seconds, and each read is two
|
|
1821
|
+
* read-only upstream calls.
|
|
1822
|
+
*/
|
|
1823
|
+
const COMPOSER_POINTS_REFRESH_INTERVAL_MS = 3e5;
|
|
1824
|
+
/**
|
|
1825
|
+
* Provider routes whose credit balance this readout can show, mapped to the
|
|
1826
|
+
* region to read it from.
|
|
1827
|
+
*
|
|
1828
|
+
* CN ONLY, and the restriction is the data's, not a preference: the Host builds
|
|
1829
|
+
* the international region's usage document from `payStatus` and never sets
|
|
1830
|
+
* `credits` on it (`webStatusFor` returns early for `region === 'ai'`), because
|
|
1831
|
+
* that side is subscription-based and has no comparable balance. Mapping
|
|
1832
|
+
* `trae-global` here would render the hardcoded "Trae CN" label next to a dash
|
|
1833
|
+
* — a number-slot with no number — while an international model is selected.
|
|
1590
1834
|
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
* shows two buckets derived from the entitlement packs; the aggregate is not
|
|
1594
|
-
* guaranteed to equal the buckets, and a sidebar number the user cannot match
|
|
1595
|
-
* against the card is worse than no number at all. The general bucket is the
|
|
1596
|
-
* one the SOLO chat path spends, so it is the one worth watching.
|
|
1835
|
+
* Mirrors the CN half of `TRAE_PROVIDERS` on the host side; duplicated here
|
|
1836
|
+
* because the client bundle must not import the host entry.
|
|
1597
1837
|
*/
|
|
1838
|
+
const TRAE_COMPOSER_PROVIDERS = { "trae": "cn" };
|
|
1839
|
+
/** The general credit balance out of one usage document, or undefined. */
|
|
1598
1840
|
function generalCreditsOf(usage) {
|
|
1599
1841
|
if (usage.status !== "signed-in") return void 0;
|
|
1600
1842
|
return usage.credits?.generalAvailable;
|
|
1601
1843
|
}
|
|
1602
|
-
/**
|
|
1603
|
-
function
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1844
|
+
/** Group the digits so a four-figure balance stays readable in a narrow row. */
|
|
1845
|
+
function formatCredits(value) {
|
|
1846
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
1847
|
+
}
|
|
1848
|
+
function formatClock(value) {
|
|
1849
|
+
return new Date(value).toLocaleTimeString("zh-CN", {
|
|
1850
|
+
hour: "2-digit",
|
|
1851
|
+
minute: "2-digit"
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
function ComposerPoints(props) {
|
|
1855
|
+
const { t, provider, region, settingsScope } = props;
|
|
1856
|
+
if (t === void 0) throw new Error("Composer points readout requires its translation function");
|
|
1857
|
+
const owned = provider === void 0 ? void 0 : TRAE_COMPOSER_PROVIDERS[provider];
|
|
1858
|
+
const activeRegion = region ?? owned;
|
|
1859
|
+
const [usage, setUsage] = (0, react.useState)(void 0);
|
|
1860
|
+
const [accountCredits, setAccountCredits] = (0, react.useState)(void 0);
|
|
1607
1861
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1862
|
+
const [switchingId, setSwitchingId] = (0, react.useState)(void 0);
|
|
1608
1863
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
1609
1864
|
const [lastRefresh, setLastRefresh] = (0, react.useState)(void 0);
|
|
1610
|
-
|
|
1865
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1611
1866
|
const inFlight = (0, react.useRef)(false);
|
|
1612
1867
|
const mounted = (0, react.useRef)(true);
|
|
1868
|
+
const rootRef = (0, react.useRef)(null);
|
|
1869
|
+
const panelRef = (0, react.useRef)(null);
|
|
1613
1870
|
(0, react.useEffect)(() => {
|
|
1614
1871
|
mounted.current = true;
|
|
1615
1872
|
return () => {
|
|
1616
1873
|
mounted.current = false;
|
|
1617
1874
|
};
|
|
1618
1875
|
}, []);
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1876
|
+
const position = useAnchoredPosition({
|
|
1877
|
+
open: open && activeRegion !== void 0,
|
|
1878
|
+
anchorRef: rootRef,
|
|
1879
|
+
panelRef,
|
|
1880
|
+
side: "top",
|
|
1881
|
+
gap: 8,
|
|
1882
|
+
margin: 12
|
|
1883
|
+
});
|
|
1884
|
+
useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
|
|
1885
|
+
const fetchUsage = (0, react.useCallback)(async (signal) => {
|
|
1886
|
+
if (inFlight.current || activeRegion === void 0) return;
|
|
1621
1887
|
inFlight.current = true;
|
|
1622
1888
|
if (mounted.current) setBusy(true);
|
|
1623
1889
|
try {
|
|
1624
|
-
const response = await fetch(withTraeRegion(TRAE_USAGE_PATH,
|
|
1890
|
+
const response = await fetch(withTraeRegion(TRAE_USAGE_PATH, activeRegion), {
|
|
1625
1891
|
headers: { accept: "application/json" },
|
|
1626
1892
|
credentials: "same-origin",
|
|
1627
1893
|
...signal === void 0 ? {} : { signal }
|
|
1628
1894
|
});
|
|
1629
1895
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1630
1896
|
const value = await response.json();
|
|
1631
|
-
if (!mounted.current || signal?.aborted === true) return
|
|
1632
|
-
|
|
1633
|
-
if (general !== void 0) {
|
|
1634
|
-
setPoints(general);
|
|
1635
|
-
setSignedIn(true);
|
|
1636
|
-
} else setSignedIn(value.status === "signed-in");
|
|
1637
|
-
setFailed(false);
|
|
1897
|
+
if (!mounted.current || signal?.aborted === true) return;
|
|
1898
|
+
setUsage(value);
|
|
1638
1899
|
setLastRefresh(Date.now());
|
|
1639
|
-
|
|
1900
|
+
setFailed(false);
|
|
1640
1901
|
} catch {
|
|
1641
1902
|
if (mounted.current && signal?.aborted !== true) setFailed(true);
|
|
1642
|
-
return false;
|
|
1643
1903
|
} finally {
|
|
1644
1904
|
inFlight.current = false;
|
|
1645
1905
|
if (mounted.current) setBusy(false);
|
|
1646
1906
|
}
|
|
1647
|
-
}, []);
|
|
1907
|
+
}, [activeRegion]);
|
|
1908
|
+
/**
|
|
1909
|
+
* Every account's general balance — one upstream read per account.
|
|
1910
|
+
*
|
|
1911
|
+
* Paid only while the panel is open (see the note at the top of this file).
|
|
1912
|
+
* A failure keeps whatever we already have rather than clearing it: a number
|
|
1913
|
+
* that went stale is better than a switch that vanished.
|
|
1914
|
+
*/
|
|
1915
|
+
const fetchAccountCredits = (0, react.useCallback)(async (signal) => {
|
|
1916
|
+
if (activeRegion === void 0) return;
|
|
1917
|
+
try {
|
|
1918
|
+
const response = await fetch(withTraeRegion(TRAE_ACCOUNT_CREDITS_PATH, activeRegion), {
|
|
1919
|
+
headers: { accept: "application/json" },
|
|
1920
|
+
credentials: "same-origin",
|
|
1921
|
+
...signal === void 0 ? {} : { signal }
|
|
1922
|
+
});
|
|
1923
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1924
|
+
const data = await response.json();
|
|
1925
|
+
if (!mounted.current || signal?.aborted === true) return;
|
|
1926
|
+
if (Array.isArray(data.accounts)) setAccountCredits(data.accounts);
|
|
1927
|
+
} catch {}
|
|
1928
|
+
}, [activeRegion]);
|
|
1648
1929
|
(0, react.useEffect)(() => {
|
|
1930
|
+
if (owned === void 0) return void 0;
|
|
1649
1931
|
let cancelled = false;
|
|
1650
1932
|
let timer;
|
|
1651
1933
|
const controller = new AbortController();
|
|
1652
|
-
|
|
1934
|
+
fetchUsage(controller.signal).finally(() => {
|
|
1653
1935
|
if (cancelled) return;
|
|
1654
1936
|
timer = window.setInterval(() => {
|
|
1655
|
-
|
|
1656
|
-
},
|
|
1937
|
+
fetchUsage(controller.signal);
|
|
1938
|
+
}, COMPOSER_POINTS_REFRESH_INTERVAL_MS);
|
|
1657
1939
|
});
|
|
1658
1940
|
return () => {
|
|
1659
1941
|
cancelled = true;
|
|
1660
1942
|
if (timer !== void 0) window.clearInterval(timer);
|
|
1661
1943
|
controller.abort();
|
|
1662
1944
|
};
|
|
1663
|
-
}, [
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1945
|
+
}, [owned, fetchUsage]);
|
|
1946
|
+
(0, react.useEffect)(() => {
|
|
1947
|
+
if (!open || activeRegion === void 0) return void 0;
|
|
1948
|
+
const controller = new AbortController();
|
|
1949
|
+
fetchAccountCredits(controller.signal);
|
|
1950
|
+
return () => {
|
|
1951
|
+
controller.abort();
|
|
1952
|
+
};
|
|
1953
|
+
}, [
|
|
1954
|
+
open,
|
|
1955
|
+
activeRegion,
|
|
1956
|
+
fetchAccountCredits
|
|
1957
|
+
]);
|
|
1958
|
+
(0, react.useEffect)(() => {
|
|
1959
|
+
if (!open) return void 0;
|
|
1960
|
+
const onKeyDown = (event) => {
|
|
1961
|
+
if (event.key === "Escape") setOpen(false);
|
|
1962
|
+
};
|
|
1963
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1964
|
+
return () => {
|
|
1965
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1966
|
+
};
|
|
1967
|
+
}, [open]);
|
|
1968
|
+
/**
|
|
1969
|
+
* Switch to another account, then re-read both the usage document and the
|
|
1970
|
+
* table so the marker, the readout, and the panel all move together.
|
|
1971
|
+
*
|
|
1972
|
+
* The merge goes through the shared `configuredAccountsOf` helper (the card
|
|
1973
|
+
* uses the same one): writing only `[region]: id` would drop the other
|
|
1974
|
+
* region's selection. The write is read-back checked — `set()` resolving is
|
|
1975
|
+
* not proof it landed — so a refused write reports failure instead of
|
|
1976
|
+
* showing a marker on an account that was never selected.
|
|
1977
|
+
*/
|
|
1978
|
+
const switchTo = async (accountId) => {
|
|
1979
|
+
if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
|
|
1980
|
+
if (activeRegion === void 0 || switchingId !== void 0) return;
|
|
1981
|
+
if (accountId === (accountCredits?.find((account) => account.selected)?.id ?? (usage?.status === "signed-in" ? usage.accountId : void 0))) return;
|
|
1982
|
+
setSwitchingId(accountId);
|
|
1983
|
+
setFailed(false);
|
|
1984
|
+
try {
|
|
1985
|
+
const configured = configuredAccountsOf(settingsScope.getSnapshot().value);
|
|
1986
|
+
const accepted = await settingsScope.set("accounts", {
|
|
1987
|
+
...configured,
|
|
1988
|
+
[activeRegion]: accountId
|
|
1989
|
+
});
|
|
1990
|
+
const readBack = configuredAccountsOf(settingsScope.getSnapshot().value);
|
|
1991
|
+
if (accepted === false || readBack[activeRegion] !== accountId) throw new Error("the Host did not persist the switch");
|
|
1992
|
+
await fetchUsage();
|
|
1993
|
+
await fetchAccountCredits();
|
|
1994
|
+
} catch {
|
|
1995
|
+
if (mounted.current) setFailed(true);
|
|
1996
|
+
} finally {
|
|
1997
|
+
if (mounted.current) setSwitchingId(void 0);
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
if (owned === void 0) return null;
|
|
2001
|
+
const signedIn = usage?.status === "signed-in" ? usage : void 0;
|
|
2002
|
+
const general = signedIn === void 0 ? void 0 : generalCreditsOf(signedIn);
|
|
2003
|
+
const signedOut = usage !== void 0 && usage.status === "signed-out";
|
|
2004
|
+
const valueText = general === void 0 ? "—" : formatCredits(general);
|
|
2005
|
+
const label = t("composer.points");
|
|
2006
|
+
/**
|
|
2007
|
+
* The table's rows: every account in this region, with its general balance.
|
|
2008
|
+
*
|
|
2009
|
+
* The endpoint's answer is authoritative (it carries the figures). Before it
|
|
2010
|
+
* lands — or if it fails — fall back to the usage document's account list so
|
|
2011
|
+
* the switches are reachable immediately, showing `—` for the figure rather
|
|
2012
|
+
* than a spinner the user has to wait on.
|
|
2013
|
+
*/
|
|
2014
|
+
const selectedId = accountCredits?.find((account) => account.selected)?.id ?? (signedIn === void 0 ? void 0 : signedIn.accountId);
|
|
2015
|
+
const rows = (accountCredits ?? (signedIn === void 0 ? [] : signedIn.accounts.filter((account) => account.region === activeRegion).map((account) => ({
|
|
2016
|
+
id: account.id,
|
|
2017
|
+
accountName: account.accountName,
|
|
2018
|
+
selected: account.selected
|
|
2019
|
+
})))).map((row) => {
|
|
2020
|
+
if (row.id !== selectedId || row.generalAvailable !== void 0 || general === void 0) return row;
|
|
2021
|
+
return {
|
|
2022
|
+
...row,
|
|
2023
|
+
generalAvailable: general
|
|
2024
|
+
};
|
|
2025
|
+
});
|
|
2026
|
+
const canSwitch = settingsScope !== void 0 && settingsScope.getSnapshot().writable === true;
|
|
2027
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2028
|
+
ref: rootRef,
|
|
2029
|
+
className: "dsm-trae-composer-points",
|
|
2030
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2031
|
+
type: "button",
|
|
2032
|
+
className: "dsm-trae-composer-points-trigger",
|
|
2033
|
+
"aria-haspopup": "dialog",
|
|
2034
|
+
"aria-expanded": open,
|
|
2035
|
+
onClick: () => {
|
|
2036
|
+
setOpen(!open);
|
|
2037
|
+
},
|
|
2038
|
+
children: [
|
|
2039
|
+
label,
|
|
2040
|
+
" · ",
|
|
2041
|
+
valueText
|
|
2042
|
+
]
|
|
2043
|
+
}), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2044
|
+
ref: panelRef,
|
|
2045
|
+
className: "dsm-trae-composer-panel",
|
|
2046
|
+
style: position ?? {
|
|
2047
|
+
visibility: "hidden",
|
|
2048
|
+
left: 0,
|
|
2049
|
+
top: 0
|
|
2050
|
+
},
|
|
2051
|
+
role: "dialog",
|
|
2052
|
+
"aria-label": t("composer.panelTitle"),
|
|
2053
|
+
children: [
|
|
2054
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2055
|
+
className: "dsm-trae-composer-panel-head",
|
|
2056
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2057
|
+
className: "dsm-trae-composer-panel-title",
|
|
2058
|
+
children: t("composer.panelTitle")
|
|
2059
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2060
|
+
type: "button",
|
|
2061
|
+
className: "dsm-trae-composer-panel-refresh",
|
|
2062
|
+
disabled: busy || switchingId !== void 0,
|
|
2063
|
+
onClick: () => {
|
|
2064
|
+
fetchUsage();
|
|
2065
|
+
},
|
|
2066
|
+
children: busy ? t("composer.refreshing") : t("composer.refresh")
|
|
2067
|
+
})]
|
|
2068
|
+
}),
|
|
2069
|
+
signedOut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2070
|
+
className: "dsm-trae-composer-panel-empty",
|
|
2071
|
+
children: t("composer.signedOut")
|
|
2072
|
+
}) : rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2073
|
+
className: "dsm-trae-composer-panel-empty",
|
|
2074
|
+
children: t("composer.noAccounts")
|
|
2075
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
2076
|
+
className: "dsm-trae-composer-panel-table",
|
|
2077
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2078
|
+
scope: "col",
|
|
2079
|
+
children: t("composer.account")
|
|
2080
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2081
|
+
scope: "col",
|
|
2082
|
+
className: "dsm-trae-composer-panel-num",
|
|
2083
|
+
children: t("composer.generalCredits")
|
|
2084
|
+
})] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row) => {
|
|
2085
|
+
const current = row.id === selectedId;
|
|
2086
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
2087
|
+
className: current ? "dsm-trae-composer-panel-row-current" : void 0,
|
|
2088
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
2089
|
+
scope: "row",
|
|
2090
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2091
|
+
type: "button",
|
|
2092
|
+
className: "dsm-trae-composer-panel-switch",
|
|
2093
|
+
disabled: !canSwitch || switchingId !== void 0 || current,
|
|
2094
|
+
"aria-label": t("composer.switchTo", { account: row.accountName }),
|
|
2095
|
+
onClick: () => {
|
|
2096
|
+
switchTo(row.id);
|
|
2097
|
+
},
|
|
2098
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2099
|
+
className: "dsm-trae-composer-panel-dot",
|
|
2100
|
+
"aria-hidden": "true"
|
|
2101
|
+
}), row.accountName]
|
|
2102
|
+
})
|
|
2103
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2104
|
+
className: "dsm-trae-composer-panel-num",
|
|
2105
|
+
children: row.generalAvailable === void 0 ? "—" : formatCredits(row.generalAvailable)
|
|
2106
|
+
})]
|
|
2107
|
+
}, row.id);
|
|
2108
|
+
}) })]
|
|
2109
|
+
}),
|
|
2110
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2111
|
+
className: "dsm-trae-composer-panel-foot",
|
|
2112
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2113
|
+
t("composer.lastRefresh"),
|
|
2114
|
+
" ",
|
|
2115
|
+
lastRefresh === void 0 ? "—" : formatClock(lastRefresh)
|
|
2116
|
+
] })
|
|
2117
|
+
}),
|
|
2118
|
+
failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2119
|
+
className: "dsm-trae-composer-panel-error",
|
|
1682
2120
|
role: "status",
|
|
1683
|
-
children: t("
|
|
1684
|
-
}) : null
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
type: "button",
|
|
1688
|
-
className: "dsm-trae-sidebar-points-refresh",
|
|
1689
|
-
disabled: busy,
|
|
1690
|
-
"aria-label": busy ? t("sidebar.refreshing") : t("sidebar.refresh"),
|
|
1691
|
-
title: busy ? t("sidebar.refreshing") : t("sidebar.refresh"),
|
|
1692
|
-
onClick: () => {
|
|
1693
|
-
fetchPoints();
|
|
1694
|
-
},
|
|
1695
|
-
children: busy ? t("sidebar.refreshing") : t("sidebar.refresh")
|
|
1696
|
-
}),
|
|
1697
|
-
onHide === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1698
|
-
type: "button",
|
|
1699
|
-
className: "dsm-trae-sidebar-points-hide",
|
|
1700
|
-
"aria-label": t("sidebar.hide"),
|
|
1701
|
-
title: t("sidebar.hide"),
|
|
1702
|
-
onClick: onHide,
|
|
1703
|
-
children: "×"
|
|
1704
|
-
})
|
|
1705
|
-
]
|
|
2121
|
+
children: t("composer.refreshFailed")
|
|
2122
|
+
}) : null
|
|
2123
|
+
]
|
|
2124
|
+
}), document.body)]
|
|
1706
2125
|
});
|
|
1707
2126
|
}
|
|
1708
2127
|
//#endregion
|
|
1709
|
-
//#region src/client/
|
|
2128
|
+
//#region src/client/ComposerPointsGate.tsx
|
|
1710
2129
|
/**
|
|
1711
|
-
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
* can be imported from the Node/jsdom test suite: the entry imports
|
|
1715
|
-
* browser-only DSH packages the test environment cannot load, but this gate
|
|
1716
|
-
* depends only on React, the card's settings-scope shape and the line
|
|
1717
|
-
* component.
|
|
2130
|
+
* Decides whether the composer credit readout belongs in this session, by
|
|
2131
|
+
* reading the Host's own `modelSelection` projection and the plugin's
|
|
2132
|
+
* `showPointsInMainUi` switch.
|
|
1718
2133
|
*
|
|
1719
|
-
*
|
|
2134
|
+
* Kept separate from {@link ComposerPoints} (and from the browser-plugin entry)
|
|
2135
|
+
* so the decision can be unit-tested without a Host: the entry imports
|
|
2136
|
+
* browser-only DSH packages the test environment cannot load, while this module
|
|
2137
|
+
* depends only on React and a narrow projection shape.
|
|
1720
2138
|
*
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
2139
|
+
* WHY the projection rather than this plugin's own state: the projection is
|
|
2140
|
+
* what the model picker writes and what the next request will use, so the
|
|
2141
|
+
* readout cannot disagree with the model actually in effect. `next` outranks
|
|
2142
|
+
* `lastUsed` because a selection that has been made but not yet sent is still
|
|
2143
|
+
* what the composer shows the user.
|
|
1723
2144
|
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
*
|
|
1728
|
-
*
|
|
1729
|
-
|
|
2145
|
+
* The switch is read the same way the retired sidebar gate read it: absent or
|
|
2146
|
+
* false means OFF, and only an explicit `true` shows the readout. The composer
|
|
2147
|
+
* row is shared with the shell's own permission/agent/model controls, so the
|
|
2148
|
+
* readout must be asked for, not imposed — 2.13.0 rendered it unconditionally
|
|
2149
|
+
* and 2.13.3 restored the opt-in after the user went looking for the switch.
|
|
2150
|
+
*/
|
|
2151
|
+
/**
|
|
2152
|
+
* The provider route the session is currently pointed at.
|
|
1730
2153
|
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
* the bail-out below.
|
|
2154
|
+
* `next` wins over `lastUsed`: a switch that has landed but not yet been sent
|
|
2155
|
+
* is still the selection the composer displays.
|
|
1734
2156
|
*/
|
|
2157
|
+
function selectedProviderOf(projection) {
|
|
2158
|
+
const provider = (projection?.next ?? projection?.lastUsed)?.provider;
|
|
2159
|
+
return typeof provider === "string" && provider !== "" ? provider : void 0;
|
|
2160
|
+
}
|
|
1735
2161
|
/**
|
|
1736
|
-
* Read the
|
|
2162
|
+
* Read the composer-readout switch out of a committed settings snapshot.
|
|
1737
2163
|
*
|
|
1738
|
-
* Absent means
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
* the balance.
|
|
2164
|
+
* Absent means OFF, matching the host default (`false`): a scope that has not
|
|
2165
|
+
* populated yet, or a host too old to serve the field, must not put a row in
|
|
2166
|
+
* the composer toolbar that the user never asked for.
|
|
1742
2167
|
*/
|
|
1743
|
-
function
|
|
1744
|
-
if (scope === void 0) return
|
|
1745
|
-
return unwrapVolatileDeep(scope.getSnapshot().value)?.showPointsInMainUi
|
|
2168
|
+
function composerPointsEnabledOf(scope) {
|
|
2169
|
+
if (scope === void 0) return false;
|
|
2170
|
+
return unwrapVolatileDeep(scope.getSnapshot().value)?.showPointsInMainUi === true;
|
|
1746
2171
|
}
|
|
1747
|
-
function
|
|
1748
|
-
const { t, settingsScope,
|
|
1749
|
-
const [enabled, setEnabled] = (0, react.useState)(() =>
|
|
2172
|
+
function ComposerPointsGate(props) {
|
|
2173
|
+
const { t, settingsScope, useProjection } = props;
|
|
2174
|
+
const [enabled, setEnabled] = (0, react.useState)(() => composerPointsEnabledOf(settingsScope));
|
|
1750
2175
|
(0, react.useEffect)(() => settingsScope?.subscribe(() => {
|
|
1751
|
-
setEnabled(
|
|
2176
|
+
setEnabled(composerPointsEnabledOf(settingsScope));
|
|
1752
2177
|
}), [settingsScope]);
|
|
1753
|
-
|
|
1754
|
-
* Write the switch back through the same field the card writes, so the two
|
|
1755
|
-
* controls can never disagree. The read-back is not awaited for its value:
|
|
1756
|
-
* the subscription above re-renders when the committed snapshot changes, and
|
|
1757
|
-
* an unwritable scope simply leaves the current state alone.
|
|
1758
|
-
*/
|
|
1759
|
-
const setShown = (0, react.useCallback)((next) => {
|
|
1760
|
-
if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
|
|
1761
|
-
settingsScope.set("showPointsInMainUi", next).catch(() => {});
|
|
1762
|
-
}, [settingsScope]);
|
|
1763
|
-
if (wide === false) return null;
|
|
2178
|
+
const projection = useProjection === void 0 ? void 0 : useProjection("modelSelection");
|
|
1764
2179
|
if (!enabled) return null;
|
|
1765
|
-
|
|
2180
|
+
const provider = selectedProviderOf(projection);
|
|
2181
|
+
const region = provider === void 0 ? void 0 : TRAE_COMPOSER_PROVIDERS[provider];
|
|
2182
|
+
if (region === void 0) return null;
|
|
2183
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerPoints, {
|
|
1766
2184
|
t,
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
}
|
|
2185
|
+
provider,
|
|
2186
|
+
region,
|
|
2187
|
+
...settingsScope === void 0 ? {} : { settingsScope }
|
|
1770
2188
|
});
|
|
1771
2189
|
}
|
|
1772
2190
|
//#endregion
|
|
@@ -1843,15 +2261,19 @@ window.__ModuleLoader__.load({
|
|
|
1843
2261
|
"row.modelsTestEnabled": "Test enabled models",
|
|
1844
2262
|
"row.modelsTesting": "Testing {done}/{total}…",
|
|
1845
2263
|
"row.modelsTestHint": "Sends one minimal message per enabled model to see which ones actually answer. Costs a small amount of quota.",
|
|
1846
|
-
"row.showPointsInMainUi": "Show credits in the
|
|
1847
|
-
"row.showPointsInMainUiHint": "
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"
|
|
1851
|
-
"
|
|
1852
|
-
"
|
|
1853
|
-
"
|
|
1854
|
-
"
|
|
2264
|
+
"row.showPointsInMainUi": "Show credits in the composer row",
|
|
2265
|
+
"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.",
|
|
2266
|
+
"composer.switchTo": "Switch to {account}",
|
|
2267
|
+
"composer.noAccounts": "No account in this region",
|
|
2268
|
+
"composer.panelTitle": "Trae credits",
|
|
2269
|
+
"composer.account": "Account",
|
|
2270
|
+
"composer.generalCredits": "General credits",
|
|
2271
|
+
"composer.lastRefresh": "Last refreshed",
|
|
2272
|
+
"composer.signedOut": "Not signed in",
|
|
2273
|
+
"composer.points": "Trae CN",
|
|
2274
|
+
"composer.refresh": "Refresh credits",
|
|
2275
|
+
"composer.refreshing": "Refreshing credits…",
|
|
2276
|
+
"composer.refreshFailed": "Could not refresh credits",
|
|
1855
2277
|
"row.accountsRefreshed": "Accounts refreshed: {count} found{selected}",
|
|
1856
2278
|
"row.accountsKept": "still bound to",
|
|
1857
2279
|
"row.accountsRefreshedNone": "Accounts refreshed: no usable Trae account found",
|
|
@@ -1955,15 +2377,19 @@ window.__ModuleLoader__.load({
|
|
|
1955
2377
|
"row.modelsTestEnabled": "一键测试启用模型",
|
|
1956
2378
|
"row.modelsTesting": "正在测试 {done}/{total}…",
|
|
1957
2379
|
"row.modelsTestHint": "给每个已启用模型发一条最小消息,看哪些真的能应答。会消耗少量额度。",
|
|
1958
|
-
"row.showPointsInMainUi": "
|
|
1959
|
-
"row.showPointsInMainUiHint": "
|
|
1960
|
-
"
|
|
1961
|
-
"
|
|
1962
|
-
"
|
|
1963
|
-
"
|
|
1964
|
-
"
|
|
1965
|
-
"
|
|
1966
|
-
"
|
|
2380
|
+
"row.showPointsInMainUi": "在输入框工具栏显示积分读数",
|
|
2381
|
+
"row.showPointsInMainUiHint": "在输入框工具栏显示可点击的「Trae CN · 余额」读数,仅当选中的模型属于本插件时出现;点击可查看账号与积分明细。",
|
|
2382
|
+
"composer.switchTo": "切换到 {account}",
|
|
2383
|
+
"composer.noAccounts": "该区域暂无账号",
|
|
2384
|
+
"composer.panelTitle": "Trae 积分",
|
|
2385
|
+
"composer.account": "账号",
|
|
2386
|
+
"composer.generalCredits": "通用积分",
|
|
2387
|
+
"composer.lastRefresh": "最近刷新",
|
|
2388
|
+
"composer.signedOut": "未登录",
|
|
2389
|
+
"composer.points": "Trae CN",
|
|
2390
|
+
"composer.refresh": "刷新积分",
|
|
2391
|
+
"composer.refreshing": "正在刷新积分…",
|
|
2392
|
+
"composer.refreshFailed": "积分刷新失败",
|
|
1967
2393
|
"row.accountsRefreshed": "已刷新账号:找到 {count} 个账号{selected}",
|
|
1968
2394
|
"row.accountsKept": "仍绑定",
|
|
1969
2395
|
"row.accountsRefreshedNone": "已刷新账号:未找到可用的 Trae 账号",
|
|
@@ -2125,31 +2551,37 @@ window.__ModuleLoader__.load({
|
|
|
2125
2551
|
registerCard("plugins.bundle.config", "dsh-connect-trae");
|
|
2126
2552
|
registerCard("plugins.row.config", "dsh-connect-trae#dsh-connect-trae");
|
|
2127
2553
|
/**
|
|
2128
|
-
*
|
|
2129
|
-
*
|
|
2554
|
+
* Credit readout in the composer tool row (`conversation.input.left`, a
|
|
2555
|
+
* session-scope list slot).
|
|
2556
|
+
*
|
|
2557
|
+
* Provider-scoped on purpose: it renders only while the session's selected
|
|
2558
|
+
* model belongs to this plugin. That is what keeps two connector plugins
|
|
2559
|
+
* from competing for one shared row — the sidebar foot, where this line used
|
|
2560
|
+
* to live, is a single root row, and the observed result beside
|
|
2561
|
+
* dsh-connect-workbuddy's two rows was this plugin's line squeezed down to a
|
|
2562
|
+
* clipped "Tr".
|
|
2130
2563
|
*
|
|
2131
|
-
* Registration is unconditional
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
* DOM and no fetch loop.
|
|
2564
|
+
* Registration is unconditional; the gate reads the Host's modelSelection
|
|
2565
|
+
* projection and returns null for every other provider, so nothing is drawn
|
|
2566
|
+
* and no fetch loop starts while another provider is selected.
|
|
2135
2567
|
*/
|
|
2136
|
-
const
|
|
2568
|
+
const registerComposerPoints = () => {
|
|
2137
2569
|
try {
|
|
2138
|
-
ctx.slots.inject("
|
|
2139
|
-
name: "
|
|
2140
|
-
id: "dsh-connect-trae-points",
|
|
2570
|
+
ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
|
|
2571
|
+
name: "conversation.input.left",
|
|
2572
|
+
id: "dsh-connect-trae-composer-points",
|
|
2141
2573
|
order: 4,
|
|
2142
2574
|
locale: namespace,
|
|
2143
2575
|
inject: () => ({
|
|
2144
2576
|
t,
|
|
2145
2577
|
settingsScope
|
|
2146
2578
|
})
|
|
2147
|
-
},
|
|
2579
|
+
}, ComposerPointsGate));
|
|
2148
2580
|
} catch (error) {
|
|
2149
|
-
console.error("[dsh-connect-trae]
|
|
2581
|
+
console.error("[dsh-connect-trae] composer points slot failed to register (host provider unaffected):", error);
|
|
2150
2582
|
}
|
|
2151
2583
|
};
|
|
2152
|
-
|
|
2584
|
+
registerComposerPoints();
|
|
2153
2585
|
} catch (error) {
|
|
2154
2586
|
console.error("[dsh-connect-trae] client card failed to load (host provider unaffected):", error);
|
|
2155
2587
|
}
|