dsh-connect-workbuddy 3.6.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react_dom = require("react-dom");
9
10
  //#region src/status-paths.ts
10
11
  /**
11
12
  * Node-free constants and types shared by the Host and browser halves.
@@ -23,6 +24,18 @@ window.__ModuleLoader__.load({
23
24
  /** Plugin-owned usage endpoint consumed by its browser half. */
24
25
  const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
25
26
  /**
27
+ * Per-account credit balances for one region, read ONLY while the composer
28
+ * panel is open.
29
+ *
30
+ * Separate from {@link WORKBUDDY_USAGE_PATH} because its cost is different in
31
+ * kind: the usage document is one upstream read for the selected account, while
32
+ * this is one read PER account in the region. Folding it into the usage route
33
+ * would make every 5-minute refresh pay for a table nobody is looking at, so it
34
+ * is fetched on demand instead — the same principle the pool applies to its
35
+ * per-account figures.
36
+ */
37
+ const WORKBUDDY_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-workbuddy/account-credits";
38
+ /**
26
39
  * The project's public repository.
27
40
  *
28
41
  * Lives here rather than beside one component because TWO sections link to it
@@ -173,6 +186,32 @@ window.__ModuleLoader__.load({
173
186
  });
174
187
  }
175
188
  /**
189
+ * Whether one region's sidebar credit line is shown. Opt-out: only an explicit
190
+ * `false` hides it, so a config written before this switch existed keeps the
191
+ * line visible exactly as a fresh install does.
192
+ *
193
+ * Per-REGION by construction (it reads one region's slot), which is the point:
194
+ * WB CN and WB AI are separate accounts on separate gateways, so "show credits"
195
+ * is not one decision. `value` may be the whole settings section or the
196
+ * `regions` map itself.
197
+ */
198
+ function regionCreditsShownOf(value, region) {
199
+ return regionSlotOf(value, region)["showCreditsInMainUi"] !== false;
200
+ }
201
+ /**
202
+ * Build the next `regions` settings value for the sidebar-credit on/off toggle.
203
+ *
204
+ * Same merge rule as {@link nextRegionEnabled}: ONLY the target region's flag
205
+ * changes, so every other field of that slot (directory, selection, pool
206
+ * preferences) and every other region's slot survive the write.
207
+ */
208
+ function nextRegionCreditsShown(value, region, shown) {
209
+ return nextRegionSlots(regionsMapOf(value), region, {
210
+ ...regionSlotOf(value, region),
211
+ showCreditsInMainUi: shown
212
+ });
213
+ }
214
+ /**
176
215
  * Project one card row into its persisted `lastCatalog` shape: the native
177
216
  * context window becomes the stored `contextWindow`, and the card-only
178
217
  * presentation fields (`nativeContextWindow`, `multimodal`) are removed BY
@@ -489,6 +528,33 @@ window.__ModuleLoader__.load({
489
528
  });
490
529
  }
491
530
  /**
531
+ * Show or hide one region's sidebar credit line, with the same landed-check as
532
+ * every other settings write.
533
+ *
534
+ * Routed through {@link writeField} for exactly the reason
535
+ * {@link writeRegionEnabled} is: on the affected 0.1.7 deployments a direct
536
+ * `scope.set()` settles without storing anything, so the switch would appear to
537
+ * work and then silently revert — which for this control means the line
538
+ * reappears on the next render with no explanation.
539
+ *
540
+ * The caller passes the region's COMPLETE next slot (the same whole-slot merge
541
+ * rule as the other region writes), so hiding a line cannot drop the model
542
+ * directory, selection, image opt-ins, budgets or pool preferences that live
543
+ * alongside this flag in the same slot.
544
+ *
545
+ * @param scope - the bound settings scope for this plugin's namespace.
546
+ * @param region - the region whose line is being shown or hidden.
547
+ * @param shown - the desired state.
548
+ * @param slot - the region's COMPLETE next slot (caller owns the merge).
549
+ * @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
550
+ */
551
+ async function writeRegionCreditsShown(scope, region, shown, slot) {
552
+ await writeField(scope, "regions", region, slot, (readBack) => {
553
+ const stored = readBack;
554
+ return stored !== null && stored !== void 0 && stored["showCreditsInMainUi"] === shown;
555
+ });
556
+ }
557
+ /**
492
558
  * Write one region's account-pool PREFERENCES, verifying they landed.
493
559
  *
494
560
  * Routed through {@link writeField} like every other settings write, so it gets
@@ -533,6 +599,21 @@ window.__ModuleLoader__.load({
533
599
  });
534
600
  }
535
601
  //#endregion
602
+ //#region src/client/exclusion-text.ts
603
+ /** The exclusion label for one account, or undefined when it is usable. */
604
+ function exclusionText(t, excludedBy) {
605
+ switch (excludedBy) {
606
+ case "rate-limited": return t("row.poolExcludedRateLimited");
607
+ case "out-of-credit": return t("row.poolExcludedOutOfCredit");
608
+ case "credential-rejected": return t("row.poolExcludedRejected");
609
+ case "not-found": return t("row.probeNotFound");
610
+ case "unavailable": return t("row.probeUnavailable");
611
+ case "failed": return t("row.probeFailed");
612
+ case "unusable": return t("row.poolExcludedUnusable");
613
+ default: return;
614
+ }
615
+ }
616
+ //#endregion
536
617
  //#region src/client/ndjson.ts
537
618
  /**
538
619
  * Read a newline-delimited JSON body, one object at a time.
@@ -988,7 +1069,7 @@ window.__ModuleLoader__.load({
988
1069
  hourCycle: "h23"
989
1070
  }).format(new Date(value));
990
1071
  }
991
- function formatClock(value) {
1072
+ function formatClock$1(value) {
992
1073
  return new Intl.DateTimeFormat(void 0, {
993
1074
  hour: "2-digit",
994
1075
  minute: "2-digit",
@@ -1002,19 +1083,6 @@ window.__ModuleLoader__.load({
1002
1083
  if (account.probe === void 0) return "var(--dsw-alias-label-dimmed, #9aa0a6)";
1003
1084
  return outcomeOk(account.probe.outcome) ? "var(--dsw-alias-state-success-primary, #22a06b)" : "var(--dsw-alias-state-warn-primary, #f59e0b)";
1004
1085
  }
1005
- /** The exclusion label for one account, or undefined when it is usable. */
1006
- function exclusionText(t, account) {
1007
- switch (account.excludedBy) {
1008
- case "rate-limited": return t("row.poolExcludedRateLimited");
1009
- case "out-of-credit": return t("row.poolExcludedOutOfCredit");
1010
- case "credential-rejected": return t("row.poolExcludedRejected");
1011
- case "not-found": return t("row.probeNotFound");
1012
- case "unavailable": return t("row.probeUnavailable");
1013
- case "failed": return t("row.probeFailed");
1014
- case "unusable": return t("row.poolExcludedUnusable");
1015
- default: return;
1016
- }
1017
- }
1018
1086
  /**
1019
1087
  * The pool section.
1020
1088
  *
@@ -1430,7 +1498,7 @@ window.__ModuleLoader__.load({
1430
1498
  function renderAccountRow(input) {
1431
1499
  const { t, account, checked, canEdit, checkinSupported, onToggle } = input;
1432
1500
  const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
1433
- const excluded = exclusionText(t, account);
1501
+ const excluded = exclusionText(t, account.excludedBy);
1434
1502
  const probe = account.probe;
1435
1503
  const outcomeLine = probe === void 0 ? t("row.poolNeverTested") : probe.retryAtMs !== void 0 ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryAt", { at: formatShort(probe.retryAtMs) })}` : probe.outcome === "rate-limited" || probe.outcome === "out-of-credit" ? `${outcomeText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}` : outcomeText(t, probe.outcome);
1436
1504
  const remaining = probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now());
@@ -1550,7 +1618,7 @@ window.__ModuleLoader__.load({
1550
1618
  }, t("row.poolLogClear"))), log.length === 0 ? (0, react.createElement)("div", { className: "dsm-workbuddy-pool-log-empty" }, t("row.poolLogEmpty")) : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-log-body" }, ...log.map((entry, index) => (0, react.createElement)("div", {
1551
1619
  className: "dsm-workbuddy-pool-log-row",
1552
1620
  key: `${entry.atMs}-${index}`
1553
- }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-log-time" }, formatClock(entry.atMs)), (0, react.createElement)("span", { className: `dsm-workbuddy-pool-log-text dsm-workbuddy-pool-log-${entry.tone}` }, entry.text)))));
1621
+ }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-log-time" }, formatClock$1(entry.atMs)), (0, react.createElement)("span", { className: `dsm-workbuddy-pool-log-text dsm-workbuddy-pool-log-${entry.tone}` }, entry.text)))));
1554
1622
  }
1555
1623
  //#endregion
1556
1624
  //#region src/native-modality.ts
@@ -1666,6 +1734,12 @@ window.__ModuleLoader__.load({
1666
1734
  .dsm-workbuddy-tab-switch{display:inline-flex;align-items:center;flex:none;padding:0 8px 0 2px;cursor:pointer}
1667
1735
  .dsm-workbuddy-tab-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
1668
1736
  .dsm-workbuddy-tab-switch input:disabled{opacity:.4;cursor:default}
1737
+ /* One sidebar-credit switch, belonging to the ACTIVE tab's region. It sits on
1738
+ its own line so it reads as a property of this tab rather than as a shared
1739
+ pair of settings — the tab bar already says which region is in scope. */
1740
+ .dsm-workbuddy-credits-switch{display:flex;align-items:center;gap:6px;margin:8px 0 2px;cursor:pointer;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
1741
+ .dsm-workbuddy-credits-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
1742
+ .dsm-workbuddy-credits-switch input:disabled{opacity:.4;cursor:default}
1669
1743
  .dsm-workbuddy-tab-off{opacity:.55}
1670
1744
  /* Re-detection keeps a copy in the signed-out branch (that branch is where
1671
1745
  "I just signed in over there" applies), so it needs its own placement: the
@@ -1854,6 +1928,139 @@ window.__ModuleLoader__.load({
1854
1928
  .dsm-workbuddy-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
1855
1929
  .dsm-workbuddy-pool .sw input:disabled+.sw-track{opacity:.4}
1856
1930
  .dsm-workbuddy-pool .sw input:disabled{cursor:default}
1931
+ `;
1932
+ /**
1933
+ * Composer credit readout styles, injected by the readout component itself.
1934
+ *
1935
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
1936
+ * — 该项目的 `COMPOSER_POINTS_CSS`,类名改为 `dsm-workbuddy-*`,
1937
+ * 其余(尺寸、节奏、主题变量与回退值、表格对齐)逐条沿用。
1938
+ *
1939
+ * Separate from the card bundle because the readout lives in the session's
1940
+ * composer, not inside the plugin card; a card that is closed still shows this,
1941
+ * and on some hosts the card is never mounted at all.
1942
+ */
1943
+ const WORKBUDDY_COMPOSER_CSS = `
1944
+ /* The readout is plain composer text — no border, no fill at rest. The padding
1945
+ is only the click target, invisible until the pointer arrives. */
1946
+ .dsm-workbuddy-composer-credits{display:inline-flex;align-items:center;min-width:0}
1947
+ .dsm-workbuddy-composer-credits-trigger{
1948
+ appearance:none;display:inline-flex;align-items:center;align-self:stretch;
1949
+ padding:2px 6px;border:0;border-radius:6px;background:transparent;
1950
+ /* EXPLICIT, not inherit. The composer row's own font and body colour are
1951
+ noticeably larger and darker than the neighbouring controls — a credit
1952
+ readout is secondary information and must not shout. 12px matches this
1953
+ plugin's original line; label-tertiary matches the shell's own composer-row
1954
+ control (ContextMeter), which is the sibling this reads alongside. */
1955
+ font-size:12px;line-height:18px;
1956
+ color:var(--dsw-alias-label-tertiary,#999);
1957
+ white-space:nowrap;cursor:pointer;
1958
+ font-variant-numeric:tabular-nums;
1959
+ }
1960
+ .dsm-workbuddy-composer-credits-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14))}
1961
+ .dsm-workbuddy-composer-credits-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
1962
+ /* The panel is portaled to document.body and positioned from the trigger, so it
1963
+ MUST be fixed — otherwise the left/top it measures are ignored and it lands in
1964
+ document flow. All material comes from shell tokens so it follows the theme. */
1965
+ .dsm-workbuddy-composer-panel{
1966
+ position:fixed;z-index:1100;box-sizing:border-box;
1967
+ width:min(248px,calc(100vw - 24px));padding:12px;
1968
+ border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.28));
1969
+ border-radius:var(--dsw-radius-lg,14px);
1970
+ background:var(--dsw-specific-menu,Canvas);
1971
+ box-shadow:var(--dsw-elevation-prominent,0 8px 32px rgba(0,0,0,.22));
1972
+ backdrop-filter:var(--dsw-menu-backdrop-filter,none);
1973
+ font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#444);
1974
+ }
1975
+ .dsm-workbuddy-composer-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
1976
+ .dsm-workbuddy-composer-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,CanvasText)}
1977
+ /* Self-contained: the dsm-btn primitive lives in TRAE_CARD_CSS, which the
1978
+ composer never loads, so the refresh button must style itself. */
1979
+ .dsm-workbuddy-composer-panel-refresh{
1980
+ appearance:none;flex:none;margin-left:auto;padding:3px 10px;
1981
+ border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.34));
1982
+ border-radius:7px;background:transparent;
1983
+ color:var(--dsw-alias-label-secondary,#444);
1984
+ font:inherit;font-size:11px;line-height:16px;cursor:pointer;
1985
+ }
1986
+ .dsm-workbuddy-composer-panel-refresh:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed,rgba(127,127,127,.6));color:var(--dsw-alias-label-primary,CanvasText)}
1987
+ .dsm-workbuddy-composer-panel-refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
1988
+ .dsm-workbuddy-composer-panel-refresh:disabled{opacity:.5;cursor:default}
1989
+ /* The panel is a TABLE: one row per account in the region, one figure (the
1990
+ general balance) per row, and a row click switches accounts.
1991
+
1992
+ Rhythm is deliberate: the header sits on a hairline, the switch button owns
1993
+ ALL horizontal padding (and pulls back by the same amount so its label lines
1994
+ up with the header), and every row is the same height. A misaligned first
1995
+ column is what makes a two-column popover read as broken. */
1996
+ .dsm-workbuddy-composer-panel-table{
1997
+ width:100%;border-collapse:collapse;
1998
+ font-size:12px;line-height:18px;
1999
+ }
2000
+ .dsm-workbuddy-composer-panel-table thead th{
2001
+ padding:0 0 7px;font-weight:500;text-align:left;font-size:11px;
2002
+ color:var(--dsw-alias-label-tertiary,GrayText);
2003
+ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));
2004
+ }
2005
+ .dsm-workbuddy-composer-panel-num{
2006
+ text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;
2007
+ }
2008
+ .dsm-workbuddy-composer-panel-table thead th.dsm-workbuddy-composer-panel-num{padding-right:0}
2009
+ .dsm-workbuddy-composer-panel-table tbody th{
2010
+ padding:0;font-weight:400;text-align:left;vertical-align:middle;
2011
+ }
2012
+ .dsm-workbuddy-composer-panel-table tbody td{
2013
+ padding:4px 0;vertical-align:middle;
2014
+ color:var(--dsw-alias-label-primary,CanvasText);
2015
+ }
2016
+ /* The current account reads as current without shouting: a filled dot and a
2017
+ slightly heavier weight. Disabled — pressing it would write a selection that
2018
+ has not changed. */
2019
+ .dsm-workbuddy-composer-panel-table tbody tr.dsm-workbuddy-composer-panel-row-current td{font-weight:600}
2020
+ .dsm-workbuddy-composer-panel-switch{
2021
+ appearance:none;display:flex;align-items:center;gap:7px;
2022
+ width:100%;padding:4px 6px;margin-left:-6px;
2023
+ border:0;border-radius:6px;background:transparent;
2024
+ color:inherit;font:inherit;font-size:12px;text-align:left;cursor:pointer;
2025
+ white-space:nowrap;overflow:hidden;
2026
+ }
2027
+ .dsm-workbuddy-composer-panel-switch>span:last-child{overflow:hidden;text-overflow:ellipsis}
2028
+ /* The name + optional reason mark. A COLUMN so the mark drops onto its own line
2029
+ under the name: side by side they would stretch the name column and squeeze
2030
+ the numeric one, and the switch's own overflow rule would ellipsise the mark
2031
+ instead of the name. */
2032
+ .dsm-workbuddy-composer-panel-who{display:flex;flex-direction:column;min-width:0;gap:1px}
2033
+ .dsm-workbuddy-composer-panel-name{overflow:hidden;text-overflow:ellipsis}
2034
+ /* Why the account cannot serve right now ("被限流"), in the warn colour the pool
2035
+ table uses for the same fact — so one measurement reads the same on both. */
2036
+ .dsm-workbuddy-composer-panel-mark{
2037
+ font-size:11px;line-height:15px;
2038
+ color:var(--dsw-alias-state-warn-primary,#f59e0b);
2039
+ }
2040
+ /* Hover belongs to the switch, not the row: the number cell is not clickable,
2041
+ so lighting it up would promise an action it cannot perform. */
2042
+ .dsm-workbuddy-composer-panel-switch:hover:not(:disabled){
2043
+ background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));
2044
+ }
2045
+ .dsm-workbuddy-composer-panel-switch:focus-visible{
2046
+ outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:-1px;
2047
+ }
2048
+ .dsm-workbuddy-composer-panel-switch:disabled{cursor:default}
2049
+ .dsm-workbuddy-composer-panel-dot{
2050
+ flex:none;width:7px;height:7px;border-radius:50%;
2051
+ background:var(--dsw-alias-label-dimmed,rgba(127,127,127,.5));
2052
+ }
2053
+ .dsm-workbuddy-composer-panel-table tr.dsm-workbuddy-composer-panel-row-current .dsm-workbuddy-composer-panel-dot{
2054
+ background:var(--dsw-state-success,#22a06b);
2055
+ }
2056
+ .dsm-workbuddy-composer-panel-foot{
2057
+ display:flex;justify-content:space-between;gap:8px;
2058
+ margin-top:9px;padding-top:7px;
2059
+ border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));
2060
+ font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,GrayText);
2061
+ }
2062
+ .dsm-workbuddy-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
2063
+ .dsm-workbuddy-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
1857
2064
  `;
1858
2065
  //#endregion
1859
2066
  //#region src/client/searched-paths.ts
@@ -2233,6 +2440,14 @@ window.__ModuleLoader__.load({
2233
2440
  /** Region whose on/off checkbox write is in flight, so its box can't race. */
2234
2441
  const [togglingRegion, setTogglingRegion] = (0, react.useState)(void 0);
2235
2442
  /**
2443
+ * Region whose SIDEBAR-credit checkbox write is in flight.
2444
+ *
2445
+ * Tracked separately from {@link togglingRegion} because they are different
2446
+ * fields of the same region slot: one in-flight write must not disable the
2447
+ * other control, and both must stay serialized against the SAVE writers.
2448
+ */
2449
+ const [togglingCredits, setTogglingCredits] = (0, react.useState)(void 0);
2450
+ /**
2236
2451
  * Probe results, keyed by region then model id.
2237
2452
  *
2238
2453
  * Kept per region for the same reason drafts are: the two tabs are separate
@@ -2425,6 +2640,21 @@ window.__ModuleLoader__.load({
2425
2640
  };
2426
2641
  const activeRegionOn = regionOn(activeRegion);
2427
2642
  /**
2643
+ * Whether one region's sidebar credit line is shown — for the ACTIVE tab.
2644
+ *
2645
+ * Prefers the Host's answer, exactly like {@link regionOn} and for the same
2646
+ * measured reason: on the affected 0.1.7 deployment the browser's settings
2647
+ * mirror never picks up this plugin's writes, so a checkbox reading the mirror
2648
+ * rendered its previous value after a successful save and looked impossible to
2649
+ * turn off ("关闭不了"). The Host is the authority; the mirror is only a
2650
+ * fallback for a Host line that predates the field.
2651
+ */
2652
+ const creditsShownOn = (item) => {
2653
+ const fromHost = statusByRegion[item]?.showCreditsInMainUi;
2654
+ if (typeof fromHost === "boolean") return fromHost;
2655
+ return regionCreditsShownOf(settingsScope?.getSnapshot().value, item);
2656
+ };
2657
+ /**
2428
2658
  * Switch one region's provider off or on. The write carries the region's
2429
2659
  * whole slot through untouched — only `enabled` changes — so the user's
2430
2660
  * directory, model picks, image opt-ins and budgets survive a round trip.
@@ -2450,6 +2680,32 @@ window.__ModuleLoader__.load({
2450
2680
  if (mounted.current) setTogglingRegion(void 0);
2451
2681
  }
2452
2682
  };
2683
+ /**
2684
+ * Show or hide one region's sidebar credit line.
2685
+ *
2686
+ * This is the SECOND control for that line: the sidebar row also carries an
2687
+ * `×` that writes the same `regions.<region>.showCreditsInMainUi` flag, and
2688
+ * both go through the same landed-checked writer, so they can never disagree.
2689
+ * The checkbox stays because the sidebar is not always rendered (collapsed
2690
+ * rail, or a host that never mounts the footer slot).
2691
+ *
2692
+ * Writes the region's WHOLE slot — unchanged except for this one flag — so a
2693
+ * hide never drops the directory, model picks, budgets or pool preferences
2694
+ * that live alongside it.
2695
+ */
2696
+ const toggleCredits = async (item, shown) => {
2697
+ if (settingsScope === void 0) return;
2698
+ setTogglingCredits(item);
2699
+ try {
2700
+ const slot = nextRegionCreditsShown(settingsScope.getSnapshot().value, item, shown)[item];
2701
+ await writeRegionCreditsShown(settingsScope, item, shown, typeof slot === "object" && slot !== null ? slot : {});
2702
+ await refreshUsage(item);
2703
+ } catch (error) {
2704
+ if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
2705
+ } finally {
2706
+ if (mounted.current) setTogglingCredits(void 0);
2707
+ }
2708
+ };
2453
2709
  const refreshModels = async () => {
2454
2710
  setBusy(true);
2455
2711
  try {
@@ -2773,6 +3029,18 @@ window.__ModuleLoader__.load({
2773
3029
  className: "dsm-workbuddy-tab-off-notice",
2774
3030
  children: t("row.tabOffNotice")
2775
3031
  }) : null,
3032
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3033
+ className: "dsm-workbuddy-credits-switch",
3034
+ title: activeRegion === "cn" ? t("row.showCreditsCnHint") : t("row.showCreditsGlobalHint"),
3035
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3036
+ type: "checkbox",
3037
+ checked: activeRegionOn ? creditsShownOn(activeRegion) : false,
3038
+ disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
3039
+ onChange: (event) => {
3040
+ toggleCredits(activeRegion, event.target.checked);
3041
+ }
3042
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activeRegion === "cn" ? t("row.showCreditsCn") : t("row.showCreditsGlobal") })]
3043
+ }),
2776
3044
  status.status === "error" || status.status === "signed-in" && status.creditsError !== void 0 ? status.status === "signed-in" && status.credentialRejected === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2777
3045
  className: "dsm-workbuddy-usage-hint",
2778
3046
  children: t("row.reloginHint")
@@ -3076,6 +3344,565 @@ window.__ModuleLoader__.load({
3076
3344
  });
3077
3345
  }
3078
3346
  //#endregion
3347
+ //#region src/client/popover.ts
3348
+ /**
3349
+ * The two shell behaviours the composer credit panel needs: hanging the panel
3350
+ * off its trigger, and dismissing it on an outside press.
3351
+ *
3352
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
3353
+ * — 该项目的 `client/popover.ts`,逐条沿用其判断:同样的上下放置、同样的
3354
+ * 视口收边、同样的 scroll(capture)/resize/自身尺寸变化重测、同样的
3355
+ * 「pointerdown 落在触发器与面板之外即关闭」。这些不是随手写的——那边
3356
+ * 2.12.0/2.13.0 的客户端加载失败与面板位置错乱都留下了记录,这里直接
3357
+ * 采用其最终结论。
3358
+ *
3359
+ * Implemented locally rather than imported from
3360
+ * `@deepseek-ai/dsh-client-ui-primitives`. The shell DOES register that package
3361
+ * for plugin bundles, so importing it is legal — this is a deliberate choice,
3362
+ * not a workaround: the two behaviours are ~40 lines of DOM geometry, and owning
3363
+ * them keeps this component renderable in the Node test suite without mocking a
3364
+ * package that itself reaches `*.module.css` and further DSH dependencies.
3365
+ *
3366
+ * NOTE ON A MISDIAGNOSIS, kept because it is the kind of thing that gets
3367
+ * re-litigated: the client load failure on the referring project was first
3368
+ * attributed to importing the primitives package. It was not that. The real
3369
+ * cause was `import { createPortal } from 'react-dom'` being BUNDLED rather than
3370
+ * externalized — the inlined development build opens with
3371
+ * `process.env.NODE_ENV` and threw `ReferenceError: process is not defined` in
3372
+ * the browser, at module-evaluation time, so the ENTIRE client half failed to
3373
+ * import. See `tsdown.config.ts` (`CLIENT_EXTERNALS`) for the fix and
3374
+ * `tests/client-runtime-imports.spec.ts` for the rule that now pins it.
3375
+ */
3376
+ /**
3377
+ * Clearance to keep from the top of the viewport.
3378
+ *
3379
+ * The shell reserves a strip at the top of the frame
3380
+ * (`--dsh-frame-top-clearance`) for the window chrome, so a panel clamped to
3381
+ * `margin` alone can still land under it. Absent variable or no `data-fullscreen`
3382
+ * distinction is handled the same way the shell does: fall back to `min`.
3383
+ */
3384
+ function overlayTopMargin(min) {
3385
+ if (typeof document === "undefined") return min;
3386
+ const root = document.documentElement;
3387
+ const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue("--dsh-frame-top-clearance"));
3388
+ if (Number.isNaN(clearance)) return min;
3389
+ return Math.max(min, (root.hasAttribute("data-fullscreen") ? 0 : clearance) + 20);
3390
+ }
3391
+ /** Keep a `position: fixed` floating element anchored to a trigger. */
3392
+ function useAnchoredPosition(options) {
3393
+ const { open, anchorRef, panelRef, side = "bottom", align = "start", gap, margin } = options;
3394
+ const [position, setPosition] = (0, react.useState)(null);
3395
+ (0, react.useLayoutEffect)(() => {
3396
+ if (!open) {
3397
+ setPosition(null);
3398
+ return;
3399
+ }
3400
+ const place = () => {
3401
+ const rect = anchorRef.current?.getBoundingClientRect();
3402
+ if (rect === void 0) return;
3403
+ const panel = panelRef.current;
3404
+ const width = panel?.offsetWidth ?? 0;
3405
+ const height = panel?.offsetHeight ?? 0;
3406
+ let left = align === "end" ? rect.right - width : rect.left;
3407
+ let top = side === "top" ? rect.top - gap - height : rect.bottom + gap;
3408
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
3409
+ if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin);
3410
+ setPosition({
3411
+ left,
3412
+ top
3413
+ });
3414
+ };
3415
+ place();
3416
+ window.addEventListener("scroll", place, true);
3417
+ window.addEventListener("resize", place);
3418
+ const panel = panelRef.current;
3419
+ let observer = null;
3420
+ if (typeof ResizeObserver !== "undefined" && panel !== null) {
3421
+ observer = new ResizeObserver(place);
3422
+ observer.observe(panel);
3423
+ }
3424
+ return () => {
3425
+ observer?.disconnect();
3426
+ window.removeEventListener("scroll", place, true);
3427
+ window.removeEventListener("resize", place);
3428
+ };
3429
+ }, [
3430
+ open,
3431
+ anchorRef,
3432
+ panelRef,
3433
+ side,
3434
+ align,
3435
+ gap,
3436
+ margin
3437
+ ]);
3438
+ return position;
3439
+ }
3440
+ /**
3441
+ * Close an open popover when a pointerdown lands outside it.
3442
+ *
3443
+ * `portal` counts as inside: the panel is portaled to `document.body`, so it is
3444
+ * not a descendant of `root` and a click on the panel itself would otherwise
3445
+ * read as "outside" and dismiss the popover the user is trying to use.
3446
+ */
3447
+ function useDismissOnOutsidePointer(root, open, setOpen, portal) {
3448
+ (0, react.useEffect)(() => {
3449
+ if (!open) return void 0;
3450
+ const closeOutside = (event) => {
3451
+ if (event.target instanceof Node && root.current?.contains(event.target) !== true && portal?.current?.contains(event.target) !== true) setOpen(false);
3452
+ };
3453
+ document.addEventListener("pointerdown", closeOutside);
3454
+ return () => {
3455
+ document.removeEventListener("pointerdown", closeOutside);
3456
+ };
3457
+ }, [
3458
+ root,
3459
+ open,
3460
+ setOpen,
3461
+ portal
3462
+ ]);
3463
+ }
3464
+ //#endregion
3465
+ //#region src/client/ComposerCredits.tsx
3466
+ /**
3467
+ * Credit readout for the composer tool row (`conversation.input.left`).
3468
+ *
3469
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
3470
+ * — 该项目的 `client/ComposerPoints.tsx`:读数形态(`<区域> · <余额>`)、
3471
+ * 点击弹出锚定面板、面板内是**账号表格**(点行即切换账号)、刷新按钮
3472
+ * 放在面板里而不是读数上、每账号读数**只在面板打开时抓**、5 分钟定时器、
3473
+ * 失败保留上次数值。本文件把「只有国内版」扩成「两个区域都支持」:那边
3474
+ * 的国际版是订阅制、没有可比数字,本插件的国际版有真实余额,所以按当前
3475
+ * 选中模型所属的区域显示,并在读数上标明是 WB CN 还是 WB AI。
3476
+ *
3477
+ * Shows the selected region's general credit balance, but ONLY while the
3478
+ * session's selected model belongs to this plugin (the gate decides that).
3479
+ *
3480
+ * The row itself is a bare clickable readout — `WB CN · 1,072` — with NO
3481
+ * refresh control: the manual refresh lives in the panel, because the row is
3482
+ * shared with the shell's own permission/agent/model controls and every extra
3483
+ * control there costs space that is not ours. Clicking it opens an anchored
3484
+ * panel: a TABLE with one row per account in that region and its balance, where
3485
+ * a row click switches to it.
3486
+ *
3487
+ * Two deliberate choices in that panel:
3488
+ *
3489
+ * - Per-account figures are fetched ONLY while the panel is open. The 5-minute
3490
+ * readout must not multiply its upstream reads by the account count — a
3491
+ * healthy balance should not pay for a table nobody is looking at.
3492
+ * - A row whose balance could not be read shows `—`, never `0`. The endpoint
3493
+ * omits the field in that case precisely so this component cannot render a
3494
+ * wrong claim about the account.
3495
+ */
3496
+ /**
3497
+ * Inject the readout's styles into the document head, once, on module load.
3498
+ *
3499
+ * Same idempotent pattern as the card's injection, and for the same reason: the
3500
+ * classes are attached to DOM this component owns, and `WORKBUDDY_CARD_CSS` only
3501
+ * loads when the settings card does — which is not guaranteed on a host where
3502
+ * the card is never rendered. The referring project shipped exactly that bug
3503
+ * (the trigger rendered as a default button box and the panel, without
3504
+ * `position: fixed`, had its measured left/top ignored and appeared nowhere).
3505
+ */
3506
+ if (typeof document !== "undefined") {
3507
+ const cssId = "dsh-connect-workbuddy/composer-credits.css";
3508
+ if (!document.querySelector(`style[data-plugin-css="${cssId}"]`)) {
3509
+ const styleTag = document.createElement("style");
3510
+ styleTag.dataset.plugin = "dsh-connect-workbuddy";
3511
+ styleTag.dataset.pluginCss = cssId;
3512
+ styleTag.textContent = WORKBUDDY_COMPOSER_CSS;
3513
+ document.head.appendChild(styleTag);
3514
+ }
3515
+ }
3516
+ /**
3517
+ * Automatic refresh period. Five minutes, matching a credit balance's actual
3518
+ * rate of change: it moves on the scale of model calls, not seconds, and each
3519
+ * read is a read-only upstream call.
3520
+ */
3521
+ const COMPOSER_CREDITS_REFRESH_INTERVAL_MS = 3e5;
3522
+ /**
3523
+ * Provider routes whose credit balance this readout can show, mapped to the
3524
+ * region to read it from.
3525
+ *
3526
+ * BOTH regions, unlike the referring project: its international side is
3527
+ * subscription-based and has no comparable balance, while WorkBuddy's
3528
+ * international accounts do carry one, so there is a real number to show on
3529
+ * either side. Mirrors `WORKBUDDY_PROVIDERS` on the host side; duplicated here
3530
+ * because the client bundle must not import the host entry.
3531
+ */
3532
+ const WORKBUDDY_COMPOSER_PROVIDERS = {
3533
+ "workbuddy": "cn",
3534
+ "workbuddy-global": "global"
3535
+ };
3536
+ /** The credit total out of one usage document, or undefined. */
3537
+ function creditsOf(usage) {
3538
+ if (usage.status !== "signed-in") return void 0;
3539
+ return usage.credits?.total;
3540
+ }
3541
+ /** Group the digits so a four-figure balance stays readable in a narrow row. */
3542
+ function formatCredits(value) {
3543
+ return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
3544
+ }
3545
+ function formatClock(value) {
3546
+ return new Date(value).toLocaleTimeString("zh-CN", {
3547
+ hour: "2-digit",
3548
+ minute: "2-digit"
3549
+ });
3550
+ }
3551
+ function ComposerCredits(props) {
3552
+ const { t, provider, region, settingsScope } = props;
3553
+ if (t === void 0) throw new Error("Composer credits readout requires its translation function");
3554
+ const owned = provider === void 0 ? void 0 : WORKBUDDY_COMPOSER_PROVIDERS[provider];
3555
+ const activeRegion = region ?? owned;
3556
+ const [usage, setUsage] = (0, react.useState)(void 0);
3557
+ const [accountCredits, setAccountCredits] = (0, react.useState)(void 0);
3558
+ const [busy, setBusy] = (0, react.useState)(false);
3559
+ const [switchingId, setSwitchingId] = (0, react.useState)(void 0);
3560
+ const [failed, setFailed] = (0, react.useState)(false);
3561
+ const [lastRefresh, setLastRefresh] = (0, react.useState)(void 0);
3562
+ const [open, setOpen] = (0, react.useState)(false);
3563
+ const inFlight = (0, react.useRef)(false);
3564
+ const mounted = (0, react.useRef)(true);
3565
+ const rootRef = (0, react.useRef)(null);
3566
+ const panelRef = (0, react.useRef)(null);
3567
+ (0, react.useEffect)(() => {
3568
+ mounted.current = true;
3569
+ return () => {
3570
+ mounted.current = false;
3571
+ };
3572
+ }, []);
3573
+ const position = useAnchoredPosition({
3574
+ open: open && activeRegion !== void 0,
3575
+ anchorRef: rootRef,
3576
+ panelRef,
3577
+ side: "top",
3578
+ gap: 8,
3579
+ margin: 12
3580
+ });
3581
+ useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
3582
+ const fetchUsage = (0, react.useCallback)(async (signal) => {
3583
+ if (inFlight.current || activeRegion === void 0) return;
3584
+ inFlight.current = true;
3585
+ if (mounted.current) setBusy(true);
3586
+ try {
3587
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, activeRegion), {
3588
+ headers: { accept: "application/json" },
3589
+ credentials: "same-origin",
3590
+ ...signal === void 0 ? {} : { signal }
3591
+ });
3592
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
3593
+ const value = await response.json();
3594
+ if (!mounted.current || signal?.aborted === true) return;
3595
+ setUsage(value);
3596
+ setLastRefresh(Date.now());
3597
+ setFailed(false);
3598
+ } catch {
3599
+ if (mounted.current && signal?.aborted !== true) setFailed(true);
3600
+ } finally {
3601
+ inFlight.current = false;
3602
+ if (mounted.current) setBusy(false);
3603
+ }
3604
+ }, [activeRegion]);
3605
+ /**
3606
+ * Every account's balance in the region — one upstream read per account.
3607
+ *
3608
+ * Paid only while the panel is open (see the note at the top of this file).
3609
+ * A failure keeps whatever we already have rather than clearing it: the table
3610
+ * falls back to the usage document's account list, so a failed figure must not
3611
+ * remove the switch.
3612
+ */
3613
+ const fetchAccountCredits = (0, react.useCallback)(async (signal) => {
3614
+ if (activeRegion === void 0) return;
3615
+ try {
3616
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNT_CREDITS_PATH, activeRegion), {
3617
+ headers: { accept: "application/json" },
3618
+ credentials: "same-origin",
3619
+ ...signal === void 0 ? {} : { signal }
3620
+ });
3621
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
3622
+ const data = await response.json();
3623
+ if (!mounted.current || signal?.aborted === true) return;
3624
+ if (Array.isArray(data.accounts)) setAccountCredits(data.accounts);
3625
+ } catch {}
3626
+ }, [activeRegion]);
3627
+ (0, react.useEffect)(() => {
3628
+ if (owned === void 0) return void 0;
3629
+ let cancelled = false;
3630
+ let timer;
3631
+ const controller = new AbortController();
3632
+ fetchUsage(controller.signal).finally(() => {
3633
+ if (cancelled) return;
3634
+ timer = window.setInterval(() => {
3635
+ fetchUsage(controller.signal);
3636
+ }, COMPOSER_CREDITS_REFRESH_INTERVAL_MS);
3637
+ });
3638
+ return () => {
3639
+ cancelled = true;
3640
+ if (timer !== void 0) window.clearInterval(timer);
3641
+ controller.abort();
3642
+ };
3643
+ }, [owned, fetchUsage]);
3644
+ (0, react.useEffect)(() => {
3645
+ if (!open || activeRegion === void 0) return void 0;
3646
+ const controller = new AbortController();
3647
+ fetchAccountCredits(controller.signal);
3648
+ return () => {
3649
+ controller.abort();
3650
+ };
3651
+ }, [
3652
+ open,
3653
+ activeRegion,
3654
+ fetchAccountCredits
3655
+ ]);
3656
+ (0, react.useEffect)(() => {
3657
+ if (!open) return void 0;
3658
+ const onKeyDown = (event) => {
3659
+ if (event.key === "Escape") setOpen(false);
3660
+ };
3661
+ document.addEventListener("keydown", onKeyDown);
3662
+ return () => {
3663
+ document.removeEventListener("keydown", onKeyDown);
3664
+ };
3665
+ }, [open]);
3666
+ /**
3667
+ * Switch to another account, then re-read both the usage document and the
3668
+ * table so the marker, the readout and the panel all move together.
3669
+ *
3670
+ * Goes through the shared `writeAccountSlot`, which is read-back checked and
3671
+ * carries the host-endpoint-first path: `set()` resolving is not proof the
3672
+ * write landed, and a refused write must report failure instead of showing a
3673
+ * marker on an account that was never selected.
3674
+ */
3675
+ const switchTo = async (accountId) => {
3676
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
3677
+ if (activeRegion === void 0 || switchingId !== void 0) return;
3678
+ if (accountId === (accountCredits?.find((account) => account.selected)?.id ?? (usage?.status === "signed-in" ? usage.accountId : void 0))) return;
3679
+ setSwitchingId(accountId);
3680
+ setFailed(false);
3681
+ try {
3682
+ await writeAccountSlot(settingsScope, activeRegion, accountId);
3683
+ await fetchUsage();
3684
+ await fetchAccountCredits();
3685
+ } catch {
3686
+ if (mounted.current) setFailed(true);
3687
+ } finally {
3688
+ if (mounted.current) setSwitchingId(void 0);
3689
+ }
3690
+ };
3691
+ if (owned === void 0) return null;
3692
+ const signedIn = usage?.status === "signed-in" ? usage : void 0;
3693
+ const total = signedIn === void 0 ? void 0 : creditsOf(signedIn);
3694
+ const signedOut = usage !== void 0 && usage.status === "signed-out";
3695
+ const valueText = total === void 0 ? "—" : formatCredits(total);
3696
+ const label = t(activeRegion === "cn" ? "composer.pointsCn" : "composer.pointsGlobal");
3697
+ /**
3698
+ * The table's rows: every account in this region, with its balance.
3699
+ *
3700
+ * The endpoint's answer is authoritative (it carries the figures). Before it
3701
+ * lands — or if it fails — fall back to the usage document's account list so
3702
+ * the switches are reachable immediately, showing `—` for the figure rather
3703
+ * than a spinner the user has to wait on.
3704
+ */
3705
+ const selectedId = accountCredits?.find((account) => account.selected)?.id ?? (signedIn === void 0 ? void 0 : signedIn.accountId);
3706
+ const rows = (accountCredits ?? (signedIn === void 0 ? [] : signedIn.accounts.map((account) => ({
3707
+ id: account.id,
3708
+ accountName: account.accountName,
3709
+ selected: account.selected
3710
+ })))).map((row) => {
3711
+ if (row.id !== selectedId || row.credits !== void 0 || total === void 0) return row;
3712
+ return {
3713
+ ...row,
3714
+ credits: total
3715
+ };
3716
+ });
3717
+ const canSwitch = settingsScope !== void 0 && settingsScope.getSnapshot().writable === true;
3718
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3719
+ ref: rootRef,
3720
+ className: "dsm-workbuddy-composer-credits",
3721
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3722
+ type: "button",
3723
+ className: "dsm-workbuddy-composer-credits-trigger",
3724
+ "aria-haspopup": "dialog",
3725
+ "aria-expanded": open,
3726
+ onClick: () => {
3727
+ setOpen(!open);
3728
+ },
3729
+ children: [
3730
+ label,
3731
+ " · ",
3732
+ valueText
3733
+ ]
3734
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3735
+ ref: panelRef,
3736
+ className: "dsm-workbuddy-composer-panel",
3737
+ style: position ?? {
3738
+ visibility: "hidden",
3739
+ left: 0,
3740
+ top: 0
3741
+ },
3742
+ role: "dialog",
3743
+ "aria-label": t("composer.panelTitle"),
3744
+ children: [
3745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3746
+ className: "dsm-workbuddy-composer-panel-head",
3747
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3748
+ className: "dsm-workbuddy-composer-panel-title",
3749
+ children: t(activeRegion === "cn" ? "composer.panelTitleCn" : "composer.panelTitleGlobal")
3750
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3751
+ type: "button",
3752
+ className: "dsm-workbuddy-composer-panel-refresh",
3753
+ disabled: busy || switchingId !== void 0,
3754
+ onClick: () => {
3755
+ fetchUsage();
3756
+ },
3757
+ children: busy ? t("composer.refreshing") : t("composer.refresh")
3758
+ })]
3759
+ }),
3760
+ signedOut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3761
+ className: "dsm-workbuddy-composer-panel-empty",
3762
+ children: t("composer.signedOut")
3763
+ }) : rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3764
+ className: "dsm-workbuddy-composer-panel-empty",
3765
+ children: t("composer.noAccounts")
3766
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
3767
+ className: "dsm-workbuddy-composer-panel-table",
3768
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
3769
+ scope: "col",
3770
+ children: t("composer.account")
3771
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
3772
+ scope: "col",
3773
+ className: "dsm-workbuddy-composer-panel-num",
3774
+ children: t("composer.credits")
3775
+ })] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row) => {
3776
+ const current = row.id === selectedId;
3777
+ const excluded = exclusionText(t, row.excludedBy);
3778
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
3779
+ className: current ? "dsm-workbuddy-composer-panel-row-current" : void 0,
3780
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
3781
+ scope: "row",
3782
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3783
+ type: "button",
3784
+ className: "dsm-workbuddy-composer-panel-switch",
3785
+ disabled: !canSwitch || switchingId !== void 0 || current,
3786
+ "aria-label": t("composer.switchTo", { account: row.accountName }),
3787
+ onClick: () => {
3788
+ switchTo(row.id);
3789
+ },
3790
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3791
+ className: "dsm-workbuddy-composer-panel-dot",
3792
+ "aria-hidden": "true"
3793
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3794
+ className: "dsm-workbuddy-composer-panel-who",
3795
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3796
+ className: "dsm-workbuddy-composer-panel-name",
3797
+ children: row.accountName
3798
+ }), excluded === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3799
+ className: "dsm-workbuddy-composer-panel-mark",
3800
+ children: excluded
3801
+ })]
3802
+ })]
3803
+ })
3804
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
3805
+ className: "dsm-workbuddy-composer-panel-num",
3806
+ children: row.credits === void 0 ? "—" : formatCredits(row.credits)
3807
+ })]
3808
+ }, row.id);
3809
+ }) })]
3810
+ }),
3811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3812
+ className: "dsm-workbuddy-composer-panel-foot",
3813
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
3814
+ t("composer.lastRefresh"),
3815
+ " ",
3816
+ lastRefresh === void 0 ? "—" : formatClock(lastRefresh)
3817
+ ] })
3818
+ }),
3819
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3820
+ className: "dsm-workbuddy-composer-panel-error",
3821
+ role: "status",
3822
+ children: t("composer.refreshFailed")
3823
+ }) : null
3824
+ ]
3825
+ }), document.body)]
3826
+ });
3827
+ }
3828
+ //#endregion
3829
+ //#region src/client/ComposerCreditsGate.tsx
3830
+ /**
3831
+ * Decides whether the composer credit readout belongs in this session, by
3832
+ * reading the Host's own `modelSelection` projection and the plugin's
3833
+ * per-region `showCreditsInMainUi` switch.
3834
+ *
3835
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
3836
+ * — 该项目的 `client/ComposerPointsGate.tsx`:从 `modelSelection` 投影读
3837
+ * 「本会话当前用哪个 provider」来决定要不要渲染,以及 `next` 优先于
3838
+ * `lastUsed` 的判定。这是本项目从侧边栏迁到输入框的核心机制。
3839
+ *
3840
+ * WHY the projection rather than this plugin's own state: the projection is
3841
+ * what the model picker writes and what the next request will use, so the
3842
+ * readout cannot disagree with the model actually in effect. `next` outranks
3843
+ * `lastUsed` because a selection that has been made but not yet sent is still
3844
+ * what the composer shows the user — the same precedence the shell itself uses.
3845
+ *
3846
+ * WHY the composer row rather than the sidebar foot: the sidebar foot is a
3847
+ * SINGLE root-scope row shared by every bundle, so two connector plugins each
3848
+ * parking a line there fight over the same width (measured on the referring
3849
+ * project: its line was squeezed to a clipped "Tr" beside this plugin's two
3850
+ * rows). The composer row is per-session and provider-scoped, so exactly one
3851
+ * plugin's credits are relevant at a time and nothing overlaps.
3852
+ *
3853
+ * Kept separate from {@link ComposerCredits} (and from the browser-plugin
3854
+ * entry) so the decision can be unit-tested without a Host: the entry imports
3855
+ * browser-only DSH packages the test environment cannot load, while this module
3856
+ * depends only on React and a narrow projection shape.
3857
+ */
3858
+ /**
3859
+ * The provider route the session is currently pointed at.
3860
+ *
3861
+ * `next` wins over `lastUsed`: a switch that has landed but not yet been sent
3862
+ * is still the selection the composer displays.
3863
+ */
3864
+ function selectedProviderOf(projection) {
3865
+ const provider = (projection?.next ?? projection?.lastUsed)?.provider;
3866
+ return typeof provider === "string" && provider !== "" ? provider : void 0;
3867
+ }
3868
+ /**
3869
+ * Which region's readout this session should show, or undefined for none.
3870
+ *
3871
+ * Returns undefined when the selected provider is not this plugin's — the
3872
+ * ownership test and the region choice are ONE lookup, so there is no second
3873
+ * copy of the rule to drift.
3874
+ *
3875
+ * The region's own switches are applied HERE rather than inside the component,
3876
+ * so a region that is switched off (or whose provider is off) starts no fetch
3877
+ * loop at all.
3878
+ */
3879
+ function composerCreditsRegionOf(projection, scope) {
3880
+ const provider = selectedProviderOf(projection);
3881
+ const region = provider === void 0 ? void 0 : WORKBUDDY_COMPOSER_PROVIDERS[provider];
3882
+ if (region === void 0) return void 0;
3883
+ const value = scope === void 0 ? void 0 : unwrapVolatileDeep(scope.getSnapshot().value);
3884
+ if (!regionCreditsShownOf(value, region)) return void 0;
3885
+ if (!regionEnabledOf(value, region)) return void 0;
3886
+ return region;
3887
+ }
3888
+ function ComposerCreditsGate(props) {
3889
+ const { t, settingsScope, useProjection } = props;
3890
+ const [, setRevision] = (0, react.useState)(0);
3891
+ (0, react.useEffect)(() => settingsScope?.subscribe(() => {
3892
+ setRevision((value) => value + 1);
3893
+ }), [settingsScope]);
3894
+ const projection = useProjection === void 0 ? void 0 : useProjection("modelSelection");
3895
+ const region = composerCreditsRegionOf(projection, settingsScope);
3896
+ if (region === void 0) return null;
3897
+ const provider = selectedProviderOf(projection);
3898
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerCredits, {
3899
+ t,
3900
+ provider,
3901
+ region,
3902
+ ...settingsScope === void 0 ? {} : { settingsScope }
3903
+ });
3904
+ }
3905
+ //#endregion
3079
3906
  //#region src/client/locales.ts
3080
3907
  /**
3081
3908
  * Plugin-card copy registered under the settings.workbuddy locale namespace.
@@ -3276,7 +4103,32 @@ window.__ModuleLoader__.load({
3276
4103
  "row.poolCurrentBadge": "In use",
3277
4104
  "row.poolCurrentHeader": "In use: {account}",
3278
4105
  "row.poolNotMember": "Not in pool",
3279
- "row.poolUnsavedMembers": "Selection changed — save to apply"
4106
+ "row.poolUnsavedMembers": "Selection changed — save to apply",
4107
+ "row.showCreditsCn": "Show WB CN credits in the composer",
4108
+ "row.showCreditsGlobal": "Show WB AI credits in the composer",
4109
+ "row.showCreditsCnHint": "Show the domestic (WB CN) credit balance in the composer toolbar, but only while this session uses a WB CN model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
4110
+ "row.showCreditsGlobalHint": "Show the international (WB AI) credit balance in the composer toolbar, but only while this session uses a WB AI model. Click it for per-account balances and a manual refresh (refreshes by itself every 5 minutes).",
4111
+ "composer.pointsCn": "WB CN",
4112
+ "composer.pointsGlobal": "WB AI",
4113
+ "composer.panelTitleCn": "WB CN credits",
4114
+ "composer.panelTitleGlobal": "WB AI credits",
4115
+ "composer.account": "Account",
4116
+ "composer.credits": "Credits",
4117
+ "composer.switchTo": "Switch to {account}",
4118
+ "composer.noAccounts": "No account in this region",
4119
+ "composer.signedOut": "Not signed in",
4120
+ "composer.refresh": "Refresh credits",
4121
+ "composer.refreshing": "Refreshing credits…",
4122
+ "composer.refreshFailed": "Could not refresh credits",
4123
+ "composer.lastRefresh": "Last refreshed",
4124
+ "composer.panelTitle": "WorkBuddy credits",
4125
+ "sidebar.creditsCn": "WB CN credits",
4126
+ "sidebar.creditsGlobal": "WB AI credits",
4127
+ "sidebar.refresh": "Refresh",
4128
+ "sidebar.refreshing": "Refreshing…",
4129
+ "sidebar.lastRefresh": "Last refreshed {time}",
4130
+ "sidebar.refreshFailed": "Refresh failed",
4131
+ "sidebar.signedOut": "Not signed in"
3280
4132
  };
3281
4133
  const zh = {
3282
4134
  "row.title": "接入使用 WorkBuddy 积分与模型(dsh-connect-workbuddy)",
@@ -3466,7 +4318,32 @@ window.__ModuleLoader__.load({
3466
4318
  "row.poolCurrentBadge": "当前使用",
3467
4319
  "row.poolCurrentHeader": "当前使用:{account}",
3468
4320
  "row.poolNotMember": "未入池",
3469
- "row.poolUnsavedMembers": "勾选已改动 —— 保存后生效"
4321
+ "row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
4322
+ "row.showCreditsCn": "在对话框显示 WB CN 积分",
4323
+ "row.showCreditsGlobal": "在对话框显示 WB AI 积分",
4324
+ "row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
4325
+ "row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
4326
+ "composer.pointsCn": "WB CN",
4327
+ "composer.pointsGlobal": "WB AI",
4328
+ "composer.panelTitleCn": "WB CN 积分",
4329
+ "composer.panelTitleGlobal": "WB AI 积分",
4330
+ "composer.account": "账号",
4331
+ "composer.credits": "积分",
4332
+ "composer.switchTo": "切换到 {account}",
4333
+ "composer.noAccounts": "该区域暂无账号",
4334
+ "composer.signedOut": "未登录",
4335
+ "composer.refresh": "刷新积分",
4336
+ "composer.refreshing": "正在刷新积分…",
4337
+ "composer.refreshFailed": "积分刷新失败",
4338
+ "composer.lastRefresh": "最近刷新",
4339
+ "composer.panelTitle": "WorkBuddy 积分",
4340
+ "sidebar.creditsCn": "WB CN 积分",
4341
+ "sidebar.creditsGlobal": "WB AI 积分",
4342
+ "sidebar.refresh": "刷新",
4343
+ "sidebar.refreshing": "刷新中…",
4344
+ "sidebar.lastRefresh": "最近刷新 {time}",
4345
+ "sidebar.refreshFailed": "刷新失败",
4346
+ "sidebar.signedOut": "未登录"
3470
4347
  };
3471
4348
  //#endregion
3472
4349
  //#region src/client/index.tsx
@@ -3543,6 +4420,41 @@ window.__ModuleLoader__.load({
3543
4420
  };
3544
4421
  registerCard("plugins.bundle.config", "dsh-connect-workbuddy");
3545
4422
  registerCard("plugins.row.config", "dsh-connect-workbuddy#dsh-connect-workbuddy");
4423
+ /**
4424
+ * Credit readout in the composer tool row (`conversation.input.left`, a
4425
+ * session-scope list slot).
4426
+ *
4427
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
4428
+ * — 该项目的 `registerComposerPoints`:无条件注册插槽、由 gate 读
4429
+ * `modelSelection` 投影自行决定是否渲染。本项目原先放在侧边栏底部
4430
+ * (`sidebar.footer.action`),那里是**单个 root 级共享行**,两个连接器
4431
+ * 插件各占一行会互相挤压;输入框这行是 per-session 且按 provider 归属,
4432
+ * 天然只有一个插件的积分相关。
4433
+ *
4434
+ * Provider-scoped on purpose: it renders only while the session's selected
4435
+ * model belongs to this plugin, which is what keeps two connector plugins
4436
+ * from competing for one shared row. Registration is unconditional; the
4437
+ * gate reads the Host's `modelSelection` projection and returns null for
4438
+ * every other provider, so nothing is drawn and no fetch loop starts while
4439
+ * another provider is selected.
4440
+ */
4441
+ const registerComposerCredits = () => {
4442
+ try {
4443
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
4444
+ name: "conversation.input.left",
4445
+ id: "dsh-connect-workbuddy-composer-credits",
4446
+ order: 4,
4447
+ locale: namespace,
4448
+ inject: () => settingsScope === void 0 ? { t } : {
4449
+ t,
4450
+ settingsScope
4451
+ }
4452
+ }, ComposerCreditsGate));
4453
+ } catch (error) {
4454
+ console.error("[dsh-connect-workbuddy] composer credits slot failed to register (host provider unaffected):", error);
4455
+ }
4456
+ };
4457
+ registerComposerCredits();
3546
4458
  } catch (error) {
3547
4459
  console.error("[dsh-connect-workbuddy] client card failed to load (host provider unaffected):", error);
3548
4460
  }