dsh-connect-trae 3.8.1 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -326,6 +326,787 @@ declare class TraeCatalog {
326
326
  set(models: readonly TraeModelInfo[]): void;
327
327
  }
328
328
  //#endregion
329
+ //#region src/status-paths.d.ts
330
+ /** Plugin-owned usage endpoint consumed by its browser half. */
331
+ declare const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
332
+ /**
333
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
334
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
335
+ * (minimal) chat call per model — so it is user-initiated only, and the card
336
+ * labels the button as consuming a small amount.
337
+ */
338
+ declare const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
339
+ /**
340
+ * Per-account status (check-in / credits / subscription) for the account table.
341
+ *
342
+ * Each row of the account table reads its OWN state through this route — a CN
343
+ * row carries its check-in state and balance, an ai row its subscription.
344
+ * POST with the accountId in the body: one upstream read per account, paid
345
+ * only when the table is actually shown.
346
+ */
347
+ declare const TRAE_ACCOUNT_STATUS_PATH = "/plugins/dsh-connect-trae/account-status";
348
+ /**
349
+ * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
350
+ * route in this plugin that changes upstream account state. The accountId in
351
+ * the body names WHICH account to claim for, so the button can sit in each
352
+ * account row.
353
+ */
354
+ declare const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
355
+ /**
356
+ * Outcome vocabulary of one account probe, shared by the Host and the card.
357
+ *
358
+ * Mirrors `TraePoolOutcome` in `./account-pool.ts`, and is DECLARED again here
359
+ * rather than imported for the same reason workbuddy repeats it: this module is
360
+ * the node-free bridge for the browser build target and must not pull in the
361
+ * Host-side pool module (which imports the credential type). The two lists must
362
+ * stay in step — the card renders one label per value, and a value Host can send
363
+ * that the card has no label for renders as the raw token.
364
+ *
365
+ * `plan-gated` is Trae-only (upstream `1005`: this ACCOUNT's plan does not cover
366
+ * the model) and has no workbuddy counterpart.
367
+ */
368
+ type TraeWebPoolOutcome = 'ok' | 'rate-limited' |
369
+ /**
370
+ * The MODEL is loaded, not the account.
371
+ *
372
+ * Kept apart from `rate-limited` because the two need OPPOSITE advice:
373
+ * `rate-limited` is this account's volume wall (wait, or bill another account),
374
+ * while `model-busy` follows the model everywhere — every account hits the same
375
+ * wall, so switching only spends a request per account to learn what the first
376
+ * already said.
377
+ */
378
+ 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'plan-gated' | 'unavailable' | 'not-found' | 'failed';
379
+ /** Why one account is currently out of the pool. */
380
+ type TraeWebPoolExclusion = 'unusable' | 'rate-limited' | 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'plan-gated' | 'not-found' | 'unavailable' | 'failed';
381
+ /**
382
+ * One account's row in the pool, as the card renders it.
383
+ *
384
+ * Ties together the three facts the card shows per account — credits, the last
385
+ * test, and whether it can serve right now — so the ranking the card displays
386
+ * comes from ONE Host answer rather than being re-derived in the browser. The
387
+ * browser re-deriving it is how a card ends up calling an excluded account "in
388
+ * use", which is the inconsistency this document exists to prevent.
389
+ */
390
+ interface TraeWebPoolAccount {
391
+ accountId: string;
392
+ accountName: string;
393
+ /** Total remaining credit, absent when this account's balance read failed. */
394
+ credits?: number;
395
+ /**
396
+ * The GENERAL bucket — the credit this plugin's chat can actually spend.
397
+ *
398
+ * Trae-only, because a Trae balance is TWO buckets and SOLO chat only drains
399
+ * General (measured on the live `available_endpoint` split). A row whose total
400
+ * looks rich but whose General reads zero cannot serve a request, which is the
401
+ * same trap workbuddy hit with its expiry rule: a ranking keyed on a figure that
402
+ * the billing path cannot reach is not a ranking. The card shows both.
403
+ */
404
+ generalAvailable?: number;
405
+ /** The WORK bucket, kept for display: this plugin's chat never spends it. */
406
+ workAvailable?: number;
407
+ /** Credit lapsing inside 3 days. Zero means "measured, nothing at risk". */
408
+ expiringSoon?: number;
409
+ /** When the nearest pack lapses, epoch ms. */
410
+ nearestExpiryMs?: number;
411
+ /**
412
+ * This account's stored access token has lapsed, so its balance cannot be read
413
+ * until it is refreshed or the user signs in again.
414
+ *
415
+ * A reason for the dash, not a second dash: without it "0 credit" and "cannot
416
+ * read the balance" look identical on the card, and only one of them is the
417
+ * user's problem to fix.
418
+ */
419
+ tokenExpired?: boolean;
420
+ /**
421
+ * The last test of this account, absent when never tested.
422
+ *
423
+ * `modelId` lives INSIDE the measurement rather than being assumed from the
424
+ * row's current setting: the setting can change after the test, and a result
425
+ * must keep naming what it actually measured.
426
+ */
427
+ probe?: {
428
+ outcome: TraeWebPoolOutcome;
429
+ atMs: number;
430
+ /**
431
+ * What wrote this measurement, so the card can explain a change the user
432
+ * watched happen: `test-batch` is a deliberate test, `live-request` is a real
433
+ * request that failed and overwrote it. ABSENT for records written before the
434
+ * field existed — the card must then say "unknown", not guess.
435
+ */
436
+ source?: 'test-batch' | 'live-request';
437
+ /** WHICH model this measurement is about; absent = unknown scope. */
438
+ modelId?: string;
439
+ /** Only when the upstream named a time; never invented locally. */
440
+ retryAtMs?: number;
441
+ /** Upstream's own words, already redacted. Absent on success. */
442
+ message?: string;
443
+ };
444
+ /** Set when this account cannot serve right now. */
445
+ excludedBy?: TraeWebPoolExclusion;
446
+ /**
447
+ * Whether this is the account billing traffic for this panel RIGHT NOW.
448
+ *
449
+ * The pool's runtime pick when enabled, else the saved selection. It is the
450
+ * HOST's answer, not the card's guess: a card that derived "in use" from the
451
+ * saved setting would contradict the pool after a rotation.
452
+ */
453
+ current: boolean;
454
+ /**
455
+ * Whether the user checked this account into this panel's pool.
456
+ *
457
+ * Listed rows include UNCHECKED accounts so the card can offer them to check:
458
+ * membership is an explicit opt-in and a user can only opt in to something they
459
+ * can see. Only checked rows can bill or be batched.
460
+ */
461
+ member: boolean;
462
+ /**
463
+ * Whether this machine still holds a usable sign-in for the account.
464
+ *
465
+ * `false` marks a GHOST: checked or listed, but no local credential resolves, so
466
+ * a batch can neither test it nor check it in. It is kept as a row (rather than
467
+ * filtered out) because the only way to stop carrying a broken entry is to see
468
+ * it and remove it — a filtered row would leave the saved list permanently dirty
469
+ * with nothing on screen to explain why the pool runs on fewer accounts than the
470
+ * checkbox count says.
471
+ */
472
+ reachable?: boolean;
473
+ /**
474
+ * Today's check-in state for this account.
475
+ *
476
+ * ABSENT means it was not read — the card must then say so rather than render a
477
+ * definite "not checked in", which would contradict a claim the user just
478
+ * watched succeed.
479
+ */
480
+ checkedInToday?: boolean;
481
+ /**
482
+ * Whether today's check-in is spent on this MACHINE for this account.
483
+ *
484
+ * Trae-only, because Trae keys the daily claim to `x-device-id` (code `9095`).
485
+ * `checkedInToday === false` together with this true is the "switched account"
486
+ * case the check-in card already documents: this device has no claim left.
487
+ */
488
+ deviceCheckedIn?: boolean;
489
+ }
490
+ /**
491
+ * The pool's state for one panel.
492
+ *
493
+ * `targetModelId`/`targetModelSource` exist so the card can state WHICH model a
494
+ * test would use and WHERE that choice came from. `none` is a real, common
495
+ * answer for Trae — the CN static fallback catalog carries no `creditMultiplier`
496
+ * at all until its first live refresh, and "missing multiplier ≠ free" means the
497
+ * card must render "no free model" rather than silently substituting a paid one.
498
+ */
499
+ interface TraeWebPool {
500
+ enabled: boolean;
501
+ /**
502
+ * Whether this panel offers the daily check-in at all.
503
+ *
504
+ * Only `cn` does. The ai gateways answer 404 for the whole `/trae/api/v2/ug/*`
505
+ * family (probed 2026-09-24) and the enterprise panel has no personal reward,
506
+ * so the card hides the action entirely rather than rendering a button whose
507
+ * only outcome is a refusal.
508
+ */
509
+ checkinSupported: boolean;
510
+ /** The model a test would use right now; absent when none can be resolved. */
511
+ targetModelId?: string;
512
+ /**
513
+ * Where the target came from. `'stale'` means the SAVED id is no longer in the
514
+ * catalog — a distinct state from `'none'` (no free model exists), because the
515
+ * two need opposite advice (offer a way back to automatic / say there is no
516
+ * free model at all).
517
+ */
518
+ targetModelSource: 'preferred' | 'free' | 'none' | 'stale';
519
+ /**
520
+ * How much text a pool test sends (0 = the minimal probe). Always a number so
521
+ * the card's select has a value to show; the saved preference is the default.
522
+ */
523
+ probeInputTokens: number;
524
+ /** The saved target id that this panel's catalog no longer offers. */
525
+ staleTargetModelId?: string;
526
+ /** The account ids the user has checked into this panel's pool. */
527
+ memberAccountIds?: readonly string[];
528
+ /**
529
+ * The subset of {@link TraeWebPool.memberAccountIds} that resolves to a real
530
+ * local credential — i.e. what a batch actually runs on.
531
+ *
532
+ * Sent so the card never re-derives it. Counting the raw saved list is what
533
+ * lets a UI claim "1 of 1 selected" and enable both buttons while the Host ran
534
+ * on ZERO accounts and reported success.
535
+ */
536
+ effectiveMemberAccountIds?: readonly string[];
537
+ /**
538
+ * Last measured display name per saved member id, including members that have
539
+ * since stopped resolving.
540
+ *
541
+ * A member whose sign-in disappeared would otherwise be listed as a bare id
542
+ * and an unexplained count, and the user could not tell WHICH one they unchecked.
543
+ */
544
+ memberNames?: Record<string, string>;
545
+ /** Rows in the Host's ranking order: candidates first, excluded rows last. */
546
+ accounts: readonly TraeWebPoolAccount[];
547
+ /**
548
+ * Why the pool could not pick anybody, when it could not.
549
+ *
550
+ * Set when every member is excluded, so the card can explain that traffic is
551
+ * going to the saved selection rather than leaving the user to assume the pool
552
+ * is serving the top row.
553
+ */
554
+ noCandidate?: TraeWebPoolExclusion | 'empty';
555
+ }
556
+ /**
557
+ * The pool's saved preferences for one panel, as stored in `regions[panel].pool`.
558
+ *
559
+ * Declared in the node-free bridge because BOTH halves touch it: the Host reads
560
+ * it to decide membership and ranking, the card drafts and saves it. Keeping one
561
+ * declaration is what stops the card from writing a field the Host ignores (or
562
+ * vice versa) — the failure workbuddy hit when its client and Host each had a
563
+ * private copy of this shape.
564
+ *
565
+ * Every field is optional because this is a user-edited draft: a partially filled
566
+ * slot must still load, and a missing `enabled` means OFF rather than ON.
567
+ */
568
+ interface TraePoolPreferences {
569
+ /** Whether this panel bills through the pool at all. Default OFF. */
570
+ enabled?: boolean;
571
+ /**
572
+ * The model a test batch probes. Absent or `''` means "automatic": pick the
573
+ * free model (see `pickFreeModel` in `./account-pool.ts`). An id this panel no
574
+ * longer offers is PRESERVED so the user's choice survives a catalog blip, and
575
+ * reported as `stale` rather than silently replaced.
576
+ */
577
+ targetModelId?: string;
578
+ /**
579
+ * The accounts the user checked into this panel's pool, in the order they were
580
+ * checked.
581
+ *
582
+ * Explicit opt-in with NO default: an account never joins the pool because it
583
+ * exists. Order is only a display hint — the ranking is the Host's.
584
+ *
585
+ * A MUTABLE array, not `readonly`: this interface is also the shape the
586
+ * settings schema declares (`z.array(z.string())`), and the schema rebuilds the
587
+ * config document on every save — a readonly type there is a compile-time
588
+ * mismatch that would take the whole Config schema down. Readers still treat it
589
+ * as immutable; only the owner of a draft copies it.
590
+ */
591
+ memberAccountIds?: string[];
592
+ /**
593
+ * How much text a pool TEST sends, in tokens. `0` (the default) keeps the
594
+ * minimal one-word probe.
595
+ *
596
+ * Exists because a verdict earned by a one-word request says nothing about the
597
+ * long turn the user is about to send: an upstream that refuses large requests
598
+ * answers the small one happily. Padding the probe therefore buys a truthful
599
+ * "works at my size" answer at the cost of spending real credit per test.
600
+ */
601
+ probeInputTokens?: number;
602
+ }
603
+ /** Query parameter naming the region a card request addresses. */
604
+ declare const TRAE_REGION_PARAM = "region";
605
+ /** Every region, in card tab order. */
606
+ declare const TRAE_REGIONS: readonly TraeRegion[];
607
+ /**
608
+ * Address one region's status route. The two regions are separate provider
609
+ * stacks; every card request carries the region whose tab the user is on.
610
+ */
611
+ declare function withTraeRegion(path: string, panel: TraePanel): string;
612
+ /**
613
+ * Read the region parameter off a status-route URL. Absent means the domestic
614
+ * tab (`cn`); a present-but-unknown value returns undefined so the route can
615
+ * answer 400 instead of guessing.
616
+ */
617
+ declare function regionOfTraeStatusUrl(url: string): TraePanel | undefined;
618
+ /** One credit pack and its remaining credit. */
619
+ interface TraeWebCreditAccount {
620
+ displayDesc: string;
621
+ remain: number;
622
+ size: number;
623
+ /**
624
+ * When this pack lapses, in epoch milliseconds, taken from the upstream's own
625
+ * `end_time`. Absent when the upstream reported no usable time (0 / missing),
626
+ * so a consumer can tell "no date" apart from "1970".
627
+ */
628
+ expiresAtMs?: number;
629
+ /**
630
+ * Raw upstream `available_endpoint`: which bucket this pack lives in.
631
+ *
632
+ * Measured on the live response — `0` is the GENERAL bucket that SOLO chat
633
+ * actually spends, `1` is the WORK bucket it cannot reach. The projection
634
+ * dropped this field until the account pool needed it: the pool ranks accounts
635
+ * by what they can SERVE, and credit lapsing in the WORK bucket is not a
636
+ * reason to bill that account first, because nothing here can ever spend it.
637
+ * Absent, not guessed, when the upstream omits it.
638
+ */
639
+ endpoint?: number;
640
+ }
641
+ /** Credit lapsing inside one horizon, as the card reports it. */
642
+ interface TraeWebExpiringCredits {
643
+ /** Sum of the contributing packs' remaining credit. */
644
+ total: number;
645
+ /** How many packs contribute. */
646
+ packs: number;
647
+ /** Soonest expiry among them, epoch milliseconds. */
648
+ earliestExpiresAtMs?: number;
649
+ }
650
+ /**
651
+ * Credit at risk, bucketed by how soon it lapses.
652
+ *
653
+ * Only present when at least one pack carried a usable `expiresAtMs` — a card
654
+ * must never read "we were not told when anything expires" as "nothing is
655
+ * about to lapse".
656
+ */
657
+ interface TraeWebCreditExpiry {
658
+ within24h: TraeWebExpiringCredits;
659
+ within3d: TraeWebExpiringCredits;
660
+ within7d: TraeWebExpiringCredits;
661
+ /** Packs already past their end time but still carrying credit. */
662
+ expired: TraeWebExpiringCredits;
663
+ }
664
+ /** Aggregated usage/credit answer rendered by the plugin card. */
665
+ interface TraeWebCredits {
666
+ total: number;
667
+ consumed: number;
668
+ available: number;
669
+ workAvailable: number;
670
+ generalAvailable: number;
671
+ accounts: readonly TraeWebCreditAccount[];
672
+ /**
673
+ * Credit lapsing soon, for the card's warning banner.
674
+ *
675
+ * Derived from the per-pack {@link TraeWebCreditAccount.expiresAtMs} the
676
+ * `/usage` route already carries, so it costs no extra upstream read.
677
+ */
678
+ expiry?: TraeWebCreditExpiry;
679
+ }
680
+ /** Editable Trae model row rendered by the plugin-owned settings card. */
681
+ interface TraeWebModel {
682
+ id: string;
683
+ name: string;
684
+ contextWindow?: number;
685
+ maxTokens?: number;
686
+ input?: ('text' | 'image')[];
687
+ creditMultiplier?: number;
688
+ reasoningSupported?: boolean;
689
+ reasoning?: {
690
+ supported: string[];
691
+ defaultEffort?: string;
692
+ };
693
+ maxContextWindow?: number;
694
+ /**
695
+ * Trae's own image-capability flag, as the runtime catalog carries it. The
696
+ * card renders these rows with their image checkbox already ticked so the
697
+ * control matches what DSH is actually told (issue #16); absent means the
698
+ * directory did not declare it, which is not a capability claim either way.
699
+ */
700
+ multimodal?: boolean;
701
+ /**
702
+ * The availability verdict from the one-click test, when this card session
703
+ * has run one. Absent = not tested. Deliberately NOT persisted with the
704
+ * settings: it describes one moment on one account, and a stale verdict shown
705
+ * as current is worse than a blank field.
706
+ */
707
+ probe?: TraeModelProbeVerdict;
708
+ /** Upstream's own words for the verdict, when it gave any. */
709
+ probeDetail?: string;
710
+ }
711
+ /**
712
+ * One model's availability verdict from the one-click test.
713
+ *
714
+ * `unknown` is a first-class outcome, not a failure: a timeout, a proxy drop or
715
+ * a throttle says nothing about the model, and reporting it as unusable would
716
+ * make the user delete a model that works. Only an upstream refusal is negative.
717
+ */
718
+ type TraeModelProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
719
+ /** What the probe route answers with. */
720
+ interface TraeModelsTestResponse {
721
+ /** One entry per model tested, in the order they were sent. */
722
+ results: {
723
+ id: string;
724
+ verdict: TraeModelProbeVerdict;
725
+ detail: string;
726
+ }[];
727
+ /** Models skipped because the region has no live catalog row for them. */
728
+ skipped?: string[];
729
+ }
730
+ /**
731
+ * Daily check-in state rendered below the credit stats.
732
+ *
733
+ * The two booleans answer DIFFERENT questions and must never be collapsed into
734
+ * one "done" flag — doing that was a shipped bug (see CHANGELOG 2.3.1):
735
+ *
736
+ * - `checkedIn` is ACCOUNT-level: today's reward pack exists for this account,
737
+ * so there is nothing left to claim and the balance already includes it.
738
+ * - `didCheckedIn` is DEVICE-level: this MACHINE already spent its one
739
+ * check-in for the day, whichever account spent it. The upstream keys it on
740
+ * the `x-device-id` in the request and answers it per device, not per
741
+ * account (measured 2026-09-26: the same account and Beijing day answered
742
+ * `false` with the header omitted, with a synthetic id, and with another
743
+ * install's id, and `true` for the device that actually claimed).
744
+ *
745
+ * So `didCheckedIn && !checkedIn` is the "switched account" case: the device's
746
+ * daily check-in is used up, but THIS account was never rewarded. Claiming
747
+ * again is refused upstream (business code 9095, "该设备今日已参与签到"), so
748
+ * the card disables the button — but it must say that, not "claimed today".
749
+ */
750
+ interface TraeWebCheckin {
751
+ checkedIn: boolean;
752
+ didCheckedIn: boolean;
753
+ credits: number;
754
+ enabled: boolean;
755
+ extraCredits?: number;
756
+ }
757
+ /** The claim route's answer, so the card can report a business refusal verbatim. */
758
+ interface TraeWebCheckinClaim {
759
+ /** False when the upstream answered a non-zero business code (e.g. 9004). */
760
+ claimed: boolean;
761
+ /**
762
+ * True only when THIS ACCOUNT's reward for today already exists, so nothing
763
+ * was sent upstream. Distinct from {@link TraeWebCheckinClaim.deviceCheckedIn}:
764
+ * a device that already claimed for another account leaves this false.
765
+ */
766
+ alreadyCheckedIn: boolean;
767
+ /**
768
+ * True when today's check-in was already spent on this DEVICE (by this or
769
+ * another account), so no claim was sent or the upstream refused it with
770
+ * 9095. Not an error: it is the truthful answer to "can this account claim
771
+ * here today?" — no, and the reason is the machine, not the account.
772
+ */
773
+ deviceCheckedIn?: boolean;
774
+ code?: number;
775
+ message?: string;
776
+ /** The refreshed check-in state, present whenever the claim path ran. */
777
+ checkin?: TraeWebCheckin;
778
+ }
779
+ /** One reward activity rule (name + work/general credits when present). */
780
+ interface TraeWebActivity {
781
+ activityId: string;
782
+ enabled: boolean;
783
+ workCredits?: number;
784
+ generalCredits?: number;
785
+ }
786
+ interface TraeWebAccount {
787
+ id: string;
788
+ accountName: string;
789
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
790
+ /**
791
+ * Whether this account belongs to a Trae tenant, i.e. may bill the enterprise
792
+ * pool. The enterprise tab lists only these, because the tenant gateway
793
+ * serves a directory to any CN token but refuses a personal account's chat
794
+ * with `4019` — so nothing else reveals the difference until a turn fails.
795
+ */
796
+ enterprise: boolean;
797
+ /** Routing bucket of this account, derived from its credential. */
798
+ region: TraeRegion;
799
+ source: 'desktop' | 'dsh' | 'cli';
800
+ tokenExpiresAtMs: number;
801
+ /**
802
+ * When this account's REFRESH token expires, when the credential carries one.
803
+ *
804
+ * The access token above is renewed automatically — the plugin refreshes it
805
+ * five minutes before it lapses — so by itself it answers "when does this
806
+ * need attention", which is "never, while the refresh token lives". The
807
+ * refresh token is the real horizon: once IT expires the automatic renewal
808
+ * stops working and the user must sign in to Trae again. Before this field the
809
+ * card could only say "auto-renewal", which stays true right up to the moment
810
+ * it silently stops being true.
811
+ */
812
+ refreshExpiresAtMs?: number;
813
+ selected: boolean;
814
+ }
815
+ /**
816
+ * The account the Trae desktop app is signed in to right now, when it differs
817
+ * from the one this plugin is bound to (issue #29).
818
+ *
819
+ * Reported on the usage document so the card can offer one-click follow without
820
+ * an extra round trip; also written back on a POST to the accounts refresh
821
+ * route when the client confirms it saw the hint. Absent whenever the two
822
+ * agree or the app is not readable — a healthy document carries no extra
823
+ * fields and the plugin never bills from this value directly: the FOLLOW is a
824
+ * settings write of `accounts.<region>`, made by the card or by the host's
825
+ * follow switch, never a silent rebind here.
826
+ */
827
+ interface TraeWebAppLogin {
828
+ id: string;
829
+ accountName: string;
830
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
831
+ region: TraeRegion;
832
+ }
833
+ /**
834
+ * The per-account state shown inside an account row of the card's table.
835
+ *
836
+ * Every locally detected account in a region is rendered as a row; the
837
+ * interesting per-account facts differ by region, which is why this is a union
838
+ * of the three surfaces the card previously showed only for the BOUND account:
839
+ *
840
+ * - CN rows carry their own check-in state (Trae keys the daily reward per
841
+ * ACCOUNT, and the one-per-device tally per DEVICE, see {@link TraeWebCheckin})
842
+ * and their Work/General balances.
843
+ * - ai rows carry their own subscription / trial status.
844
+ *
845
+ * Each field is optional: a read that fails keeps the row visible (perhaps
846
+ * without a figure) rather than dropping the account and its switch.
847
+ */
848
+ interface TraeWebAccountState {
849
+ /** The account this state belongs to. */
850
+ id: string;
851
+ /** CN: today's check-in state of this account (absent for ai rows). */
852
+ checkin?: TraeWebCheckin;
853
+ /** CN: this account's Work/General balances (absent for ai rows). */
854
+ credits?: TraeWebCredits;
855
+ /** ai: this account's subscription / trial status (absent for CN rows). */
856
+ payStatus?: TraeWebPayStatus;
857
+ }
858
+ interface TraeWebCreditAlternative {
859
+ id: string;
860
+ accountName: string;
861
+ edition: TraeWebAccount['edition'];
862
+ workAvailable: number;
863
+ generalAvailable: number;
864
+ }
865
+ /** One probed candidate path and why it did not yield an account. */
866
+ interface TraeWebSearchPath {
867
+ path: string;
868
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
869
+ source: 'desktop' | 'cli';
870
+ reason: 'missing' | 'unreadable' | 'invalid';
871
+ message?: string;
872
+ }
873
+ /**
874
+ * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
875
+ * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
876
+ */
877
+ declare function unwrapVolatile<T>(value: T): T;
878
+ /**
879
+ * Deep copy of a value with every `{get(): T}` live reference replaced by the
880
+ * value it resolves to.
881
+ *
882
+ * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
883
+ * reference is still `typeof === 'object'` — so it passes a naive object check
884
+ * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
885
+ * does NOT read the field: it produces `{ get: <function> }`. Any merge that
886
+ * spreads a resolved field to preserve its siblings — the card's "write one
887
+ * region, keep the other" write, or the account map's spread — would otherwise
888
+ * DROP every sibling and leak a function into the document.
889
+ *
890
+ * Both halves therefore unwrap before touching a resolved field. This lives in
891
+ * the node-free host↔client bridge because the browser half needs it too.
892
+ *
893
+ * Non-reference values are recursed into so a nested volatile field is caught
894
+ * as well; arrays and objects are rebuilt rather than mutated, so the caller's
895
+ * value is never touched.
896
+ */
897
+ declare function unwrapVolatileDeep<T>(value: T): T;
898
+ /**
899
+ * Read the per-region account selections out of a settings value.
900
+ *
901
+ * SHARED because `accounts` now has TWO writers: the card, and the composer
902
+ * panel's account table (2.14.0). Two writers of one field must use one merge
903
+ * rule, or they drift — and the rule is not obvious.
904
+ *
905
+ * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
906
+ * live reference, which passes the `typeof === 'object'` check and would be
907
+ * returned as if it were the map. Every lookup on it is then `undefined`, and
908
+ * the merge (`{ ...configured, [region]: id }`) would spread a reference into
909
+ * `{get: <function>}` — dropping the other region's selection and leaking a
910
+ * function into the settings document.
911
+ */
912
+ declare function configuredAccountsOf(configured: unknown): Record<string, string>;
913
+ /**
914
+ * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
915
+ * #29). Absent reads as OFF, so an existing configuration keeps the strict
916
+ * bind it shipped with and the switch is an opt-in — the same opt-out
917
+ * semantics as {@link regionEnabledOf}, for the same reason: a config written
918
+ * before this field existed cannot ask for behavior it never declared.
919
+ *
920
+ * Reads the whole settings section (or the bare field); deep-unwraps first
921
+ * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
922
+ * reference, whose `typeof === 'object'` would otherwise pass a naive check.
923
+ */
924
+ declare function followAppLoginEnabled(configured: unknown): boolean;
925
+ /**
926
+ * Build the next `regions` settings value for the card's save. The write
927
+ * targets ONLY the signed-in account's region slot; every other region's slot
928
+ * is carried over untouched, so switching accounts never clobbers the other
929
+ * region's picks. Tolerates any stored shape (absent, non-object) by starting
930
+ * from an empty document.
931
+ */
932
+ declare function nextRegionSlots<Slot extends object>(regions: unknown, panel: TraePanel, slot: Slot): Record<string, unknown>;
933
+ /**
934
+ * Whether one region's provider is switched on. Opt-out semantics: only an
935
+ * explicit `false` disables it, so a config written before this switch existed
936
+ * (and the pre-region-split flat fields, which never carry `enabled`) keep both
937
+ * providers running exactly as before. The Host reads the same rule through
938
+ * `regionStateOf`, so card and Host can never disagree about a region's state.
939
+ *
940
+ * `value` may be the whole settings section or the `regions` map (see
941
+ * {@link regionsMapOf}).
942
+ */
943
+ declare function regionEnabledOf(value: unknown, panel: TraePanel): boolean;
944
+ /**
945
+ * Build the next `regions` settings value for a provider on/off toggle. ONLY
946
+ * the target region's `enabled` flag changes: every other field of that slot
947
+ * (its directory, selection, image opt-ins, context budgets) and every other
948
+ * region's slot are carried over verbatim, so switching a provider off never
949
+ * discards the user's model picks and switching it back on restores them.
950
+ *
951
+ * This is deliberately separate from {@link nextRegionSlots}: that helper
952
+ * writes a whole slot from a signed-in tab's draft, while this one must work
953
+ * for a region that is signed OUT — which is precisely the region a user wants
954
+ * to switch off (no international install, no international account).
955
+ *
956
+ * `value` may be the whole settings section or the `regions` map; the RETURN
957
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
958
+ * 'regions', ...)` needs.
959
+ */
960
+ declare function nextRegionEnabled(value: unknown, panel: TraePanel, enabled: boolean): Record<string, unknown>;
961
+ /**
962
+ * The two seats this region's credit readout can occupy.
963
+ *
964
+ * `composer` is the composer tool row (`conversation.input.left`); `stats` is
965
+ * the ambient dock below the composer card (`conversation.composer.dock`),
966
+ * beside the shell's own round/step/token pills.
967
+ *
968
+ * WHY THE SEAT IS A CHOICE AT ALL (learned from dsh-connect-workbuddy issue
969
+ * #62): the composer tool row is a LIST seat, so it is shared not only with the
970
+ * shell's own controls but with any other plugin that registers there — and a
971
+ * list seat cannot promise that every occupant is visible. The report behind
972
+ * that issue is exactly that: with a third-party plugin installed the readout
973
+ * stopped appearing. A fixed seat therefore lets one plugin's presence decide
974
+ * another's, so the seat belongs to the user.
975
+ *
976
+ * Named as a union rather than a boolean because a third seat is plausible and
977
+ * a `showAtTop: boolean` would have to be MIGRATED rather than extended.
978
+ */
979
+ type TraeCreditPosition = 'composer' | 'stats';
980
+ /**
981
+ * Which seat one region's readout uses, defaulting to the composer row.
982
+ *
983
+ * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
984
+ * the two cases that matter: a profile written BEFORE this setting existed (the
985
+ * composer row was the only seat, so an upgrade moves nothing on screen), and a
986
+ * hand-edited value the schema does not know. Falling back rather than
987
+ * rendering nowhere is deliberate — an unrecognized value must not make the
988
+ * readout disappear, which is the very defect this setting exists to fix.
989
+ *
990
+ * `value` may be the whole settings section or the `regions` map (see
991
+ * {@link regionsMapOf}).
992
+ */
993
+ declare function regionCreditPositionOf(value: unknown, panel: TraePanel): TraeCreditPosition;
994
+ /**
995
+ * Build the next `regions` settings value for one region's credit seat.
996
+ *
997
+ * Same merge rule as every sibling region write: only the target region's
998
+ * `creditPosition` changes, so a seat change cannot drop that slot's model
999
+ * directory, selection, budgets or enabled flag, nor touch the other region.
1000
+ *
1001
+ * `value` may be the whole settings section or the `regions` map; the RETURN
1002
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1003
+ * 'regions', ...)` needs.
1004
+ */
1005
+ declare function nextRegionCreditPosition(value: unknown, panel: TraePanel, position: TraeCreditPosition): Record<string, unknown>;
1006
+ /** Subscription status of an international (ai) account, rendered instead of the CN credit packs. */
1007
+ interface TraeWebPayStatus {
1008
+ isDollarUsageBilling: boolean;
1009
+ hasPackage: boolean;
1010
+ isPayFreshman: boolean;
1011
+ inTrial: boolean;
1012
+ trialEndTimeMs: number;
1013
+ enableSoloLite: boolean;
1014
+ enableSoloBuilder: boolean;
1015
+ enableSoloCoder: boolean;
1016
+ enableSoloWeb: boolean;
1017
+ /** Trae's authoritative tier number (`user_pay_identity`); `0` means Free. */
1018
+ payIdentity?: number;
1019
+ /** Trae's authoritative tier label (`user_pay_identity_str`), e.g. `Free`. */
1020
+ payIdentityStr?: string;
1021
+ fission?: {
1022
+ startTimeMs: number;
1023
+ expireTimeMs: number;
1024
+ maxUsage: number;
1025
+ };
1026
+ }
1027
+ /** The JSON document the plugin card renders. */
1028
+ type TraeWebUsage = {
1029
+ status: 'signed-out';
1030
+ accounts: readonly TraeWebAccount[];
1031
+ message?: string;
1032
+ searched?: readonly TraeWebSearchPath[];
1033
+ enabled?: boolean;
1034
+ } | {
1035
+ status: 'signed-in';
1036
+ accountId: string;
1037
+ accountName: string;
1038
+ tokenExpiresAtMs: number;
1039
+ /**
1040
+ * When the BOUND account's refresh token expires (absent when the credential
1041
+ * carries none). See {@link TraeWebAccount.refreshExpiresAtMs} — this is the
1042
+ * horizon that actually matters, because the access token above renews
1043
+ * itself until this one lapses.
1044
+ */
1045
+ refreshExpiresAtMs?: number;
1046
+ /** Which per-region model directory and selection this account owns. */
1047
+ region: TraeRegion;
1048
+ /** Whether this region's provider is currently offered to DSH. */
1049
+ enabled?: boolean;
1050
+ accounts: readonly TraeWebAccount[];
1051
+ models: readonly TraeWebModel[];
1052
+ enabledModelIds: readonly string[];
1053
+ rawChat?: {
1054
+ state: 'disabled' | 'unchecked' | 'available' | 'protocol-gated' | 'authentication' | 'credit' | 'rate' | 'transport' | 'server';
1055
+ checkedAtMs?: number;
1056
+ status?: number;
1057
+ };
1058
+ credits?: TraeWebCredits;
1059
+ creditsError?: string;
1060
+ /**
1061
+ * Set when this panel has no readable balance by design, rather than by
1062
+ * failure. Only `enterprise` does: a tenant's spend is not reported by any
1063
+ * endpoint the plugin can reach (every tenant quota path answers 404), so
1064
+ * the figure exists solely in the enterprise console. The card must say so
1065
+ * instead of rendering the consumer packs, which belong to the personal
1066
+ * balance and would be read as the tenant's.
1067
+ */
1068
+ creditsUnavailable?: 'enterprise';
1069
+ /**
1070
+ * Present only when the bound account is out of credit AND another account
1071
+ * on this machine has some (issue #25). The card then offers a one-click
1072
+ * switch instead of leaving a permanently empty panel. Absent in every
1073
+ * other case so a healthy account never pays for the extra read.
1074
+ */
1075
+ alternatives?: readonly TraeWebCreditAlternative[];
1076
+ /**
1077
+ * Present only when the Trae app's CURRENT sign-in differs from the
1078
+ * account this plugin is bound to (issue #29): the card then offers a
1079
+ * one-click follow instead of making the user hunt the account dropdown
1080
+ * after every app-side sign-out. Absent in every other case — a healthy
1081
+ * document costs no extra read beyond the accounts scan the document
1082
+ * already runs.
1083
+ */
1084
+ appLogin?: TraeWebAppLogin;
1085
+ /**
1086
+ * Daily check-in state (CN only). Absent on the international region,
1087
+ * whose check-in surface does not exist — probed 2026-09-24: the
1088
+ * `/trae/api/v2/ug/*` family answers 404 on every ai gateway, so the card
1089
+ * shows no claim button there rather than a button that cannot work.
1090
+ */
1091
+ checkin?: TraeWebCheckin;
1092
+ checkinError?: string;
1093
+ /** Subscription status of an international account (region ai only). */
1094
+ payStatus?: TraeWebPayStatus;
1095
+ payStatusError?: string;
1096
+ /**
1097
+ * This panel's account-pool state, when the Host build supports it.
1098
+ *
1099
+ * Absent on a Host without pool support, so the card renders no pool section
1100
+ * instead of an empty one. Assembled LAST in the status document and never
1101
+ * allowed to fail it: the pool reads per-account credit, and one dead account
1102
+ * must not blank out the panel the user is looking at.
1103
+ */
1104
+ pool?: TraeWebPool;
1105
+ } | {
1106
+ status: 'error';
1107
+ message: string;
1108
+ };
1109
+ //#endregion
329
1110
  //#region src/pi-ai-runtime.d.ts
330
1111
  /**
331
1112
  * Which `@earendil-works/pi-ai` copy THIS plugin resolves at runtime, and which
@@ -474,6 +1255,47 @@ interface TraeShimRoute {
474
1255
  /** Stable label used to look the route's secret up (`solo` | `enterprise`). */
475
1256
  label: string;
476
1257
  client: TraeUpstreamClient;
1258
+ /**
1259
+ * Account-pool hooks for this route (issue #44). Absent means "one fixed
1260
+ * account, no rotation and no failover" — the behaviour before the pool
1261
+ * existed, and still the behaviour when the pool is switched off.
1262
+ */
1263
+ pool?: TraeShimPool;
1264
+ }
1265
+ /**
1266
+ * What an account pool tells the shim around one chat attempt.
1267
+ *
1268
+ * The shim owns the HTTP surface and the pool owns "who bills", so the two meet
1269
+ * here: the pool may re-point the next request, hand back a client for a
1270
+ * different account after a failure, and hear about failures that only a real
1271
+ * request — not a probe — can produce.
1272
+ *
1273
+ * Every method must resolve rather than throw: a pool failure must never turn a
1274
+ * chat request into a 500 when a plain answer was possible.
1275
+ */
1276
+ interface TraeShimPool {
1277
+ /**
1278
+ * Choose the account for this request and report which one it is. Called once,
1279
+ * before the first attempt; `undefined` means "the pool chose nobody", which
1280
+ * leaves the route's own client to bill the bound account.
1281
+ */
1282
+ prepare(): Promise<string | undefined>;
1283
+ /**
1284
+ * The client for the next account worth trying after the current one failed,
1285
+ * or `undefined` when the pool has nothing left to try.
1286
+ *
1287
+ * The returned client must be bound to that account's OWN credential:
1288
+ * failing over is not a rebind, so it must not move the account the user
1289
+ * selected (the same rule per-account probes already follow).
1290
+ */
1291
+ next(tried: readonly string[]): Promise<{
1292
+ accountId: string;
1293
+ client: TraeUpstreamClient;
1294
+ } | undefined>;
1295
+ /** Record a real request's failure against the account that hit it. */
1296
+ failed(accountId: string, kind: TraeUpstreamErrorKind, message: string): Promise<void>;
1297
+ /** One clause naming why no other account can serve, for the error text. */
1298
+ shortfall(tried: readonly string[]): Promise<string | undefined>;
477
1299
  }
478
1300
  interface TraeShimOptions {
479
1301
  catalog: TraeCatalog;
@@ -801,6 +1623,37 @@ declare class TraeCredentialStore {
801
1623
  private readonly refresh;
802
1624
  private readonly refreshMarginMs;
803
1625
  private inflight;
1626
+ /**
1627
+ * In-flight refreshes of individual accounts, one per account id.
1628
+ *
1629
+ * The card reads every member's balance in one poll and each balance can
1630
+ * decide to refresh, so without this dedupe the same account would exchange
1631
+ * its token several times in a second and the last writer would win over a
1632
+ * copy another read already holds.
1633
+ */
1634
+ private readonly inflightByAccount;
1635
+ /**
1636
+ * When a per-account refresh last FAILED, so a dead refresh token is not
1637
+ * exchanged again on every poll.
1638
+ *
1639
+ * The card reads every member's balance on each refresh, and a revoked refresh
1640
+ * token fails forever: without this memo each poll would spend two upstream
1641
+ * calls (the current contract and the superseded one) on an account no retry
1642
+ * can fix. Five minutes keeps a transient network failure cheap to retry while
1643
+ * a revoked sign-in costs almost nothing.
1644
+ */
1645
+ private readonly refreshFailedAtMs;
1646
+ /**
1647
+ * The account the NEXT request should bill, when a pool — not the user's
1648
+ * binding — is choosing (issue #44).
1649
+ *
1650
+ * Runtime only, never persisted: the binding in settings is what a save
1651
+ * writes and what the card's account table reports as selected, while this is
1652
+ * "who serves right now". Keeping them apart is what lets the pool rotate
1653
+ * without silently rewriting the user's choice, and what makes turning the
1654
+ * pool off restore the bound account with no rebind at all.
1655
+ */
1656
+ private rotatedAccountId;
804
1657
  constructor(options: TraeCredentialStoreOptions);
805
1658
  /** Whether a credential's own claim belongs to this store's region. */
806
1659
  private matchesRegion;
@@ -903,6 +1756,31 @@ declare class TraeCredentialStore {
903
1756
  * their back.
904
1757
  */
905
1758
  private inUse;
1759
+ /**
1760
+ * Point this store at one specific account for the NEXT request, without
1761
+ * touching the user's saved binding (issue #44).
1762
+ *
1763
+ * Called by the account pool before each chat request. `undefined` clears the
1764
+ * override, which is how switching the pool off immediately restores the
1765
+ * binding — no rebind, no settings write, nothing to undo.
1766
+ *
1767
+ * A rotation is a REQUEST, not an assertion: `current()` honours it only when
1768
+ * the account still exists locally and this store's own eligibility rules
1769
+ * allow it, and otherwise falls back to the binding. So a rotation can never
1770
+ * invent a credential that {@link current} would refuse.
1771
+ */
1772
+ setRotatedAccount(accountId: string | undefined): void;
1773
+ /** The rotation currently in effect, for diagnostics and the pool's own card. */
1774
+ rotatedAccount(): string | undefined;
1775
+ /**
1776
+ * The credential a rotation points at, or `undefined` when it cannot be
1777
+ * honoured (the login was removed from this machine, or a tenant-scoped store
1778
+ * refuses a personal account).
1779
+ *
1780
+ * Never substitutes a different account here: the caller then applies the
1781
+ * user's binding, which is the honest answer when the pool's choice is gone.
1782
+ */
1783
+ private rotated;
906
1784
  current(): Promise<TraeCredential | undefined>;
907
1785
  /**
908
1786
  * The stored credential of one specific account, without changing which
@@ -919,7 +1797,23 @@ declare class TraeCredentialStore {
919
1797
  * `accountId` is left untouched. An unknown id yields undefined rather than a
920
1798
  * fallback, for the same reason {@link current} refuses to fall back.
921
1799
  */
922
- credentialOf(accountId: string): Promise<TraeCredential | undefined>;
1800
+ credentialOf(accountId: string): Promise<TraeCredential | undefined>;
1801
+ /**
1802
+ * A SPECIFIC account's credential, refreshed when its access token is due.
1803
+ *
1804
+ * {@link credentialOf} is a pure lookup and hands back an expired token as-is,
1805
+ * which is right for the chat path (that request fails honestly) but wrong for
1806
+ * a READ of that account's own data: Trae's access token lives for days while
1807
+ * the refresh token lives for months, so a pooled account the user has not
1808
+ * used this week reads as "no balance" — the card showed a bare `—` for an
1809
+ * account whose refresh token was still good. Only the refresh margin decides
1810
+ * here, and a FAILED refresh still yields the stored credential so the caller
1811
+ * reports the failure it actually hit instead of inventing "not signed in".
1812
+ *
1813
+ * This never touches the binding or the rotation: refreshing account B must
1814
+ * not make B the account the next chat request bills.
1815
+ */
1816
+ resolveCredentialOf(accountId: string): Promise<TraeCredential | undefined>;
923
1817
  /**
924
1818
  * Store imported credentials (issue #37), replacing any entry with the same
925
1819
  * account id and leaving every other entry alone.
@@ -1957,605 +2851,323 @@ interface TraeCheckinClaim {
1957
2851
  interface TraeActivityRule {
1958
2852
  activityId: string;
1959
2853
  enabled: boolean;
1960
- activityType: number;
1961
- startTimeMs: number;
1962
- endTimeMs: number;
1963
- workExtra?: Record<string, unknown>;
1964
- }
1965
- interface TraeUsageView {
1966
- snapshot: TraeUsageSnapshot;
1967
- checkin: TraeCheckinStatus;
1968
- activities: TraeActivityRule[];
1969
- }
1970
- /**
1971
- * A client for the verified Trae usage/credits endpoints.
1972
- *
1973
- * Every method is read-only except {@link TraeUsageClient.claimCheckin}, which
1974
- * claims the daily check-in reward — the one deliberate mutation, requested by
1975
- * the user from the card and guarded before it is sent.
1976
- */
1977
- declare class TraeUsageClient {
1978
- private readonly options;
1979
- private readonly fetchImpl;
1980
- private readonly baseUrl;
1981
- private readonly timeoutMs;
1982
- constructor(options: TraeUsageOptions);
1983
- /** Region of the current credential; `cn` when unresolvable. */
1984
- private currentRegion;
1985
- /**
1986
- * Pay base for the current credential's region. An explicit baseUrl (tests,
1987
- * diagnostics) pins the endpoint; otherwise CN uses `api.trae.cn` and the
1988
- * international region its own verified pay gateway.
1989
- */
1990
- private payBase;
1991
- /**
1992
- * Device id for the check-in routes, or undefined when the machine's
1993
- * installation identity cannot be read. Best-effort on purpose: a missing
1994
- * identity must not break the read-only status query, which the upstream
1995
- * answers with or without the header.
1996
- */
1997
- private deviceIdHeader;
1998
- private authedHeaders;
1999
- private post;
2000
- /**
2001
- * Subscription/pay status of an international (ai) account. The CN region
2002
- * never calls this (its contract is unverified there); the ai region uses
2003
- * this instead of the Work-credit `snapshot`.
2004
- */
2005
- payStatus(signal?: AbortSignal): Promise<TraePayStatus>;
2006
- /**
2007
- * The Work-credit endpoints are CN-only. The international region is
2008
- * subscription-based and must read {@link payStatus} instead; guarding here
2009
- * keeps the card's degraded `creditsError` message diagnosable rather than
2010
- * letting an ai credential hit an unverified path on its pay gateway.
2011
- */
2012
- private requireCnRegion;
2013
- /** Total entitlements / credits and per-pack breakdown. */
2014
- snapshot(signal?: AbortSignal): Promise<TraeUsageSnapshot>;
2015
- /**
2016
- * Daily check-in status. Read-only, and answered with or without the device
2017
- * header — the claim is what needs it.
2018
- *
2019
- * The read DOES carry the header whenever this installation has one, and
2020
- * that is deliberate rather than incidental: `did_checked_in` is answered
2021
- * per device, so omitting the header would make a device that already claimed
2022
- * today look identical to one that never has (measured 2026-09-26, see
2023
- * {@link TraeCheckinStatus.didCheckedIn}).
2024
- */
2025
- checkinStatus(signal?: AbortSignal): Promise<TraeCheckinStatus>;
2026
- /**
2027
- * Claim today's check-in reward. The ONLY state-changing call in this client.
2028
- *
2029
- * The upstream is idempotent per Beijing day (verified 2026-09-24: repeating
2030
- * the call on an already-claimed day answers `code: 0` while the entitlement
2031
- * total stays byte-identical), so a double click cannot double-grant. The
2032
- * card and its route still guard, because "cannot double-grant" is a property
2033
- * of the upstream we verify rather than one we rely on.
2034
- *
2035
- * A business refusal arrives as HTTP 200 with a non-zero `code` — most often
2036
- * `9004` when the request carries no `x-device-id`. That is reported as
2037
- * `claimed: false` rather than thrown, so the caller can tell "the upstream
2038
- * refused this" apart from "the request never arrived".
2039
- */
2040
- claimCheckin(signal?: AbortSignal): Promise<TraeCheckinClaim>;
2041
- /** Rewards / activity rules. */
2042
- activities(signal?: AbortSignal): Promise<TraeActivityRule[]>;
2043
- /** Convenience: snapshot + check-in + activities in one call (best-effort, non-fatal on missing). */
2044
- view(signal?: AbortSignal): Promise<TraeUsageView>;
2045
- }
2046
- //#endregion
2047
- //#region src/status-paths.d.ts
2048
- /** Plugin-owned usage endpoint consumed by its browser half. */
2049
- declare const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
2050
- /**
2051
- * Plugin-owned availability probe: POST the model ids to test, get one verdict
2052
- * per model. Unlike every read-only sibling this route SPENDS quota — a real
2053
- * (minimal) chat call per model — so it is user-initiated only, and the card
2054
- * labels the button as consuming a small amount.
2055
- */
2056
- declare const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
2057
- /**
2058
- * Per-account status (check-in / credits / subscription) for the account table.
2059
- *
2060
- * Each row of the account table reads its OWN state through this route — a CN
2061
- * row carries its check-in state and balance, an ai row its subscription.
2062
- * POST with the accountId in the body: one upstream read per account, paid
2063
- * only when the table is actually shown.
2064
- */
2065
- declare const TRAE_ACCOUNT_STATUS_PATH = "/plugins/dsh-connect-trae/account-status";
2066
- /**
2067
- * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
2068
- * route in this plugin that changes upstream account state. The accountId in
2069
- * the body names WHICH account to claim for, so the button can sit in each
2070
- * account row.
2071
- */
2072
- declare const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
2073
- /** Query parameter naming the region a card request addresses. */
2074
- declare const TRAE_REGION_PARAM = "region";
2075
- /** Every region, in card tab order. */
2076
- declare const TRAE_REGIONS: readonly TraeRegion[];
2077
- /**
2078
- * Address one region's status route. The two regions are separate provider
2079
- * stacks; every card request carries the region whose tab the user is on.
2080
- */
2081
- declare function withTraeRegion(path: string, panel: TraePanel): string;
2082
- /**
2083
- * Read the region parameter off a status-route URL. Absent means the domestic
2084
- * tab (`cn`); a present-but-unknown value returns undefined so the route can
2085
- * answer 400 instead of guessing.
2086
- */
2087
- declare function regionOfTraeStatusUrl(url: string): TraePanel | undefined;
2088
- /** One credit pack and its remaining credit. */
2089
- interface TraeWebCreditAccount {
2090
- displayDesc: string;
2091
- remain: number;
2092
- size: number;
2093
- /**
2094
- * When this pack lapses, in epoch milliseconds, taken from the upstream's own
2095
- * `end_time`. Absent when the upstream reported no usable time (0 / missing),
2096
- * so a consumer can tell "no date" apart from "1970".
2097
- */
2098
- expiresAtMs?: number;
2099
- }
2100
- /** Credit lapsing inside one horizon, as the card reports it. */
2101
- interface TraeWebExpiringCredits {
2102
- /** Sum of the contributing packs' remaining credit. */
2103
- total: number;
2104
- /** How many packs contribute. */
2105
- packs: number;
2106
- /** Soonest expiry among them, epoch milliseconds. */
2107
- earliestExpiresAtMs?: number;
2108
- }
2109
- /**
2110
- * Credit at risk, bucketed by how soon it lapses.
2111
- *
2112
- * Only present when at least one pack carried a usable `expiresAtMs` — a card
2113
- * must never read "we were not told when anything expires" as "nothing is
2114
- * about to lapse".
2115
- */
2116
- interface TraeWebCreditExpiry {
2117
- within24h: TraeWebExpiringCredits;
2118
- within3d: TraeWebExpiringCredits;
2119
- within7d: TraeWebExpiringCredits;
2120
- /** Packs already past their end time but still carrying credit. */
2121
- expired: TraeWebExpiringCredits;
2122
- }
2123
- /** Aggregated usage/credit answer rendered by the plugin card. */
2124
- interface TraeWebCredits {
2125
- total: number;
2126
- consumed: number;
2127
- available: number;
2128
- workAvailable: number;
2129
- generalAvailable: number;
2130
- accounts: readonly TraeWebCreditAccount[];
2131
- /**
2132
- * Credit lapsing soon, for the card's warning banner.
2133
- *
2134
- * Derived from the per-pack {@link TraeWebCreditAccount.expiresAtMs} the
2135
- * `/usage` route already carries, so it costs no extra upstream read.
2136
- */
2137
- expiry?: TraeWebCreditExpiry;
2138
- }
2139
- /** Editable Trae model row rendered by the plugin-owned settings card. */
2140
- interface TraeWebModel {
2141
- id: string;
2142
- name: string;
2143
- contextWindow?: number;
2144
- maxTokens?: number;
2145
- input?: ('text' | 'image')[];
2146
- creditMultiplier?: number;
2147
- reasoningSupported?: boolean;
2148
- reasoning?: {
2149
- supported: string[];
2150
- defaultEffort?: string;
2151
- };
2152
- maxContextWindow?: number;
2153
- /**
2154
- * Trae's own image-capability flag, as the runtime catalog carries it. The
2155
- * card renders these rows with their image checkbox already ticked so the
2156
- * control matches what DSH is actually told (issue #16); absent means the
2157
- * directory did not declare it, which is not a capability claim either way.
2158
- */
2159
- multimodal?: boolean;
2160
- /**
2161
- * The availability verdict from the one-click test, when this card session
2162
- * has run one. Absent = not tested. Deliberately NOT persisted with the
2163
- * settings: it describes one moment on one account, and a stale verdict shown
2164
- * as current is worse than a blank field.
2165
- */
2166
- probe?: TraeModelProbeVerdict;
2167
- /** Upstream's own words for the verdict, when it gave any. */
2168
- probeDetail?: string;
2169
- }
2170
- /**
2171
- * One model's availability verdict from the one-click test.
2172
- *
2173
- * `unknown` is a first-class outcome, not a failure: a timeout, a proxy drop or
2174
- * a throttle says nothing about the model, and reporting it as unusable would
2175
- * make the user delete a model that works. Only an upstream refusal is negative.
2176
- */
2177
- type TraeModelProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
2178
- /** What the probe route answers with. */
2179
- interface TraeModelsTestResponse {
2180
- /** One entry per model tested, in the order they were sent. */
2181
- results: {
2182
- id: string;
2183
- verdict: TraeModelProbeVerdict;
2184
- detail: string;
2185
- }[];
2186
- /** Models skipped because the region has no live catalog row for them. */
2187
- skipped?: string[];
2188
- }
2189
- /**
2190
- * Daily check-in state rendered below the credit stats.
2191
- *
2192
- * The two booleans answer DIFFERENT questions and must never be collapsed into
2193
- * one "done" flag — doing that was a shipped bug (see CHANGELOG 2.3.1):
2194
- *
2195
- * - `checkedIn` is ACCOUNT-level: today's reward pack exists for this account,
2196
- * so there is nothing left to claim and the balance already includes it.
2197
- * - `didCheckedIn` is DEVICE-level: this MACHINE already spent its one
2198
- * check-in for the day, whichever account spent it. The upstream keys it on
2199
- * the `x-device-id` in the request and answers it per device, not per
2200
- * account (measured 2026-09-26: the same account and Beijing day answered
2201
- * `false` with the header omitted, with a synthetic id, and with another
2202
- * install's id, and `true` for the device that actually claimed).
2203
- *
2204
- * So `didCheckedIn && !checkedIn` is the "switched account" case: the device's
2205
- * daily check-in is used up, but THIS account was never rewarded. Claiming
2206
- * again is refused upstream (business code 9095, "该设备今日已参与签到"), so
2207
- * the card disables the button — but it must say that, not "claimed today".
2208
- */
2209
- interface TraeWebCheckin {
2210
- checkedIn: boolean;
2211
- didCheckedIn: boolean;
2212
- credits: number;
2213
- enabled: boolean;
2214
- extraCredits?: number;
2854
+ activityType: number;
2855
+ startTimeMs: number;
2856
+ endTimeMs: number;
2857
+ workExtra?: Record<string, unknown>;
2215
2858
  }
2216
- /** The claim route's answer, so the card can report a business refusal verbatim. */
2217
- interface TraeWebCheckinClaim {
2218
- /** False when the upstream answered a non-zero business code (e.g. 9004). */
2219
- claimed: boolean;
2859
+ interface TraeUsageView {
2860
+ snapshot: TraeUsageSnapshot;
2861
+ checkin: TraeCheckinStatus;
2862
+ activities: TraeActivityRule[];
2863
+ }
2864
+ /**
2865
+ * A client for the verified Trae usage/credits endpoints.
2866
+ *
2867
+ * Every method is read-only except {@link TraeUsageClient.claimCheckin}, which
2868
+ * claims the daily check-in reward — the one deliberate mutation, requested by
2869
+ * the user from the card and guarded before it is sent.
2870
+ */
2871
+ declare class TraeUsageClient {
2872
+ private readonly options;
2873
+ private readonly fetchImpl;
2874
+ private readonly baseUrl;
2875
+ private readonly timeoutMs;
2876
+ constructor(options: TraeUsageOptions);
2877
+ /** Region of the current credential; `cn` when unresolvable. */
2878
+ private currentRegion;
2220
2879
  /**
2221
- * True only when THIS ACCOUNT's reward for today already exists, so nothing
2222
- * was sent upstream. Distinct from {@link TraeWebCheckinClaim.deviceCheckedIn}:
2223
- * a device that already claimed for another account leaves this false.
2880
+ * Pay base for the current credential's region. An explicit baseUrl (tests,
2881
+ * diagnostics) pins the endpoint; otherwise CN uses `api.trae.cn` and the
2882
+ * international region its own verified pay gateway.
2224
2883
  */
2225
- alreadyCheckedIn: boolean;
2884
+ private payBase;
2226
2885
  /**
2227
- * True when today's check-in was already spent on this DEVICE (by this or
2228
- * another account), so no claim was sent or the upstream refused it with
2229
- * 9095. Not an error: it is the truthful answer to "can this account claim
2230
- * here today?" — no, and the reason is the machine, not the account.
2886
+ * Device id for the check-in routes, or undefined when the machine's
2887
+ * installation identity cannot be read. Best-effort on purpose: a missing
2888
+ * identity must not break the read-only status query, which the upstream
2889
+ * answers with or without the header.
2231
2890
  */
2232
- deviceCheckedIn?: boolean;
2233
- code?: number;
2234
- message?: string;
2235
- /** The refreshed check-in state, present whenever the claim path ran. */
2236
- checkin?: TraeWebCheckin;
2237
- }
2238
- /** One reward activity rule (name + work/general credits when present). */
2239
- interface TraeWebActivity {
2240
- activityId: string;
2241
- enabled: boolean;
2242
- workCredits?: number;
2243
- generalCredits?: number;
2244
- }
2245
- interface TraeWebAccount {
2246
- id: string;
2247
- accountName: string;
2248
- edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
2891
+ private deviceIdHeader;
2892
+ private authedHeaders;
2893
+ private post;
2249
2894
  /**
2250
- * Whether this account belongs to a Trae tenant, i.e. may bill the enterprise
2251
- * pool. The enterprise tab lists only these, because the tenant gateway
2252
- * serves a directory to any CN token but refuses a personal account's chat
2253
- * with `4019` — so nothing else reveals the difference until a turn fails.
2895
+ * Subscription/pay status of an international (ai) account. The CN region
2896
+ * never calls this (its contract is unverified there); the ai region uses
2897
+ * this instead of the Work-credit `snapshot`.
2254
2898
  */
2255
- enterprise: boolean;
2256
- /** Routing bucket of this account, derived from its credential. */
2257
- region: TraeRegion;
2258
- source: 'desktop' | 'dsh' | 'cli';
2259
- tokenExpiresAtMs: number;
2899
+ payStatus(signal?: AbortSignal): Promise<TraePayStatus>;
2260
2900
  /**
2261
- * When this account's REFRESH token expires, when the credential carries one.
2901
+ * The Work-credit endpoints are CN-only. The international region is
2902
+ * subscription-based and must read {@link payStatus} instead; guarding here
2903
+ * keeps the card's degraded `creditsError` message diagnosable rather than
2904
+ * letting an ai credential hit an unverified path on its pay gateway.
2905
+ */
2906
+ private requireCnRegion;
2907
+ /** Total entitlements / credits and per-pack breakdown. */
2908
+ snapshot(signal?: AbortSignal): Promise<TraeUsageSnapshot>;
2909
+ /**
2910
+ * Daily check-in status. Read-only, and answered with or without the device
2911
+ * header — the claim is what needs it.
2262
2912
  *
2263
- * The access token above is renewed automatically — the plugin refreshes it
2264
- * five minutes before it lapses — so by itself it answers "when does this
2265
- * need attention", which is "never, while the refresh token lives". The
2266
- * refresh token is the real horizon: once IT expires the automatic renewal
2267
- * stops working and the user must sign in to Trae again. Before this field the
2268
- * card could only say "auto-renewal", which stays true right up to the moment
2269
- * it silently stops being true.
2913
+ * The read DOES carry the header whenever this installation has one, and
2914
+ * that is deliberate rather than incidental: `did_checked_in` is answered
2915
+ * per device, so omitting the header would make a device that already claimed
2916
+ * today look identical to one that never has (measured 2026-09-26, see
2917
+ * {@link TraeCheckinStatus.didCheckedIn}).
2270
2918
  */
2271
- refreshExpiresAtMs?: number;
2272
- selected: boolean;
2919
+ checkinStatus(signal?: AbortSignal): Promise<TraeCheckinStatus>;
2920
+ /**
2921
+ * Claim today's check-in reward. The ONLY state-changing call in this client.
2922
+ *
2923
+ * The upstream is idempotent per Beijing day (verified 2026-09-24: repeating
2924
+ * the call on an already-claimed day answers `code: 0` while the entitlement
2925
+ * total stays byte-identical), so a double click cannot double-grant. The
2926
+ * card and its route still guard, because "cannot double-grant" is a property
2927
+ * of the upstream we verify rather than one we rely on.
2928
+ *
2929
+ * A business refusal arrives as HTTP 200 with a non-zero `code` — most often
2930
+ * `9004` when the request carries no `x-device-id`. That is reported as
2931
+ * `claimed: false` rather than thrown, so the caller can tell "the upstream
2932
+ * refused this" apart from "the request never arrived".
2933
+ */
2934
+ claimCheckin(signal?: AbortSignal): Promise<TraeCheckinClaim>;
2935
+ /** Rewards / activity rules. */
2936
+ activities(signal?: AbortSignal): Promise<TraeActivityRule[]>;
2937
+ /** Convenience: snapshot + check-in + activities in one call (best-effort, non-fatal on missing). */
2938
+ view(signal?: AbortSignal): Promise<TraeUsageView>;
2273
2939
  }
2940
+ //#endregion
2941
+ //#region src/account-pool.d.ts
2274
2942
  /**
2275
- * The account the Trae desktop app is signed in to right now, when it differs
2276
- * from the one this plugin is bound to (issue #29).
2943
+ * Which of the pool's writers produced a measurement.
2277
2944
  *
2278
- * Reported on the usage document so the card can offer one-click follow without
2279
- * an extra round trip; also written back on a POST to the accounts refresh
2280
- * route when the client confirms it saw the hint. Absent whenever the two
2281
- * agree or the app is not readable — a healthy document carries no extra
2282
- * fields and the plugin never bills from this value directly: the FOLLOW is a
2283
- * settings write of `accounts.<region>`, made by the card or by the host's
2284
- * follow switch, never a silent rebind here.
2945
+ * `test-batch` — the user pressed "test" (or the check-in batch), so the outcome
2946
+ * is a deliberate measurement of the target model.
2947
+ * `live-request` — a real chat request failed and its failure was recorded so the
2948
+ * NEXT request starts from a measurement instead of rediscovering the same wall.
2949
+ *
2950
+ * The distinction is diagnostic, not behavioural: both feed the same routing and
2951
+ * cooling logic. It exists so a user who watched an account measure `ok` and
2952
+ * later saw it limited can tell whether that was a later live failure (expected)
2953
+ * or an older value winning (a bug).
2285
2954
  */
2286
- interface TraeWebAppLogin {
2955
+ type TraePoolProbeSource = 'test-batch' | 'live-request';
2956
+ /** One account's pool entry: what the picker needs, plus its last measurement. */
2957
+ interface TraePoolAccount {
2958
+ /** Stable account id (the same one `TraeWebAccount` carries). */
2287
2959
  id: string;
2960
+ /** Human name, or `''` when the desktop app recorded none. */
2288
2961
  accountName: string;
2289
- edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
2290
- region: TraeRegion;
2291
2962
  }
2292
2963
  /**
2293
- * The per-account state shown inside an account row of the card's table.
2294
- *
2295
- * Every locally detected account in a region is rendered as a row; the
2296
- * interesting per-account facts differ by region, which is why this is a union
2297
- * of the three surfaces the card previously showed only for the BOUND account:
2298
- *
2299
- * - CN rows carry their own check-in state (Trae keys the daily reward per
2300
- * ACCOUNT, and the one-per-device tally per DEVICE, see {@link TraeWebCheckin})
2301
- * and their Work/General balances.
2302
- * - ai rows carry their own subscription / trial status.
2964
+ * What the pool knows about ONE account's last test of the target model.
2303
2965
  *
2304
- * Each field is optional: a read that fails keeps the row visible (perhaps
2305
- * without a figure) rather than dropping the account and its switch.
2966
+ * `undefined` means "never tested", which is deliberately distinct from every
2967
+ * measured outcome: an untested account is a candidate (the user may want to use
2968
+ * it now) whereas a measured failure is not. Collapsing the two would make a
2969
+ * fresh pool look empty until the first test.
2306
2970
  */
2307
- interface TraeWebAccountState {
2308
- /** The account this state belongs to. */
2309
- id: string;
2310
- /** CN: today's check-in state of this account (absent for ai rows). */
2311
- checkin?: TraeWebCheckin;
2312
- /** CN: this account's Work/General balances (absent for ai rows). */
2313
- credits?: TraeWebCredits;
2314
- /** ai: this account's subscription / trial status (absent for CN rows). */
2315
- payStatus?: TraeWebPayStatus;
2971
+ interface TraePoolProbe {
2972
+ outcome: TraePoolOutcome;
2973
+ /** When this measurement was taken, in epoch ms. */
2974
+ atMs: number;
2975
+ /** WHAT WROTE this measurement; absent for records written before it existed. */
2976
+ source?: TraePoolProbeSource;
2977
+ /**
2978
+ * When this account may be usable again, when the upstream stated a time.
2979
+ *
2980
+ * Present for a `rate-limited` / `out-of-credit` outcome whose failure carried
2981
+ * a reset sentence. ABSENT when the upstream named no time — that absence is
2982
+ * meaningful and must never be turned into a locally invented countdown.
2983
+ */
2984
+ retryAtMs?: number;
2985
+ /**
2986
+ * The upstream's own words on the failure, already redacted.
2987
+ *
2988
+ * Kept so the card can say WHY an account left the pool instead of repeating
2989
+ * one generic sentence for a DNS failure, a gateway 502 and a cancelled request
2990
+ * alike.
2991
+ */
2992
+ message?: string;
2993
+ /**
2994
+ * WHICH MODEL this measurement is about.
2995
+ *
2996
+ * `1005` (plan gate), `4001` (no SOLO function serves this `config_name`) and
2997
+ * `4011` (throttle) are all ACCOUNT × MODEL conditions: the same credential
2998
+ * answers other models at the same second. A row rendered as plain "可用"
2999
+ * therefore claims more than the measurement says. Storing the model id is what
3000
+ * makes the claim checkable.
3001
+ *
3002
+ * Deliberately NOT inherited when an update omits it — the opposite of
3003
+ * `accountName` and `retryAtMs`. Those describe the ACCOUNT (still true when a
3004
+ * later writer cannot see them); the model describes THIS measurement, and
3005
+ * carrying an older id forward would relabel a fresh measurement with a model
3006
+ * it was not taken on.
3007
+ */
3008
+ modelId?: string;
3009
+ /**
3010
+ * The account's display name AT THE MOMENT IT WAS MEASURED.
3011
+ *
3012
+ * Stored because a saved pool member can outlive its sign-in: once the desktop
3013
+ * app drops a login, that id resolves to no listed account and the card has
3014
+ * nothing left to render for it. The name was in hand at write time and nowhere
3015
+ * afterwards. Snapshots are never refreshed from a LISTING — only a real
3016
+ * measurement writes this.
3017
+ */
3018
+ accountName?: string;
2316
3019
  }
2317
- interface TraeWebCreditAlternative {
3020
+ /** One account's live credit situation, as the pool ranks it. */
3021
+ interface TraePoolCredits {
3022
+ /** Remaining credit across every pack (`available` of the upstream snapshot). */
3023
+ total: number;
3024
+ /** The Work bucket, kept for display: SOLO chat does NOT spend it. */
3025
+ workAvailable?: number;
3026
+ /** The General bucket, kept for display: this is what SOLO chat spends. */
3027
+ generalAvailable?: number;
3028
+ /** Credits expiring within the card's "soon" window (3 days). */
3029
+ expiringSoon: number;
3030
+ /** When the nearest at-risk pack expires, if any does. */
3031
+ nearestExpiryMs?: number;
3032
+ }
3033
+ /**
3034
+ * Everything the ranking rule reads about one account. Assembled by the Host from
3035
+ * the credential store, the per-account credit read, and the runtime probe store
3036
+ * — this module only consumes it.
3037
+ */
3038
+ interface TraePoolMember {
3039
+ account: TraePoolAccount;
3040
+ credits?: TraePoolCredits;
3041
+ probe?: TraePoolProbe;
3042
+ /** When the stored credential expires, used as the final tie-break. */
3043
+ tokenExpiresAtMs?: number;
3044
+ }
3045
+ /**
3046
+ * How one account answered the target model.
3047
+ *
3048
+ * The first nine values are the workbuddy taxonomy verbatim so the two cards read
3049
+ * the same. The tenth, `plan-gated`, is Trae-only: upstream `1005` says THIS
3050
+ * account's plan does not cover this model while another account's plan may, and
3051
+ * no amount of waiting changes a plan — so it must not be folded into
3052
+ * `credential-rejected` (which would demand a re-login) nor into
3053
+ * `policy-rejected` (which means a content filter tripped on ONE request).
3054
+ */
3055
+ type TraePoolOutcome = 'ok' | 'rate-limited' | 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'plan-gated' | 'unavailable' | 'not-found' | 'failed';
3056
+ //#endregion
3057
+ //#region src/model-probe.d.ts
3058
+ /** How one model answered. */
3059
+ type TraeProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
3060
+ interface TraeProbeResult {
3061
+ /** The model id as the card knows it. */
2318
3062
  id: string;
2319
- accountName: string;
2320
- edition: TraeWebAccount['edition'];
2321
- workAvailable: number;
2322
- generalAvailable: number;
2323
- }
2324
- /** One probed candidate path and why it did not yield an account. */
2325
- interface TraeWebSearchPath {
2326
- path: string;
2327
- edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
2328
- source: 'desktop' | 'cli';
2329
- reason: 'missing' | 'unreadable' | 'invalid';
2330
- message?: string;
3063
+ verdict: TraeProbeVerdict;
3064
+ /** Upstream's own words (already translated), or a short note. */
3065
+ detail: string;
2331
3066
  }
2332
3067
  /**
2333
- * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
2334
- * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
2335
- */
2336
- declare function unwrapVolatile<T>(value: T): T;
2337
- /**
2338
- * Deep copy of a value with every `{get(): T}` live reference replaced by the
2339
- * value it resolves to.
2340
- *
2341
- * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
2342
- * reference is still `typeof === 'object'` — so it passes a naive object check
2343
- * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
2344
- * does NOT read the field: it produces `{ get: <function> }`. Any merge that
2345
- * spreads a resolved field to preserve its siblings — the card's "write one
2346
- * region, keep the other" write, or the account map's spread — would otherwise
2347
- * DROP every sibling and leak a function into the document.
2348
- *
2349
- * Both halves therefore unwrap before touching a resolved field. This lives in
2350
- * the node-free host↔client bridge because the browser half needs it too.
2351
- *
2352
- * Non-reference values are recursed into so a nested volatile field is caught
2353
- * as well; arrays and objects are rebuilt rather than mutated, so the caller's
2354
- * value is never touched.
2355
- */
2356
- declare function unwrapVolatileDeep<T>(value: T): T;
2357
- /**
2358
- * Read the per-region account selections out of a settings value.
2359
- *
2360
- * SHARED because `accounts` now has TWO writers: the card, and the composer
2361
- * panel's account table (2.14.0). Two writers of one field must use one merge
2362
- * rule, or they drift — and the rule is not obvious.
2363
- *
2364
- * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
2365
- * live reference, which passes the `typeof === 'object'` check and would be
2366
- * returned as if it were the map. Every lookup on it is then `undefined`, and
2367
- * the merge (`{ ...configured, [region]: id }`) would spread a reference into
2368
- * `{get: <function>}` — dropping the other region's selection and leaking a
2369
- * function into the settings document.
3068
+ * How long one probe may take. Trae answers a one-word prompt in a few seconds;
3069
+ * beyond this the answer is not worth waiting for and is reported as unknown.
2370
3070
  */
2371
- declare function configuredAccountsOf(configured: unknown): Record<string, string>;
3071
+ declare const TRAE_PROBE_TIMEOUT_MS = 30000;
2372
3072
  /**
2373
- * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
2374
- * #29). Absent reads as OFF, so an existing configuration keeps the strict
2375
- * bind it shipped with and the switch is an opt-in — the same opt-out
2376
- * semantics as {@link regionEnabledOf}, for the same reason: a config written
2377
- * before this field existed cannot ask for behavior it never declared.
3073
+ * Classify a failure that arrived as a thrown stream error rather than a
3074
+ * returned result.
2378
3075
  *
2379
- * Reads the whole settings section (or the bare field); deep-unwraps first
2380
- * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
2381
- * reference, whose `typeof === 'object'` would otherwise pass a naive check.
2382
- */
2383
- declare function followAppLoginEnabled(configured: unknown): boolean;
2384
- /**
2385
- * Build the next `regions` settings value for the card's save. The write
2386
- * targets ONLY the signed-in account's region slot; every other region's slot
2387
- * is carried over untouched, so switching accounts never clobbers the other
2388
- * region's picks. Tolerates any stored shape (absent, non-object) by starting
2389
- * from an empty document.
3076
+ * The bridge errors the stream (instead of returning a result) when a refusal
3077
+ * interrupts an answer that had already started; its message is the same
3078
+ * translated text, so a named code is still decisive. Anything without a
3079
+ * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
2390
3080
  */
2391
- declare function nextRegionSlots<Slot extends object>(regions: unknown, panel: TraePanel, slot: Slot): Record<string, unknown>;
3081
+ declare function classifyProbeError(message: string): {
3082
+ verdict: TraeProbeVerdict;
3083
+ detail: string;
3084
+ };
2392
3085
  /**
2393
- * Whether one region's provider is switched on. Opt-out semantics: only an
2394
- * explicit `false` disables it, so a config written before this switch existed
2395
- * (and the pre-region-split flat fields, which never carry `enabled`) keep both
2396
- * providers running exactly as before. The Host reads the same rule through
2397
- * `regionStateOf`, so card and Host can never disagree about a region's state.
3086
+ * Classify a completed upstream call.
2398
3087
  *
2399
- * `value` may be the whole settings section or the `regions` map (see
2400
- * {@link regionsMapOf}).
3088
+ * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
3089
+ * arrived inside a 200 body has already been turned into a real error result
3090
+ * (and its code translated into `message`) by the bridge — this function only
3091
+ * has to read it back.
2401
3092
  */
2402
- declare function regionEnabledOf(value: unknown, panel: TraePanel): boolean;
3093
+ declare function classifyProbeResult(result: TraeChatResult, outputSeen: boolean): {
3094
+ verdict: TraeProbeVerdict;
3095
+ detail: string;
3096
+ };
2403
3097
  /**
2404
- * Build the next `regions` settings value for a provider on/off toggle. ONLY
2405
- * the target region's `enabled` flag changes: every other field of that slot
2406
- * (its directory, selection, image opt-ins, context budgets) and every other
2407
- * region's slot are carried over verbatim, so switching a provider off never
2408
- * discards the user's model picks and switching it back on restores them.
3098
+ * The chat call one probe makes.
2409
3099
  *
2410
- * This is deliberately separate from {@link nextRegionSlots}: that helper
2411
- * writes a whole slot from a signed-in tab's draft, while this one must work
2412
- * for a region that is signed OUT — which is precisely the region a user wants
2413
- * to switch off (no international install, no international account).
3100
+ * `inputTokens` pads the user turn so the probe carries roughly as much text as
3101
+ * a real conversation. Size matters because an upstream that refuses LARGE
3102
+ * requests will happily answer a one-word probe, and a verdict earned by a
3103
+ * 1-token request says nothing about the 30k-token turn the user is about to
3104
+ * send. `0` keeps the historical minimal probe (one word) — that is the default
3105
+ * because it spends the least credit and answers the question the pool ranks on
3106
+ * ("can this account talk at all").
2414
3107
  *
2415
- * `value` may be the whole settings section or the `regions` map; the RETURN
2416
- * value is always the `regions` map, i.e. exactly what `settingsScope.set(
2417
- * 'regions', ...)` needs.
3108
+ * The filler is `x` words: one ASCII word is about one token, so the body size
3109
+ * is predictable (tokens ≈ inputTokens × 2 characters) without a tokenizer.
2418
3110
  */
2419
- declare function nextRegionEnabled(value: unknown, panel: TraePanel, enabled: boolean): Record<string, unknown>;
2420
3111
  /**
2421
- * The two seats this region's credit readout can occupy.
2422
- *
2423
- * `composer` is the composer tool row (`conversation.input.left`); `stats` is
2424
- * the ambient dock below the composer card (`conversation.composer.dock`),
2425
- * beside the shell's own round/step/token pills.
2426
- *
2427
- * WHY THE SEAT IS A CHOICE AT ALL (learned from dsh-connect-workbuddy issue
2428
- * #62): the composer tool row is a LIST seat, so it is shared not only with the
2429
- * shell's own controls but with any other plugin that registers there — and a
2430
- * list seat cannot promise that every occupant is visible. The report behind
2431
- * that issue is exactly that: with a third-party plugin installed the readout
2432
- * stopped appearing. A fixed seat therefore lets one plugin's presence decide
2433
- * another's, so the seat belongs to the user.
3112
+ * The largest probe this plugin will pad.
2434
3113
  *
2435
- * Named as a union rather than a boolean because a third seat is plausible and
2436
- * a `showAtTop: boolean` would have to be MIGRATED rather than extended.
3114
+ * A 100k-token probe is already ~200 KB of JSON; anything above that is a request
3115
+ * the user's real turn would not plausibly reach in this product, and a config
3116
+ * file edited by hand must not turn one "test all" click into a multi-megabyte
3117
+ * upload.
2437
3118
  */
2438
- type TraeCreditPosition = 'composer' | 'stats';
3119
+ declare const MAX_PROBE_INPUT_TOKENS = 100000;
3120
+ declare function probeBody(model: string, inputTokens?: number): string;
2439
3121
  /**
2440
- * Which seat one region's readout uses, defaulting to the composer row.
2441
- *
2442
- * Anything other than an explicit `'stats'` reads as `'composer'`, which covers
2443
- * the two cases that matter: a profile written BEFORE this setting existed (the
2444
- * composer row was the only seat, so an upgrade moves nothing on screen), and a
2445
- * hand-edited value the schema does not know. Falling back rather than
2446
- * rendering nowhere is deliberate — an unrecognized value must not make the
2447
- * readout disappear, which is the very defect this setting exists to fix.
2448
- *
2449
- * `value` may be the whole settings section or the `regions` map (see
2450
- * {@link regionsMapOf}).
3122
+ * Probe one model through the bridge the plugin actually chats with, so the
3123
+ * recorded verdict describes the user's real path (same wire resolution, same
3124
+ * function stamping, same error translation).
2451
3125
  */
2452
- declare function regionCreditPositionOf(value: unknown, panel: TraePanel): TraeCreditPosition;
3126
+ declare function probeOneModel(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, id: string, timeoutMs?: number, inputTokens?: number): Promise<TraeProbeResult>;
2453
3127
  /**
2454
- * Build the next `regions` settings value for one region's credit seat.
2455
- *
2456
- * Same merge rule as every sibling region write: only the target region's
2457
- * `creditPosition` changes, so a seat change cannot drop that slot's model
2458
- * directory, selection, budgets or enabled flag, nor touch the other region.
3128
+ * Probe a list of models ONE AT A TIME.
2459
3129
  *
2460
- * `value` may be the whole settings section or the `regions` map; the RETURN
2461
- * value is always the `regions` map, i.e. exactly what `settingsScope.set(
2462
- * 'regions', ...)` needs.
3130
+ * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
3131
+ * would manufacture the very `rate_limited`/`unknown` verdicts this feature
3132
+ * must avoid. An authentication failure stops the run — every later probe would
3133
+ * fail the same way, and spending calls to learn that again is waste.
2463
3134
  */
2464
- declare function nextRegionCreditPosition(value: unknown, panel: TraePanel, position: TraeCreditPosition): Record<string, unknown>;
2465
- /** Subscription status of an international (ai) account, rendered instead of the CN credit packs. */
2466
- interface TraeWebPayStatus {
2467
- isDollarUsageBilling: boolean;
2468
- hasPackage: boolean;
2469
- isPayFreshman: boolean;
2470
- inTrial: boolean;
2471
- trialEndTimeMs: number;
2472
- enableSoloLite: boolean;
2473
- enableSoloBuilder: boolean;
2474
- enableSoloCoder: boolean;
2475
- enableSoloWeb: boolean;
2476
- /** Trae's authoritative tier number (`user_pay_identity`); `0` means Free. */
2477
- payIdentity?: number;
2478
- /** Trae's authoritative tier label (`user_pay_identity_str`), e.g. `Free`. */
2479
- payIdentityStr?: string;
2480
- fission?: {
2481
- startTimeMs: number;
2482
- expireTimeMs: number;
2483
- maxUsage: number;
2484
- };
2485
- }
2486
- /** The JSON document the plugin card renders. */
2487
- type TraeWebUsage = {
2488
- status: 'signed-out';
2489
- accounts: readonly TraeWebAccount[];
2490
- message?: string;
2491
- searched?: readonly TraeWebSearchPath[];
2492
- enabled?: boolean;
2493
- } | {
2494
- status: 'signed-in';
3135
+ declare function probeModelsSequentially(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, ids: readonly string[], onResult?: (result: TraeProbeResult, index: number) => void): Promise<TraeProbeResult[]>;
3136
+ //#endregion
3137
+ //#region src/account-pool-run.d.ts
3138
+ /** One account's check-in outcome. */
3139
+ interface TraePoolCheckinRow {
2495
3140
  accountId: string;
2496
3141
  accountName: string;
2497
- tokenExpiresAtMs: number;
2498
- /**
2499
- * When the BOUND account's refresh token expires (absent when the credential
2500
- * carries none). See {@link TraeWebAccount.refreshExpiresAtMs} — this is the
2501
- * horizon that actually matters, because the access token above renews
2502
- * itself until this one lapses.
2503
- */
2504
- refreshExpiresAtMs?: number;
2505
- /** Which per-region model directory and selection this account owns. */
2506
- region: TraeRegion;
2507
- /** Whether this region's provider is currently offered to DSH. */
2508
- enabled?: boolean;
2509
- accounts: readonly TraeWebAccount[];
2510
- models: readonly TraeWebModel[];
2511
- enabledModelIds: readonly string[];
2512
- rawChat?: {
2513
- state: 'disabled' | 'unchecked' | 'available' | 'protocol-gated' | 'authentication' | 'credit' | 'rate' | 'transport' | 'server';
2514
- checkedAtMs?: number;
2515
- status?: number;
2516
- };
2517
- credits?: TraeWebCredits;
2518
- creditsError?: string;
2519
- /**
2520
- * Set when this panel has no readable balance by design, rather than by
2521
- * failure. Only `enterprise` does: a tenant's spend is not reported by any
2522
- * endpoint the plugin can reach (every tenant quota path answers 404), so
2523
- * the figure exists solely in the enterprise console. The card must say so
2524
- * instead of rendering the consumer packs, which belong to the personal
2525
- * balance and would be read as the tenant's.
2526
- */
2527
- creditsUnavailable?: 'enterprise';
2528
- /**
2529
- * Present only when the bound account is out of credit AND another account
2530
- * on this machine has some (issue #25). The card then offers a one-click
2531
- * switch instead of leaving a permanently empty panel. Absent in every
2532
- * other case so a healthy account never pays for the extra read.
2533
- */
2534
- alternatives?: readonly TraeWebCreditAlternative[];
2535
- /**
2536
- * Present only when the Trae app's CURRENT sign-in differs from the
2537
- * account this plugin is bound to (issue #29): the card then offers a
2538
- * one-click follow instead of making the user hunt the account dropdown
2539
- * after every app-side sign-out. Absent in every other case — a healthy
2540
- * document costs no extra read beyond the accounts scan the document
2541
- * already runs.
2542
- */
2543
- appLogin?: TraeWebAppLogin;
2544
3142
  /**
2545
- * Daily check-in state (CN only). Absent on the international region,
2546
- * whose check-in surface does not exist — probed 2026-09-24: the
2547
- * `/trae/api/v2/ug/*` family answers 404 on every ai gateway, so the card
2548
- * shows no claim button there rather than a button that cannot work.
2549
- */
2550
- checkin?: TraeWebCheckin;
2551
- checkinError?: string;
2552
- /** Subscription status of an international account (region ai only). */
2553
- payStatus?: TraeWebPayStatus;
2554
- payStatusError?: string;
2555
- } | {
2556
- status: 'error';
2557
- message: string;
2558
- };
3143
+ * - `claimed` — the upstream accepted today's claim for this account.
3144
+ * - `already` — this ACCOUNT's reward for today already exists, so nothing was sent.
3145
+ * - `device-already` — the MACHINE already spent today (9095). Not an account
3146
+ * failure and not a success: a truthful "no, this device is used up".
3147
+ * - `disabled` — the upstream's check-in activity is not enabled/active for
3148
+ * this account. Reported separately rather than as `failed`, because claiming
3149
+ * is impossible by policy and calling it an error would read as a broken
3150
+ * plugin.
3151
+ * - `failed` — the request never landed, or the credential could not be read.
3152
+ */
3153
+ status: 'claimed' | 'already' | 'device-already' | 'disabled' | 'failed';
3154
+ /** Credits the upstream says this claim carries, when it stated a figure. */
3155
+ credit?: number;
3156
+ /** The upstream's own words on failure/refusal, redacted by the caller. */
3157
+ message?: string;
3158
+ }
3159
+ /** One account's test outcome. */
3160
+ interface TraePoolTestRow {
3161
+ accountId: string;
3162
+ accountName: string;
3163
+ /** The model this row was probed on — one model per batch, recorded per row. */
3164
+ modelId: string;
3165
+ outcome: TraeProbeResult['verdict'];
3166
+ /** Upstream's own words / the plugin's reading of the code. */
3167
+ detail: string;
3168
+ /** A time the upstream named, when it named one. */
3169
+ retryAtMs?: number;
3170
+ }
2559
3171
  //#endregion
2560
3172
  //#region src/web-status.d.ts
2561
3173
  /**
@@ -2625,6 +3237,73 @@ interface TraeUsageRouteOptions {
2625
3237
  }>;
2626
3238
  /** Raw-Chat capability state of the requested panel. */
2627
3239
  rawDiagnostic?(panel: TraePanel): TraeRawDiagnostic;
3240
+ /**
3241
+ * The account pool's host side, when the panel can run one. Optional on
3242
+ * purpose: the pool is an add-on to the status document, and a harness without
3243
+ * it (a test host, or a panel stack that failed to build) must still get a
3244
+ * complete usage answer with no `pool` field.
3245
+ */
3246
+ pool?: TraePoolDeps;
3247
+ }
3248
+ /**
3249
+ * What the pool surface needs from the Host, addressed by panel.
3250
+ *
3251
+ * The split is deliberate. The card never learns a credential and never writes a
3252
+ * measurement: it sends an ACTION (`checkin` / `test` / `remove`) and reads the
3253
+ * ranking back out of the status document. Everything here is therefore either a
3254
+ * cheap local read (preferences, roster, saved names) or an explicitly user-
3255
+ * triggered spend (the batches). The one thing that must never happen on a GET is
3256
+ * an upstream call made for every row on every poll — the card already lazy-loads
3257
+ * per-account state, so `checkedInToday` is cached and skippable by design.
3258
+ */
3259
+ interface TraePoolDeps {
3260
+ /** The panel's saved pool preferences, read from live settings. */
3261
+ preferences(panel: TraePanel): TraePoolPreferences;
3262
+ /**
3263
+ * The full pool membership WITH live credit reads — the expensive version.
3264
+ * Only the status document's own assembly and an explicit batch call it.
3265
+ */
3266
+ members(panel: TraePanel): Promise<readonly TraePoolMember[]>;
3267
+ /**
3268
+ * The checked ids that still resolve to a local sign-in, from a local scan only.
3269
+ * The batch route's empty-pool guard runs this on every request, so it must not
3270
+ * cost an upstream call — {@link members} is not suitable there.
3271
+ */
3272
+ effectiveMemberAccountIds?(panel: TraePanel): Promise<readonly string[]>;
3273
+ /** Last measured display name per saved id, for members whose sign-in is gone. */
3274
+ memberNames?(panel: TraePanel): Promise<Record<string, string>>;
3275
+ /** Local sign-ins the pool does NOT cover, so the card can offer them to check. */
3276
+ otherAccounts?(panel: TraePanel): Promise<readonly TraePoolAccount[]>;
3277
+ /** Which account the request path would bill right now, after applying the pool. */
3278
+ currentAccountId?(panel: TraePanel): Promise<string | undefined>;
3279
+ /** Whether this panel's upstream rewards a daily check-in at all. */
3280
+ checkinSupported?(panel: TraePanel): boolean;
3281
+ /**
3282
+ * Today's check-in state per account, from the cached per-account reads.
3283
+ *
3284
+ * A missing entry means "not read", which the card renders as unknown rather
3285
+ * than as a definite "not checked in". `deviceCheckedIn` is the Trae half of the
3286
+ * pair: the daily claim is keyed to `x-device-id`, so an account can be unpaid
3287
+ * TODAY because this machine already spent its claim on another account.
3288
+ */
3289
+ checkinState?(panel: TraePanel): Promise<Record<string, TraePoolCheckinState>>;
3290
+ /** Run the check-in batch. Persists nothing to settings; drops its own caches. */
3291
+ checkin?(panel: TraePanel): Promise<readonly TraePoolCheckinRow[]>;
3292
+ /** Run the model-availability batch and persist its measurements. */
3293
+ test?(panel: TraePanel, modelId: string, onRow?: (row: TraePoolTestRow) => void): Promise<readonly TraePoolTestRow[]>;
3294
+ /** Probe ONE account against the target model (the row's own test button). */
3295
+ testOne?(panel: TraePanel, accountId: string): Promise<TraePoolTestRow | undefined>;
3296
+ /** Forget an account's measurement — used when the user removes it from the pool. */
3297
+ removeOne?(panel: TraePanel, accountId: string): Promise<boolean>;
3298
+ /** The panel's catalog, the source of the free-model default and the stale check. */
3299
+ catalog?(panel: TraePanel): readonly TraeModelInfo[];
3300
+ }
3301
+ /** One row's cached check-in facts; see {@link TraePoolDeps.checkinState}. */
3302
+ interface TraePoolCheckinState {
3303
+ checkedIn: boolean;
3304
+ deviceCheckedIn?: boolean;
3305
+ /** Whether this account is offered the reward at all; false renders `disabled`. */
3306
+ enabled?: boolean;
2628
3307
  }
2629
3308
  /**
2630
3309
  * Assemble one region's card document. `panel` is the tab the card is on; the
@@ -2637,64 +3316,6 @@ declare function traeWebUsage(deps: TraeUsageRouteOptions, panel: TraePanel): Pr
2637
3316
  /** Mount the GET usage route on an optional webServer context. */
2638
3317
  declare function registerTraeUsageRoute(ctx: Context$1, deps: TraeUsageRouteOptions): void;
2639
3318
  //#endregion
2640
- //#region src/model-probe.d.ts
2641
- /** How one model answered. */
2642
- type TraeProbeVerdict = 'available' | 'gated' | 'unsupported' | 'rate_limited' | 'auth' | 'unknown';
2643
- interface TraeProbeResult {
2644
- /** The model id as the card knows it. */
2645
- id: string;
2646
- verdict: TraeProbeVerdict;
2647
- /** Upstream's own words (already translated), or a short note. */
2648
- detail: string;
2649
- }
2650
- /**
2651
- * How long one probe may take. Trae answers a one-word prompt in a few seconds;
2652
- * beyond this the answer is not worth waiting for and is reported as unknown.
2653
- */
2654
- declare const TRAE_PROBE_TIMEOUT_MS = 30000;
2655
- /**
2656
- * Classify a failure that arrived as a thrown stream error rather than a
2657
- * returned result.
2658
- *
2659
- * The bridge errors the stream (instead of returning a result) when a refusal
2660
- * interrupts an answer that had already started; its message is the same
2661
- * translated text, so a named code is still decisive. Anything without a
2662
- * recognised code — an abort, a socket reset, a proxy — stays `unknown`.
2663
- */
2664
- declare function classifyProbeError(message: string): {
2665
- verdict: TraeProbeVerdict;
2666
- detail: string;
2667
- };
2668
- /**
2669
- * Classify a completed upstream call.
2670
- *
2671
- * `result` is what `TraeSoloBridge.chatStream` returned, so a refusal that
2672
- * arrived inside a 200 body has already been turned into a real error result
2673
- * (and its code translated into `message`) by the bridge — this function only
2674
- * has to read it back.
2675
- */
2676
- declare function classifyProbeResult(result: TraeChatResult, outputSeen: boolean): {
2677
- verdict: TraeProbeVerdict;
2678
- detail: string;
2679
- };
2680
- /** The minimal chat call one probe makes. */
2681
- declare function probeBody(model: string): string;
2682
- /**
2683
- * Probe one model through the bridge the plugin actually chats with, so the
2684
- * recorded verdict describes the user's real path (same wire resolution, same
2685
- * function stamping, same error translation).
2686
- */
2687
- declare function probeOneModel(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, id: string, timeoutMs?: number): Promise<TraeProbeResult>;
2688
- /**
2689
- * Probe a list of models ONE AT A TIME.
2690
- *
2691
- * Sequential on purpose: Trae throttles bursts, and a burst of parallel probes
2692
- * would manufacture the very `rate_limited`/`unknown` verdicts this feature
2693
- * must avoid. An authentication failure stops the run — every later probe would
2694
- * fail the same way, and spending calls to learn that again is waste.
2695
- */
2696
- declare function probeModelsSequentially(chat: (body: string, signal?: AbortSignal) => Promise<TraeChatResult>, ids: readonly string[], onResult?: (result: TraeProbeResult, index: number) => void): Promise<TraeProbeResult[]>;
2697
- //#endregion
2698
3319
  //#region src/index.d.ts
2699
3320
  declare const name = "dsh-connect-trae";
2700
3321
  declare const inject: string[];
@@ -2766,6 +3387,15 @@ interface TraeRegionState {
2766
3387
  * beside dsh-connect-workbuddy's rows — so the setting applies here too.
2767
3388
  */
2768
3389
  creditPosition?: TraeCreditPosition;
3390
+ /**
3391
+ * This region's account pool (issue #44), as the card saves it.
3392
+ *
3393
+ * Read through {@link regionStateOf} and written by the card's pool section.
3394
+ * `enabled` absent means OFF: an account pool decides where money is spent, so
3395
+ * it is opt-in and a config written before it existed must keep billing the
3396
+ * account the user picked.
3397
+ */
3398
+ pool?: TraePoolPreferences;
2769
3399
  }
2770
3400
  interface Config {
2771
3401
  authFile?: string;
@@ -2901,4 +3531,4 @@ declare function asVolatile<S>(schema: S): S;
2901
3531
  declare const Config: z<Config>;
2902
3532
  declare function apply(ctx: Context$1, config: Config): void;
2903
3533
  //#endregion
2904
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
3534
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };