dsh-connect-workbuddy 3.6.0 → 4.2.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
@@ -57,12 +70,47 @@ window.__ModuleLoader__.load({
57
70
  * splitting them would duplicate the guards and invite them to drift apart.
58
71
  */
59
72
  const WORKBUDDY_POOL_PATH = "/plugins/dsh-connect-workbuddy/pool";
73
+ /**
74
+ * Plugin-owned OAuth sign-in endpoint: one session step per call.
75
+ *
76
+ * `?action=start` asks the upstream for a login state and returns the URL to
77
+ * open; `?action=poll` (+ `loginId` in the body) reports whether the user
78
+ * finished, and on success persists the account into the vault. POST +
79
+ * loopback-only like the card's other mutations: the poll carries the login
80
+ * binding, and the Host-side session state must not be settable by another
81
+ * origin.
82
+ */
83
+ const WORKBUDDY_OAUTH_PATH = "/plugins/dsh-connect-workbuddy/oauth";
60
84
  /** Query parameter selecting which pool batch action a request means. */
61
85
  const WORKBUDDY_POOL_ACTION_PARAM = "action";
86
+ /** How long the card waits between polls while the user is scanning. */
87
+ const OAUTH_POLL_INTERVAL_MS = 2e3;
88
+ /**
89
+ * Plugin-owned account transfer endpoint: the batch export / import of
90
+ * credentials between machines and between tools of the SAME format family.
91
+ *
92
+ * `?action=export` POSTs account ids, answers a JSON ARRAY of credential
93
+ * records; `?action=preview` POSTs file text, answers a token-free listing;
94
+ * `?action=import` POSTs file text plus selected indexes, merges into the
95
+ * vault. POST + loopback-only like every other mutation: the file content is
96
+ * arbitrary user data and export carries token material.
97
+ */
98
+ const WORKBUDDY_TRANSFER_PATH = "/plugins/dsh-connect-workbuddy/transfer";
62
99
  /** Address one region's pool endpoint with an action. */
63
100
  function withWorkBuddyRegionAndAction(path, region, action) {
64
101
  return `${withWorkBuddyRegion(path, region)}&${WORKBUDDY_POOL_ACTION_PARAM}=${action}`;
65
102
  }
103
+ /**
104
+ * Address one region's endpoint for ANY route that dispatches on `action`.
105
+ *
106
+ * The OAuth and transfer routes reject a request with no action (400), so
107
+ * their callers must build the URL here rather than with
108
+ * {@link withWorkBuddyRegion} — a request missing the parameter fails the same
109
+ * way whether the omission was a mistake or not.
110
+ */
111
+ function withWorkBuddyRouteAction(path, region, action) {
112
+ return `${withWorkBuddyRegion(path, region)}&${WORKBUDDY_POOL_ACTION_PARAM}=${action}`;
113
+ }
66
114
  /** Query parameter naming the region a card request addresses. */
67
115
  const WORKBUDDY_REGION_PARAM = "region";
68
116
  /** Every region, in card tab order. */
@@ -173,6 +221,32 @@ window.__ModuleLoader__.load({
173
221
  });
174
222
  }
175
223
  /**
224
+ * Whether one region's sidebar credit line is shown. Opt-out: only an explicit
225
+ * `false` hides it, so a config written before this switch existed keeps the
226
+ * line visible exactly as a fresh install does.
227
+ *
228
+ * Per-REGION by construction (it reads one region's slot), which is the point:
229
+ * WB CN and WB AI are separate accounts on separate gateways, so "show credits"
230
+ * is not one decision. `value` may be the whole settings section or the
231
+ * `regions` map itself.
232
+ */
233
+ function regionCreditsShownOf(value, region) {
234
+ return regionSlotOf(value, region)["showCreditsInMainUi"] !== false;
235
+ }
236
+ /**
237
+ * Build the next `regions` settings value for the sidebar-credit on/off toggle.
238
+ *
239
+ * Same merge rule as {@link nextRegionEnabled}: ONLY the target region's flag
240
+ * changes, so every other field of that slot (directory, selection, pool
241
+ * preferences) and every other region's slot survive the write.
242
+ */
243
+ function nextRegionCreditsShown(value, region, shown) {
244
+ return nextRegionSlots(regionsMapOf(value), region, {
245
+ ...regionSlotOf(value, region),
246
+ showCreditsInMainUi: shown
247
+ });
248
+ }
249
+ /**
176
250
  * Project one card row into its persisted `lastCatalog` shape: the native
177
251
  * context window becomes the stored `contextWindow`, and the card-only
178
252
  * presentation fields (`nativeContextWindow`, `multimodal`) are removed BY
@@ -489,6 +563,33 @@ window.__ModuleLoader__.load({
489
563
  });
490
564
  }
491
565
  /**
566
+ * Show or hide one region's sidebar credit line, with the same landed-check as
567
+ * every other settings write.
568
+ *
569
+ * Routed through {@link writeField} for exactly the reason
570
+ * {@link writeRegionEnabled} is: on the affected 0.1.7 deployments a direct
571
+ * `scope.set()` settles without storing anything, so the switch would appear to
572
+ * work and then silently revert — which for this control means the line
573
+ * reappears on the next render with no explanation.
574
+ *
575
+ * The caller passes the region's COMPLETE next slot (the same whole-slot merge
576
+ * rule as the other region writes), so hiding a line cannot drop the model
577
+ * directory, selection, image opt-ins, budgets or pool preferences that live
578
+ * alongside this flag in the same slot.
579
+ *
580
+ * @param scope - the bound settings scope for this plugin's namespace.
581
+ * @param region - the region whose line is being shown or hidden.
582
+ * @param shown - the desired state.
583
+ * @param slot - the region's COMPLETE next slot (caller owns the merge).
584
+ * @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
585
+ */
586
+ async function writeRegionCreditsShown(scope, region, shown, slot) {
587
+ await writeField(scope, "regions", region, slot, (readBack) => {
588
+ const stored = readBack;
589
+ return stored !== null && stored !== void 0 && stored["showCreditsInMainUi"] === shown;
590
+ });
591
+ }
592
+ /**
492
593
  * Write one region's account-pool PREFERENCES, verifying they landed.
493
594
  *
494
595
  * Routed through {@link writeField} like every other settings write, so it gets
@@ -533,6 +634,21 @@ window.__ModuleLoader__.load({
533
634
  });
534
635
  }
535
636
  //#endregion
637
+ //#region src/client/exclusion-text.ts
638
+ /** The exclusion label for one account, or undefined when it is usable. */
639
+ function exclusionText(t, excludedBy) {
640
+ switch (excludedBy) {
641
+ case "rate-limited": return t("row.poolExcludedRateLimited");
642
+ case "out-of-credit": return t("row.poolExcludedOutOfCredit");
643
+ case "credential-rejected": return t("row.poolExcludedRejected");
644
+ case "not-found": return t("row.probeNotFound");
645
+ case "unavailable": return t("row.probeUnavailable");
646
+ case "failed": return t("row.probeFailed");
647
+ case "unusable": return t("row.poolExcludedUnusable");
648
+ default: return;
649
+ }
650
+ }
651
+ //#endregion
536
652
  //#region src/client/ndjson.ts
537
653
  /**
538
654
  * Read a newline-delimited JSON body, one object at a time.
@@ -988,7 +1104,7 @@ window.__ModuleLoader__.load({
988
1104
  hourCycle: "h23"
989
1105
  }).format(new Date(value));
990
1106
  }
991
- function formatClock(value) {
1107
+ function formatClock$1(value) {
992
1108
  return new Intl.DateTimeFormat(void 0, {
993
1109
  hour: "2-digit",
994
1110
  minute: "2-digit",
@@ -1002,19 +1118,6 @@ window.__ModuleLoader__.load({
1002
1118
  if (account.probe === void 0) return "var(--dsw-alias-label-dimmed, #9aa0a6)";
1003
1119
  return outcomeOk(account.probe.outcome) ? "var(--dsw-alias-state-success-primary, #22a06b)" : "var(--dsw-alias-state-warn-primary, #f59e0b)";
1004
1120
  }
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
1121
  /**
1019
1122
  * The pool section.
1020
1123
  *
@@ -1023,7 +1126,7 @@ window.__ModuleLoader__.load({
1023
1126
  * section implying the feature exists.
1024
1127
  */
1025
1128
  function AccountPool(props) {
1026
- const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount } = props;
1129
+ const { t, region, pool, settingsScope, siblingBusy, onBusyChange, onSaved, onRefresh, onRescan, rescanning, onSelectAccount, selectingAccount, accountTools } = props;
1027
1130
  const [draft, setDraft] = (0, react.useState)(null);
1028
1131
  const [saving, setSaving] = (0, react.useState)(false);
1029
1132
  const [saveError, setSaveError] = (0, react.useState)(void 0);
@@ -1299,6 +1402,98 @@ window.__ModuleLoader__.load({
1299
1402
  region,
1300
1403
  t
1301
1404
  ]);
1405
+ /** The account whose per-row action is in flight; gates every row's buttons. */
1406
+ const [rowBusyAccountId, setRowBusyAccountId] = (0, react.useState)(void 0);
1407
+ /**
1408
+ * Test ONE account from its row.
1409
+ *
1410
+ * Same Host rule as the batch (testOne reuses the batch's probe), the same
1411
+ * log vocabulary, and the same refresh-after — a row result must read exactly
1412
+ * like the batch result it replaces.
1413
+ */
1414
+ const runRowTest = (0, react.useCallback)(async (accountId) => {
1415
+ setRowBusyAccountId(accountId);
1416
+ setActionError(void 0);
1417
+ const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "test") + "&accountId=" + encodeURIComponent(accountId);
1418
+ try {
1419
+ const response = await fetch(url, {
1420
+ method: "POST",
1421
+ headers: { accept: "application/json" },
1422
+ credentials: "same-origin"
1423
+ });
1424
+ const body = await response.json().catch(() => void 0);
1425
+ if (!response.ok) throw new Error(poolFailureText(t, {
1426
+ status: response.status,
1427
+ reason: body?.reason,
1428
+ error: body?.error
1429
+ }));
1430
+ if (!mounted.current) return;
1431
+ const row = body?.rows?.[0];
1432
+ if (row === void 0) {
1433
+ setActionError(t("row.poolLogTestRowMalformed", { accountName: t("row.accountUnnamed") }));
1434
+ return;
1435
+ }
1436
+ const outcome = row.result?.outcome;
1437
+ if (outcome === void 0) appendLog(t("row.poolLogTestRowMalformed", { accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName }), "warn");
1438
+ else appendLog(t("row.poolLogTestRow", {
1439
+ accountName: row.accountName === "" ? t("row.accountUnnamed") : row.accountName,
1440
+ outcome: outcomeText(t, outcome)
1441
+ }), outcomeOk(outcome) ? "ok" : "warn");
1442
+ onRefresh?.();
1443
+ } catch (error) {
1444
+ const message = error instanceof Error ? error.message : t("row.requestFailed");
1445
+ if (mounted.current) {
1446
+ setActionError(message);
1447
+ appendLog(t("row.poolLogBatchFailed", { message }), "error");
1448
+ }
1449
+ } finally {
1450
+ if (mounted.current) setRowBusyAccountId(void 0);
1451
+ }
1452
+ }, [
1453
+ appendLog,
1454
+ onRefresh,
1455
+ region,
1456
+ t
1457
+ ]);
1458
+ /**
1459
+ * Remove ONE account's plugin-stored credential from its row. The Host only
1460
+ * ever deletes the vault entry; a desktop sign-in reappears on the next
1461
+ * rescan, and the message below says which of the two happened.
1462
+ */
1463
+ const runRowRemove = (0, react.useCallback)(async (accountId, accountName) => {
1464
+ setRowBusyAccountId(accountId);
1465
+ setActionError(void 0);
1466
+ const url = withWorkBuddyRegionAndAction(WORKBUDDY_POOL_PATH, region, "remove") + "&accountId=" + encodeURIComponent(accountId);
1467
+ try {
1468
+ const response = await fetch(url, {
1469
+ method: "POST",
1470
+ headers: { accept: "application/json" },
1471
+ credentials: "same-origin"
1472
+ });
1473
+ const body = await response.json().catch(() => void 0);
1474
+ if (!response.ok) throw new Error(poolFailureText(t, {
1475
+ status: response.status,
1476
+ reason: body?.reason,
1477
+ error: body?.error
1478
+ }));
1479
+ if (!mounted.current) return;
1480
+ appendLog(body?.removed === true ? t("row.poolRowRemoved", { accountName: accountName === "" ? t("row.accountUnnamed") : accountName }) : t("row.poolRowRemoveDesktopOnly", { accountName: accountName === "" ? t("row.accountUnnamed") : accountName }), body?.removed === true ? "ok" : "info");
1481
+ onRefresh?.();
1482
+ } catch (error) {
1483
+ const message = error instanceof Error ? error.message : t("row.requestFailed");
1484
+ if (mounted.current) {
1485
+ setActionError(message);
1486
+ appendLog(t("row.poolLogBatchFailed", { message }), "error");
1487
+ }
1488
+ } finally {
1489
+ if (mounted.current) setRowBusyAccountId(void 0);
1490
+ }
1491
+ }, [
1492
+ appendLog,
1493
+ onRefresh,
1494
+ region,
1495
+ t
1496
+ ]);
1302
1497
  if (pool === void 0) return null;
1303
1498
  /**
1304
1499
  * Where the target model came from, and which model it is.
@@ -1371,13 +1566,22 @@ window.__ModuleLoader__.load({
1371
1566
  className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
1372
1567
  disabled: effectiveMembers.length === 0 || !canEditPool,
1373
1568
  onClick: () => selectAll(false)
1374
- }, t("row.poolSelectNone")))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-table" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), pool.checkinSupported ? (0, react.createElement)("span", null, t("row.poolColumnCheckin")) : null), ...pool.accounts.map((account) => renderAccountRow({
1569
+ }, t("row.poolSelectNone")))), (0, react.createElement)("div", { className: `dsm-workbuddy-pool-table${pool.checkinSupported ? " dsm-workbuddy-pool-table-checkin" : ""}` }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-row dsm-workbuddy-pool-row-head" }, (0, react.createElement)("span", null, t("row.poolColumnAccount")), (0, react.createElement)("span", null, t("row.poolColumnCredits")), (0, react.createElement)("span", null, t("row.poolColumnProbe")), pool.checkinSupported ? (0, react.createElement)("span", null, t("row.poolColumnCheckin")) : null, (0, react.createElement)("span", null)), ...pool.accounts.map((account) => renderAccountRow({
1375
1570
  t,
1376
1571
  account,
1377
1572
  checked: active.memberAccountIds.includes(account.accountId),
1378
1573
  canEdit: canEditPool,
1379
1574
  checkinSupported: pool.checkinSupported,
1380
- onToggle: (next) => toggleMember(account.accountId, next)
1575
+ onToggle: (next) => toggleMember(account.accountId, next),
1576
+ busy: rowBusyAccountId !== void 0,
1577
+ testDisabled: pool.targetModelSource === "none" || pool.targetModelSource === "stale",
1578
+ targetHint: pool.targetModelSource === "none" ? t("row.poolTargetNone") : pool.targetModelSource === "stale" ? t("row.poolTargetStale", { model: pool.staleTargetModelId ?? "" }) : void 0,
1579
+ onTest: () => {
1580
+ runRowTest(account.accountId);
1581
+ },
1582
+ onRemove: () => {
1583
+ runRowRemove(account.accountId, account.accountName);
1584
+ }
1381
1585
  }))), effectiveMembers.length === 0 ? (0, react.createElement)("p", {
1382
1586
  className: "dsm-workbuddy-pool-warn",
1383
1587
  role: "status"
@@ -1415,7 +1619,8 @@ window.__ModuleLoader__.load({
1415
1619
  },
1416
1620
  onDiscard: discard,
1417
1621
  appendLog,
1418
- targetProvenance
1622
+ targetProvenance,
1623
+ ...accountTools === void 0 ? {} : { accountTools }
1419
1624
  }), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolRegionNote")), renderLog({
1420
1625
  t,
1421
1626
  log,
@@ -1428,9 +1633,9 @@ window.__ModuleLoader__.load({
1428
1633
  }
1429
1634
  /** One account's row. */
1430
1635
  function renderAccountRow(input) {
1431
- const { t, account, checked, canEdit, checkinSupported, onToggle } = input;
1636
+ const { t, account, checked, canEdit, checkinSupported, onToggle, busy, testDisabled, targetHint, onTest, onRemove } = input;
1432
1637
  const name = account.accountName === "" ? t("row.accountUnnamed") : account.accountName;
1433
- const excluded = exclusionText(t, account);
1638
+ const excluded = exclusionText(t, account.excludedBy);
1434
1639
  const probe = account.probe;
1435
1640
  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
1641
  const remaining = probe?.retryAtMs === void 0 ? "" : remainingText(t, probe.retryAtMs, Date.now());
@@ -1469,11 +1674,24 @@ window.__ModuleLoader__.load({
1469
1674
  }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-main" }, probeLine), probeLabel === void 0 && age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-probe-prov" }, probeLabel === void 0 ? null : (0, react.createElement)("span", { className: probeLabel.className }, probeLabel.text), probeLabel === void 0 || age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-sep" }, "·"), age === void 0 ? null : (0, react.createElement)("span", { className: "dsm-workbuddy-pool-prov-age" }, age))), checkinSupported ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checkin" }, account.checkedInToday === void 0 ? (0, react.createElement)("span", {
1470
1675
  className: "dsm-workbuddy-pool-unknown",
1471
1676
  title: t("row.poolNeverTested")
1472
- }, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null);
1677
+ }, t("row.poolCheckinUnknown")) : account.checkedInToday ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-checked" }, t("row.poolCheckedIn")) : t("row.poolNotCheckedIn")) : null, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-row-actions" }, (0, react.createElement)("button", {
1678
+ type: "button",
1679
+ className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
1680
+ disabled: busy || testDisabled,
1681
+ title: testDisabled ? targetHint : void 0,
1682
+ onClick: onTest
1683
+ }, t("row.poolRowTest")), (0, react.createElement)("button", {
1684
+ type: "button",
1685
+ className: "dsm-btn dsm-btn-outline dsm-workbuddy-pool-small-btn",
1686
+ disabled: busy,
1687
+ title: t("row.poolRowRemoveHint"),
1688
+ "aria-label": t("row.poolRowRemoveAria", { accountName: name }),
1689
+ onClick: onRemove
1690
+ }, t("row.poolRowRemove"))));
1473
1691
  }
1474
1692
  /** The preference editor plus its save/discard actions. */
1475
1693
  function renderSettings(input) {
1476
- const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance } = input;
1694
+ const { t, pool, active, dirty, saving, siblingBusy, saveError, settingsScope, onEdit, onSave, onDiscard, targetProvenance, accountTools } = input;
1477
1695
  const canEdit = settingsScope !== void 0;
1478
1696
  const toggle = (label, hint, checked, onChange, disabled = false) => (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, label), (0, react.createElement)("span", null, hint)), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("label", { className: "sw" }, (0, react.createElement)("input", {
1479
1697
  type: "checkbox",
@@ -1481,7 +1699,7 @@ window.__ModuleLoader__.load({
1481
1699
  disabled: disabled || !canEdit,
1482
1700
  onChange: (event) => onChange(event.currentTarget.checked)
1483
1701
  }), (0, react.createElement)("span", { className: "sw-track" }))));
1484
- return (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings-wrap" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolSettingsTitle")), dirty ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-dirty" }, t("row.poolDirty")) : null), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolSettingsHint")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings" }, toggle(t("row.poolEnabled"), t("row.poolEnabledHint"), active.enabled, (next) => onEdit((current) => ({
1702
+ return (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings-wrap" }, (0, react.createElement)("div", { className: "dsm-workbuddy-pool-head" }, (0, react.createElement)("h3", { className: "dsm-workbuddy-pool-title" }, t("row.poolSettingsTitle")), dirty ? (0, react.createElement)("span", { className: "dsm-workbuddy-pool-dirty" }, t("row.poolDirty")) : null), accountTools === void 0 ? null : (0, react.createElement)("div", { className: "dsm-workbuddy-pool-tools" }, accountTools), (0, react.createElement)("p", { className: "dsm-workbuddy-pool-note" }, t("row.poolSettingsHint")), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-settings" }, toggle(t("row.poolEnabled"), t("row.poolEnabledHint"), active.enabled, (next) => onEdit((current) => ({
1485
1703
  ...current,
1486
1704
  enabled: next
1487
1705
  }))), (0, react.createElement)("div", { className: "dsm-workbuddy-pool-set" }, (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-copy" }, (0, react.createElement)("b", null, t("row.poolTargetLabel")), (0, react.createElement)("span", null, targetProvenance === null ? null : `${targetProvenance} `, t("row.poolTargetHint"))), (0, react.createElement)("span", { className: "dsm-workbuddy-pool-set-ctl" }, (0, react.createElement)("select", {
@@ -1550,7 +1768,535 @@ window.__ModuleLoader__.load({
1550
1768
  }, 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
1769
  className: "dsm-workbuddy-pool-log-row",
1552
1770
  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)))));
1771
+ }, (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)))));
1772
+ }
1773
+ //#endregion
1774
+ //#region src/client/OAuthSignIn.tsx
1775
+ /**
1776
+ * The card's OAuth QR sign-in section.
1777
+ *
1778
+ * 参考:扫码登录的通行形态 —— 发起登录 → 打开授权页 →
1779
+ * 轮询到完成为止 → 展示结果。本实现把轮询循环压成一个组件内 effect:
1780
+ * 打开授权页的是用户点击的链接(浏览器标签页,非弹窗),组件只负责
1781
+ * 按 `OAUTH_POLL_INTERVAL_MS` 节奏问 Host「完成了吗」,完成即停。
1782
+ *
1783
+ * 为什么不用 window.open:卡片运行在 Harness 的设置页 iframe/沙箱里,弹窗
1784
+ * 会被拦截;把授权 URL 渲染成普通链接(target=_blank rel=noopener)是
1785
+ * 沙箱内最可靠的方式,用户也可以复制链接到手机上继续(URL 即登录态)。
1786
+ *
1787
+ * 所有状态就地管理,零跨组件往返:取消 = 停止轮询 + 清理 loginId;
1788
+ * Host 重启导致 loginId 失效会得到 done+error,与超时同一条文案路径。
1789
+ *
1790
+ * @module dsh-connect-workbuddy/client/OAuthSignIn
1791
+ */
1792
+ /** Seconds since the login started, for the waiting line. */
1793
+ function elapsedSeconds(sinceMs, nowMs) {
1794
+ return Math.max(0, Math.floor((nowMs - sinceMs) / 1e3));
1795
+ }
1796
+ /**
1797
+ * The section. Rendered on BOTH sign-in states: a signed-out machine gains
1798
+ * its first account here; a signed-in one can add another without leaving
1799
+ * the card.
1800
+ */
1801
+ function OAuthSignIn({ t, region }) {
1802
+ const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
1803
+ const [pollTick, setPollTick] = (0, react.useState)(0);
1804
+ const [nowMs, setNowMs] = (0, react.useState)(() => Date.now());
1805
+ const mounted = (0, react.useRef)(true);
1806
+ (0, react.useEffect)(() => {
1807
+ mounted.current = true;
1808
+ return () => {
1809
+ mounted.current = false;
1810
+ };
1811
+ }, []);
1812
+ const waiting = phase.kind === "waiting";
1813
+ (0, react.useEffect)(() => {
1814
+ if (!waiting || phase.kind !== "waiting") return;
1815
+ const expiresAtMs = phase.expiresAtMs;
1816
+ const timer = setInterval(() => {
1817
+ if (!mounted.current) return;
1818
+ const now = Date.now();
1819
+ setNowMs(now);
1820
+ if (now > expiresAtMs) setPhase({
1821
+ kind: "finished",
1822
+ message: "error",
1823
+ error: t("row.oauthTimedOut")
1824
+ });
1825
+ }, 1e3);
1826
+ return () => {
1827
+ clearInterval(timer);
1828
+ };
1829
+ }, [waiting, phase]);
1830
+ (0, react.useEffect)(() => {
1831
+ if (phase.kind !== "waiting") return;
1832
+ const { loginId } = phase;
1833
+ let cancelled = false;
1834
+ const timer = setTimeout(() => {
1835
+ (async () => {
1836
+ try {
1837
+ const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_OAUTH_PATH, region, "poll"), {
1838
+ method: "POST",
1839
+ headers: {
1840
+ "content-type": "application/json",
1841
+ accept: "application/json"
1842
+ },
1843
+ credentials: "same-origin",
1844
+ body: JSON.stringify({ loginId })
1845
+ });
1846
+ const body = await response.json();
1847
+ if (cancelled || !mounted.current) return;
1848
+ if (!response.ok || !body || typeof body !== "object") {
1849
+ setPhase({
1850
+ kind: "finished",
1851
+ message: "error",
1852
+ error: `HTTP ${response.status}`
1853
+ });
1854
+ return;
1855
+ }
1856
+ if (!body.done) {
1857
+ setPollTick((tick) => tick + 1);
1858
+ return;
1859
+ }
1860
+ if (typeof body.error === "string" && body.error !== "") setPhase({
1861
+ kind: "finished",
1862
+ message: "error",
1863
+ error: body.error
1864
+ });
1865
+ else setPhase({
1866
+ kind: "finished",
1867
+ message: "added",
1868
+ accountName: body.accountName ?? ""
1869
+ });
1870
+ } catch {
1871
+ setPollTick((tick) => tick + 1);
1872
+ }
1873
+ })();
1874
+ }, OAUTH_POLL_INTERVAL_MS);
1875
+ return () => {
1876
+ cancelled = true;
1877
+ clearTimeout(timer);
1878
+ };
1879
+ }, [
1880
+ phase,
1881
+ pollTick,
1882
+ region
1883
+ ]);
1884
+ const start = async () => {
1885
+ setPhase({ kind: "opening" });
1886
+ try {
1887
+ const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_OAUTH_PATH, region, "start"), {
1888
+ method: "POST",
1889
+ headers: { accept: "application/json" },
1890
+ credentials: "same-origin"
1891
+ });
1892
+ const body = await response.json();
1893
+ if (!mounted.current) return;
1894
+ if (!response.ok || typeof body.loginId !== "string" || typeof body.verificationUri !== "string") {
1895
+ setPhase({
1896
+ kind: "finished",
1897
+ message: "error",
1898
+ error: body.error ?? `HTTP ${response.status}`
1899
+ });
1900
+ return;
1901
+ }
1902
+ setPhase({
1903
+ kind: "waiting",
1904
+ loginId: body.loginId,
1905
+ verificationUri: body.verificationUri,
1906
+ startedAtMs: Date.now(),
1907
+ expiresAtMs: Date.now() + (typeof body.expiresIn === "number" && body.expiresIn > 0 ? body.expiresIn : 600) * 1e3
1908
+ });
1909
+ setNowMs(Date.now());
1910
+ } catch (error) {
1911
+ if (mounted.current) setPhase({
1912
+ kind: "finished",
1913
+ message: "error",
1914
+ error: error instanceof Error ? error.message : String(error)
1915
+ });
1916
+ }
1917
+ };
1918
+ if (phase.kind === "finished") {
1919
+ const text = phase.message === "added" ? t("row.oauthDone", { accountName: phase.accountName === "" || phase.accountName === void 0 ? t("row.accountUnnamed") : phase.accountName }) : phase.message === "cancelled" ? t("row.oauthCancelled") : t("row.oauthError", { message: phase.error ?? "" });
1920
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1921
+ className: "dsm-workbuddy-bar",
1922
+ "aria-label": t("row.oauthTitle"),
1923
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1924
+ className: `dsm-workbuddy-bar-status ${phase.message === "added" ? "dsm-workbuddy-bar-ok" : "dsm-workbuddy-bar-error"}`,
1925
+ children: text
1926
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1927
+ type: "button",
1928
+ className: "dsm-btn dsm-btn-outline",
1929
+ onClick: () => {
1930
+ setPhase({ kind: "idle" });
1931
+ },
1932
+ children: t("row.oauthStart")
1933
+ })]
1934
+ });
1935
+ }
1936
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1937
+ className: "dsm-workbuddy-bar",
1938
+ "aria-label": t("row.oauthTitle"),
1939
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1940
+ className: "dsm-workbuddy-bar-label",
1941
+ children: t("row.oauthTitle")
1942
+ }), phase.kind === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1944
+ className: "dsm-workbuddy-bar-status",
1945
+ children: [
1946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1947
+ href: phase.verificationUri,
1948
+ target: "_blank",
1949
+ rel: "noopener noreferrer",
1950
+ children: t("row.oauthOpen")
1951
+ }),
1952
+ " — ",
1953
+ t("row.oauthOpened")
1954
+ ]
1955
+ }),
1956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1957
+ className: "dsm-workbuddy-bar-hint",
1958
+ children: [
1959
+ t("row.oauthWaiting", { seconds: elapsedSeconds(phase.startedAtMs, nowMs) }),
1960
+ " ",
1961
+ t("row.oauthExpires", { minutes: Math.max(1, Math.round((phase.expiresAtMs - nowMs) / 6e4)) })
1962
+ ]
1963
+ }),
1964
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1965
+ type: "button",
1966
+ className: "dsm-btn dsm-btn-outline",
1967
+ onClick: () => {
1968
+ setPhase({
1969
+ kind: "finished",
1970
+ message: "cancelled"
1971
+ });
1972
+ },
1973
+ children: t("row.oauthCancel")
1974
+ })
1975
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1976
+ type: "button",
1977
+ className: "dsm-btn dsm-btn-outline",
1978
+ disabled: phase.kind === "opening",
1979
+ onClick: () => {
1980
+ start();
1981
+ },
1982
+ children: phase.kind === "opening" ? t("row.oauthStarting") : t("row.oauthStart")
1983
+ })]
1984
+ });
1985
+ }
1986
+ //#endregion
1987
+ //#region src/client/AccountTransfer.tsx
1988
+ /**
1989
+ * The card's account-transfer section: batch export / import of the region's
1990
+ * stored credentials, in the file format shared with sibling credential
1991
+ * managers for the same upstream.
1992
+ *
1993
+ * 参考:扫码登录小节(OAuthSignIn)的组件形态 —— 单个 section、就地管理
1994
+ * 全部状态(busy / 结果行 / 错误)、零跨组件往返。导出弹框的交互(多选
1995
+ * 账号 → 下载 JSON)与导入弹框(选文件 → 脱敏预览 → 勾选 → 导入)沿用
1996
+ * 通行实现的步骤划分;预览只含展示字段与 hasToken 布尔量,token 明文
1997
+ * 永不进入浏览器。
1998
+ *
1999
+ * 导出用 <a download> Blob:卡片运行在设置页 iframe 里,无宿主保存对话框
2000
+ * 可用;a[download] 同源下载是沙箱内最可靠的方式。
2001
+ *
2002
+ * @module dsh-connect-workbuddy/client/AccountTransfer
2003
+ */
2004
+ /** Build the default export file name: workbuddy-accounts-YYYY-MM-DD.json. */
2005
+ function exportFileName() {
2006
+ const pad = (value) => String(value).padStart(2, "0");
2007
+ const d = /* @__PURE__ */ new Date();
2008
+ return `workbuddy-accounts-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.json`;
2009
+ }
2010
+ /** Download a JSON array as a local file (no host save dialog inside the iframe). */
2011
+ function downloadJson(fileName, data) {
2012
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
2013
+ const url = URL.createObjectURL(blob);
2014
+ const anchor = document.createElement("a");
2015
+ anchor.href = url;
2016
+ anchor.download = fileName;
2017
+ document.body.appendChild(anchor);
2018
+ anchor.click();
2019
+ anchor.remove();
2020
+ URL.revokeObjectURL(url);
2021
+ }
2022
+ /** The section. Rendered on both sign-in states — a signed-out machine can
2023
+ * still import a file exported elsewhere; export without accounts answers
2024
+ * the error line. */
2025
+ function AccountTransfer({ t, region, accounts }) {
2026
+ const [phase, setPhase] = (0, react.useState)({ kind: "idle" });
2027
+ const [dialog, setDialog] = (0, react.useState)(void 0);
2028
+ const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
2029
+ const [preview, setPreview] = (0, react.useState)(void 0);
2030
+ const [importSelected, setImportSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
2031
+ const fileInput = (0, react.useRef)(null);
2032
+ const mounted = (0, react.useRef)(true);
2033
+ (0, react.useEffect)(() => {
2034
+ mounted.current = true;
2035
+ return () => {
2036
+ mounted.current = false;
2037
+ };
2038
+ }, []);
2039
+ const closeDialog = () => {
2040
+ setDialog(void 0);
2041
+ setSelected(/* @__PURE__ */ new Set());
2042
+ setPreview(void 0);
2043
+ setImportSelected(/* @__PURE__ */ new Set());
2044
+ };
2045
+ const runExport = async () => {
2046
+ if (selected.size === 0) return;
2047
+ setPhase({ kind: "exporting" });
2048
+ try {
2049
+ const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "export"), {
2050
+ method: "POST",
2051
+ headers: {
2052
+ "content-type": "application/json",
2053
+ accept: "application/json"
2054
+ },
2055
+ credentials: "same-origin",
2056
+ body: JSON.stringify({ accountIds: [...selected] })
2057
+ });
2058
+ const body = await response.json();
2059
+ if (!mounted.current) return;
2060
+ if (!response.ok || !Array.isArray(body.accounts)) {
2061
+ setPhase({
2062
+ kind: "error",
2063
+ message: body.error ?? `HTTP ${response.status}`
2064
+ });
2065
+ return;
2066
+ }
2067
+ downloadJson(exportFileName(), body.accounts);
2068
+ closeDialog();
2069
+ setPhase({
2070
+ kind: "done",
2071
+ message: t("row.transferExported", {
2072
+ count: body.accounts.length,
2073
+ fileName: exportFileName()
2074
+ })
2075
+ });
2076
+ } catch (error) {
2077
+ if (mounted.current) setPhase({
2078
+ kind: "error",
2079
+ message: error instanceof Error ? error.message : String(error)
2080
+ });
2081
+ }
2082
+ };
2083
+ const readImportFile = async (file) => {
2084
+ setPhase({ kind: "importing" });
2085
+ try {
2086
+ const text = await file.text();
2087
+ const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "preview"), {
2088
+ method: "POST",
2089
+ headers: {
2090
+ "content-type": "application/json",
2091
+ accept: "application/json"
2092
+ },
2093
+ credentials: "same-origin",
2094
+ body: JSON.stringify({ text })
2095
+ });
2096
+ const body = await response.json();
2097
+ if (!mounted.current) return;
2098
+ if (!response.ok || !Array.isArray(body.accounts)) {
2099
+ setPhase({
2100
+ kind: "error",
2101
+ message: body.error ?? `HTTP ${response.status}`
2102
+ });
2103
+ return;
2104
+ }
2105
+ setPreview(body);
2106
+ setImportSelected(new Set(body.accounts.filter((entry) => entry.hasToken).map((entry) => entry.index)));
2107
+ setPhase({ kind: "idle" });
2108
+ } catch (error) {
2109
+ if (mounted.current) setPhase({
2110
+ kind: "error",
2111
+ message: error instanceof Error ? error.message : String(error)
2112
+ });
2113
+ }
2114
+ };
2115
+ const runImport = async () => {
2116
+ if (preview === void 0 || importSelected.size === 0 || fileInput.current?.files?.[0] === void 0) return;
2117
+ setPhase({ kind: "importing" });
2118
+ try {
2119
+ const text = await fileInput.current.files[0].text();
2120
+ const response = await fetch(withWorkBuddyRouteAction(WORKBUDDY_TRANSFER_PATH, region, "import"), {
2121
+ method: "POST",
2122
+ headers: {
2123
+ "content-type": "application/json",
2124
+ accept: "application/json"
2125
+ },
2126
+ credentials: "same-origin",
2127
+ body: JSON.stringify({
2128
+ text,
2129
+ indexes: [...importSelected]
2130
+ })
2131
+ });
2132
+ const body = await response.json();
2133
+ if (!mounted.current) return;
2134
+ if (!response.ok || typeof body.imported !== "number") {
2135
+ setPhase({
2136
+ kind: "error",
2137
+ message: body.error ?? `HTTP ${response.status}`
2138
+ });
2139
+ return;
2140
+ }
2141
+ closeDialog();
2142
+ setPhase({
2143
+ kind: "done",
2144
+ message: t("row.transferImported", {
2145
+ imported: body.imported,
2146
+ skipped: body.skipped ?? 0
2147
+ })
2148
+ });
2149
+ } catch (error) {
2150
+ if (mounted.current) setPhase({
2151
+ kind: "error",
2152
+ message: error instanceof Error ? error.message : String(error)
2153
+ });
2154
+ }
2155
+ };
2156
+ const busy = phase.kind === "exporting" || phase.kind === "importing";
2157
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2158
+ className: "dsm-workbuddy-bar",
2159
+ "aria-label": t("row.transferTitle"),
2160
+ children: [
2161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2162
+ className: "dsm-workbuddy-bar-label",
2163
+ children: t("row.transferTitle")
2164
+ }),
2165
+ phase.kind === "done" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2166
+ className: "dsm-workbuddy-bar-status dsm-workbuddy-bar-ok",
2167
+ children: phase.message
2168
+ }) : phase.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2169
+ className: "dsm-workbuddy-bar-status dsm-workbuddy-bar-error",
2170
+ children: t("row.transferError", { message: phase.message })
2171
+ }) : null,
2172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2173
+ className: "dsm-workbuddy-bar-actions",
2174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2175
+ type: "button",
2176
+ className: "dsm-btn dsm-btn-outline",
2177
+ disabled: busy || accounts.length === 0,
2178
+ onClick: () => {
2179
+ setSelected(/* @__PURE__ */ new Set());
2180
+ setDialog("export");
2181
+ },
2182
+ children: t("row.transferExport")
2183
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2184
+ type: "button",
2185
+ className: "dsm-btn dsm-btn-outline",
2186
+ disabled: busy,
2187
+ onClick: () => {
2188
+ setPreview(void 0);
2189
+ setImportSelected(/* @__PURE__ */ new Set());
2190
+ setDialog("import");
2191
+ },
2192
+ children: t("row.transferImport")
2193
+ })]
2194
+ }),
2195
+ dialog === "export" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2196
+ className: "dsm-workbuddy-transfer-dialog",
2197
+ role: "dialog",
2198
+ "aria-label": t("row.transferExport"),
2199
+ children: [
2200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferExportPick", { total: accounts.length }) }),
2201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2202
+ className: "dsm-workbuddy-hint",
2203
+ children: t("row.transferHint")
2204
+ }),
2205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2206
+ className: "dsm-workbuddy-transfer-list",
2207
+ children: accounts.map((account) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
2208
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2209
+ type: "checkbox",
2210
+ checked: selected.has(account.id),
2211
+ onChange: (event) => {
2212
+ const next = new Set(selected);
2213
+ if (event.target.checked) next.add(account.id);
2214
+ else next.delete(account.id);
2215
+ setSelected(next);
2216
+ }
2217
+ }),
2218
+ " ",
2219
+ account.accountName === "" ? t("row.accountUnnamed") : account.accountName
2220
+ ] }) }, account.id))
2221
+ }),
2222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2223
+ className: "dsm-workbuddy-transfer-dialog-actions",
2224
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2225
+ type: "button",
2226
+ className: "dsm-btn dsm-btn-outline",
2227
+ onClick: closeDialog,
2228
+ children: t("row.transferCancel")
2229
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2230
+ type: "button",
2231
+ className: "dsm-btn dsm-btn-outline",
2232
+ disabled: selected.size === 0 || phase.kind === "exporting",
2233
+ onClick: () => {
2234
+ runExport();
2235
+ },
2236
+ children: phase.kind === "exporting" ? t("row.transferExporting") : t("row.transferExportConfirm", { count: selected.size })
2237
+ })]
2238
+ })
2239
+ ]
2240
+ }) : null,
2241
+ dialog === "import" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2242
+ className: "dsm-workbuddy-transfer-dialog",
2243
+ role: "dialog",
2244
+ "aria-label": t("row.transferImport"),
2245
+ children: [
2246
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferImportPick") }),
2247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2248
+ ref: fileInput,
2249
+ type: "file",
2250
+ accept: "application/json,.json",
2251
+ disabled: busy,
2252
+ onChange: (event) => {
2253
+ const file = event.target.files?.[0];
2254
+ if (file !== void 0) readImportFile(file);
2255
+ }
2256
+ }),
2257
+ preview === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2258
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("row.transferPreviewTotal", { total: preview.total }) }),
2259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2260
+ className: "dsm-workbuddy-transfer-list",
2261
+ children: preview.accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
2262
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2263
+ type: "checkbox",
2264
+ disabled: !entry.hasToken,
2265
+ checked: importSelected.has(entry.index),
2266
+ onChange: (event) => {
2267
+ const next = new Set(importSelected);
2268
+ if (event.target.checked) next.add(entry.index);
2269
+ else next.delete(entry.index);
2270
+ setImportSelected(next);
2271
+ }
2272
+ }),
2273
+ " ",
2274
+ entry.nickname !== "" ? entry.nickname : entry.email !== "" ? entry.email : entry.uid !== "" ? entry.uid : t("row.transferEntryUnknown"),
2275
+ entry.hasToken ? "" : ` — ${t("row.transferNoToken")}`
2276
+ ] }) }, entry.index))
2277
+ }),
2278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2279
+ className: "dsm-workbuddy-transfer-dialog-actions",
2280
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2281
+ type: "button",
2282
+ className: "dsm-btn dsm-btn-outline",
2283
+ onClick: closeDialog,
2284
+ children: t("row.transferCancel")
2285
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2286
+ type: "button",
2287
+ className: "dsm-btn dsm-btn-outline",
2288
+ disabled: importSelected.size === 0 || phase.kind === "importing",
2289
+ onClick: () => {
2290
+ runImport();
2291
+ },
2292
+ children: phase.kind === "importing" ? t("row.transferImporting") : t("row.transferImportConfirm", { count: importSelected.size })
2293
+ })]
2294
+ })
2295
+ ] })
2296
+ ]
2297
+ }) : null
2298
+ ]
2299
+ });
1554
2300
  }
1555
2301
  //#endregion
1556
2302
  //#region src/native-modality.ts
@@ -1666,6 +2412,12 @@ window.__ModuleLoader__.load({
1666
2412
  .dsm-workbuddy-tab-switch{display:inline-flex;align-items:center;flex:none;padding:0 8px 0 2px;cursor:pointer}
1667
2413
  .dsm-workbuddy-tab-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
1668
2414
  .dsm-workbuddy-tab-switch input:disabled{opacity:.4;cursor:default}
2415
+ /* One sidebar-credit switch, belonging to the ACTIVE tab's region. It sits on
2416
+ its own line so it reads as a property of this tab rather than as a shared
2417
+ pair of settings — the tab bar already says which region is in scope. */
2418
+ .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)}
2419
+ .dsm-workbuddy-credits-switch input{margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
2420
+ .dsm-workbuddy-credits-switch input:disabled{opacity:.4;cursor:default}
1669
2421
  .dsm-workbuddy-tab-off{opacity:.55}
1670
2422
  /* Re-detection keeps a copy in the signed-out branch (that branch is where
1671
2423
  "I just signed in over there" applies), so it needs its own placement: the
@@ -1771,7 +2523,9 @@ window.__ModuleLoader__.load({
1771
2523
  .dsm-workbuddy-pool-warn{margin:0;padding:9px 11px;border-radius:9px;font-size:12px;line-height:17px;color:var(--dsw-alias-state-warn-primary,#f59e0b);background:rgba(245,158,11,.09);border:1px solid rgba(245,158,11,.3)}
1772
2524
  .dsm-workbuddy-pool-error{margin:0;font-size:12px;line-height:17px;color:var(--dsw-alias-state-error-primary,#ef4444);white-space:pre-line}
1773
2525
  .dsm-workbuddy-pool-table{border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
1774
- .dsm-workbuddy-pool-row{display:grid;grid-template-columns:1.6fr 1fr 1.3fr .9fr;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:13px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
2526
+ .dsm-workbuddy-pool-row{display:grid;grid-template-columns:1.6fr 1fr 1.3fr .9fr .8fr;gap:10px;align-items:center;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31);font-size:13px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
2527
+ /* No check-in column (the international region): the freed track goes to the actions. */
2528
+ .dsm-workbuddy-pool-table:not(.dsm-workbuddy-pool-table-checkin) .dsm-workbuddy-pool-row{grid-template-columns:1.6fr 1fr 1.3fr 1.7fr}
1775
2529
  .dsm-workbuddy-pool-row:first-child{border-top:0}
1776
2530
  .dsm-workbuddy-pool-row-head{padding:8px 14px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-tertiary,#999);font-size:11px;font-weight:600;letter-spacing:.02em;text-transform:uppercase}
1777
2531
  .dsm-workbuddy-pool-row-current{background:rgba(86,134,254,.07)}
@@ -1815,6 +2569,12 @@ window.__ModuleLoader__.load({
1815
2569
  .dsm-workbuddy-pool-settings-wrap{display:flex;flex-direction:column;gap:10px}
1816
2570
  .dsm-workbuddy-pool-dirty{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-state-warn-primary,#f59e0b)}
1817
2571
  .dsm-workbuddy-pool-settings{display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
2572
+ /* QR sign-in + export/import, hosted inside the pool settings block.
2573
+ The block's own bordered rows belong to the SAVED preferences, so these
2574
+ get their own container: same rhythm (12px 14px) so the two read as one
2575
+ group, but a dashed edge marking them as immediate actions rather than
2576
+ drafts the Save button below would commit. */
2577
+ .dsm-workbuddy-pool-tools{display:flex;flex-direction:column;gap:2px;border:1px dashed var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;padding:4px 10px}
1818
2578
  .dsm-workbuddy-pool-set{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:12px 14px;border-top:1px solid var(--dsw-alias-border-l1,#2c2d31)}
1819
2579
  .dsm-workbuddy-pool-set:first-child{border-top:0}
1820
2580
  .dsm-workbuddy-pool-set-copy{display:flex;flex-direction:column;gap:3px;min-width:0;flex:1}
@@ -1844,6 +2604,9 @@ window.__ModuleLoader__.load({
1844
2604
  .dsm-workbuddy-pool-log-error{color:var(--dsw-alias-state-error-primary,#ef4444)}
1845
2605
  .dsm-workbuddy-pool-log-info{color:var(--dsw-alias-label-secondary,#c6c9d0)}
1846
2606
  .dsm-workbuddy-pool-log-empty{padding:14px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999)}
2607
+ /* Per-row actions (test / remove), right side of each pool row. */
2608
+ .dsm-workbuddy-pool-row-actions{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex:none}
2609
+ .dsm-workbuddy-pool-row-actions .dsm-workbuddy-pool-small-btn{padding:2px 10px;font-size:12px}
1847
2610
  @media (max-width:760px){.dsm-workbuddy-pool-row{grid-template-columns:1fr 1fr;row-gap:6px}.dsm-workbuddy-pool-select{max-width:180px}}
1848
2611
  /* Pool switch, namespaced under .dsm-workbuddy-pool so it cannot collide with host styles. */
1849
2612
  .dsm-workbuddy-pool .sw{position:relative;display:inline-block;width:38px;height:22px;flex:none;cursor:pointer}
@@ -1854,6 +2617,167 @@ window.__ModuleLoader__.load({
1854
2617
  .dsm-workbuddy-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
1855
2618
  .dsm-workbuddy-pool .sw input:disabled+.sw-track{opacity:.4}
1856
2619
  .dsm-workbuddy-pool .sw input:disabled{cursor:default}
2620
+ /* Account action bars: QR sign-in and export/import.
2621
+ One row each — label, live status, actions — instead of a padded block
2622
+ per feature. The bar is the resting height; only an open dialog (below)
2623
+ grows vertically, so the card does not scroll past a wall of chrome to
2624
+ reach the buttons. Status text shares the row rather than claiming its own,
2625
+ and is what truncates first when the card is narrow. */
2626
+ .dsm-workbuddy-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:8px 0;padding:0}
2627
+ .dsm-workbuddy-bar-label{flex:none;margin:0;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#c6c9d0)}
2628
+ .dsm-workbuddy-bar-status{flex:1 1 auto;min-width:0;margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e9ed);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
2629
+ .dsm-workbuddy-bar-hint{color:var(--dsw-alias-label-tertiary,#999)}
2630
+ .dsm-workbuddy-bar-actions{display:flex;gap:8px;flex:none;margin-left:auto}
2631
+ .dsm-workbuddy-bar a{color:var(--dsw-alias-brand-primary,#5686fe);text-decoration:none}
2632
+ .dsm-workbuddy-bar a:hover{text-decoration:underline}
2633
+ .dsm-workbuddy-bar-ok{color:var(--dsw-alias-state-success-primary,#22a06b)}
2634
+ .dsm-workbuddy-bar-error{color:var(--dsw-alias-state-error-primary,#ef4444)}
2635
+ /* Secondary text inside the transfer DIALOG (not a bar): the privacy note
2636
+ shown when credentials leave the machine. Kept independent of the bar
2637
+ classes so the dialog does not depend on the bar's layout to be readable. */
2638
+ .dsm-workbuddy-hint{color:var(--dsw-alias-label-tertiary,#999)}
2639
+ /* The transfer dialog is deliberately a block: it holds a scrollable account
2640
+ list and per-entry checkboxes, which cannot live in a one-row bar. */
2641
+ .dsm-workbuddy-transfer-dialog{margin-top:8px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:10px;background:var(--dsw-alias-bg-secondary,transparent)}
2642
+ .dsm-workbuddy-transfer-dialog p{margin:0 0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary,#e8e9ed)}
2643
+ .dsm-workbuddy-transfer-dialog-actions{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap}
2644
+ .dsm-workbuddy-transfer-list{list-style:none;margin:0 0 8px;padding:0;max-height:180px;overflow-y:auto}
2645
+ .dsm-workbuddy-transfer-list label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);cursor:pointer;padding:2px 0}
2646
+ .dsm-workbuddy-transfer-list input[type="checkbox"]{flex:none}
2647
+ .dsm-workbuddy-transfer input[type="file"]{font-size:12px;color:var(--dsw-alias-label-primary,#e8e9ed);margin-bottom:8px}
2648
+ `;
2649
+ /**
2650
+ * Composer credit readout styles, injected by the readout component itself.
2651
+ *
2652
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
2653
+ * — 该项目的 `COMPOSER_POINTS_CSS`,类名改为 `dsm-workbuddy-*`,
2654
+ * 其余(尺寸、节奏、主题变量与回退值、表格对齐)逐条沿用。
2655
+ *
2656
+ * Separate from the card bundle because the readout lives in the session's
2657
+ * composer, not inside the plugin card; a card that is closed still shows this,
2658
+ * and on some hosts the card is never mounted at all.
2659
+ */
2660
+ const WORKBUDDY_COMPOSER_CSS = `
2661
+ /* The readout is plain composer text — no border, no fill at rest. The padding
2662
+ is only the click target, invisible until the pointer arrives. */
2663
+ .dsm-workbuddy-composer-credits{display:inline-flex;align-items:center;min-width:0}
2664
+ .dsm-workbuddy-composer-credits-trigger{
2665
+ appearance:none;display:inline-flex;align-items:center;align-self:stretch;
2666
+ padding:2px 6px;border:0;border-radius:6px;background:transparent;
2667
+ /* EXPLICIT, not inherit. The composer row's own font and body colour are
2668
+ noticeably larger and darker than the neighbouring controls — a credit
2669
+ readout is secondary information and must not shout. 12px matches this
2670
+ plugin's original line; label-tertiary matches the shell's own composer-row
2671
+ control (ContextMeter), which is the sibling this reads alongside. */
2672
+ font-size:12px;line-height:18px;
2673
+ color:var(--dsw-alias-label-tertiary,#999);
2674
+ white-space:nowrap;cursor:pointer;
2675
+ font-variant-numeric:tabular-nums;
2676
+ }
2677
+ .dsm-workbuddy-composer-credits-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14))}
2678
+ .dsm-workbuddy-composer-credits-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
2679
+ /* The panel is portaled to document.body and positioned from the trigger, so it
2680
+ MUST be fixed — otherwise the left/top it measures are ignored and it lands in
2681
+ document flow. All material comes from shell tokens so it follows the theme. */
2682
+ .dsm-workbuddy-composer-panel{
2683
+ position:fixed;z-index:1100;box-sizing:border-box;
2684
+ width:min(248px,calc(100vw - 24px));padding:12px;
2685
+ border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.28));
2686
+ border-radius:var(--dsw-radius-lg,14px);
2687
+ background:var(--dsw-specific-menu,Canvas);
2688
+ box-shadow:var(--dsw-elevation-prominent,0 8px 32px rgba(0,0,0,.22));
2689
+ backdrop-filter:var(--dsw-menu-backdrop-filter,none);
2690
+ font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#444);
2691
+ }
2692
+ .dsm-workbuddy-composer-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}
2693
+ .dsm-workbuddy-composer-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,CanvasText)}
2694
+ /* Self-contained: the dsm-btn primitive lives in TRAE_CARD_CSS, which the
2695
+ composer never loads, so the refresh button must style itself. */
2696
+ .dsm-workbuddy-composer-panel-refresh{
2697
+ appearance:none;flex:none;margin-left:auto;padding:3px 10px;
2698
+ border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.34));
2699
+ border-radius:7px;background:transparent;
2700
+ color:var(--dsw-alias-label-secondary,#444);
2701
+ font:inherit;font-size:11px;line-height:16px;cursor:pointer;
2702
+ }
2703
+ .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)}
2704
+ .dsm-workbuddy-composer-panel-refresh:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
2705
+ .dsm-workbuddy-composer-panel-refresh:disabled{opacity:.5;cursor:default}
2706
+ /* The panel is a TABLE: one row per account in the region, one figure (the
2707
+ general balance) per row, and a row click switches accounts.
2708
+
2709
+ Rhythm is deliberate: the header sits on a hairline, the switch button owns
2710
+ ALL horizontal padding (and pulls back by the same amount so its label lines
2711
+ up with the header), and every row is the same height. A misaligned first
2712
+ column is what makes a two-column popover read as broken. */
2713
+ .dsm-workbuddy-composer-panel-table{
2714
+ width:100%;border-collapse:collapse;
2715
+ font-size:12px;line-height:18px;
2716
+ }
2717
+ .dsm-workbuddy-composer-panel-table thead th{
2718
+ padding:0 0 7px;font-weight:500;text-align:left;font-size:11px;
2719
+ color:var(--dsw-alias-label-tertiary,GrayText);
2720
+ border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));
2721
+ }
2722
+ .dsm-workbuddy-composer-panel-num{
2723
+ text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap;
2724
+ }
2725
+ .dsm-workbuddy-composer-panel-table thead th.dsm-workbuddy-composer-panel-num{padding-right:0}
2726
+ .dsm-workbuddy-composer-panel-table tbody th{
2727
+ padding:0;font-weight:400;text-align:left;vertical-align:middle;
2728
+ }
2729
+ .dsm-workbuddy-composer-panel-table tbody td{
2730
+ padding:4px 0;vertical-align:middle;
2731
+ color:var(--dsw-alias-label-primary,CanvasText);
2732
+ }
2733
+ /* The current account reads as current without shouting: a filled dot and a
2734
+ slightly heavier weight. Disabled — pressing it would write a selection that
2735
+ has not changed. */
2736
+ .dsm-workbuddy-composer-panel-table tbody tr.dsm-workbuddy-composer-panel-row-current td{font-weight:600}
2737
+ .dsm-workbuddy-composer-panel-switch{
2738
+ appearance:none;display:flex;align-items:center;gap:7px;
2739
+ width:100%;padding:4px 6px;margin-left:-6px;
2740
+ border:0;border-radius:6px;background:transparent;
2741
+ color:inherit;font:inherit;font-size:12px;text-align:left;cursor:pointer;
2742
+ white-space:nowrap;overflow:hidden;
2743
+ }
2744
+ .dsm-workbuddy-composer-panel-switch>span:last-child{overflow:hidden;text-overflow:ellipsis}
2745
+ /* The name + optional reason mark. A COLUMN so the mark drops onto its own line
2746
+ under the name: side by side they would stretch the name column and squeeze
2747
+ the numeric one, and the switch's own overflow rule would ellipsise the mark
2748
+ instead of the name. */
2749
+ .dsm-workbuddy-composer-panel-who{display:flex;flex-direction:column;min-width:0;gap:1px}
2750
+ .dsm-workbuddy-composer-panel-name{overflow:hidden;text-overflow:ellipsis}
2751
+ /* Why the account cannot serve right now ("被限流"), in the warn colour the pool
2752
+ table uses for the same fact — so one measurement reads the same on both. */
2753
+ .dsm-workbuddy-composer-panel-mark{
2754
+ font-size:11px;line-height:15px;
2755
+ color:var(--dsw-alias-state-warn-primary,#f59e0b);
2756
+ }
2757
+ /* Hover belongs to the switch, not the row: the number cell is not clickable,
2758
+ so lighting it up would promise an action it cannot perform. */
2759
+ .dsm-workbuddy-composer-panel-switch:hover:not(:disabled){
2760
+ background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14));
2761
+ }
2762
+ .dsm-workbuddy-composer-panel-switch:focus-visible{
2763
+ outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:-1px;
2764
+ }
2765
+ .dsm-workbuddy-composer-panel-switch:disabled{cursor:default}
2766
+ .dsm-workbuddy-composer-panel-dot{
2767
+ flex:none;width:7px;height:7px;border-radius:50%;
2768
+ background:var(--dsw-alias-label-dimmed,rgba(127,127,127,.5));
2769
+ }
2770
+ .dsm-workbuddy-composer-panel-table tr.dsm-workbuddy-composer-panel-row-current .dsm-workbuddy-composer-panel-dot{
2771
+ background:var(--dsw-state-success,#22a06b);
2772
+ }
2773
+ .dsm-workbuddy-composer-panel-foot{
2774
+ display:flex;justify-content:space-between;gap:8px;
2775
+ margin-top:9px;padding-top:7px;
2776
+ border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.24));
2777
+ font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,GrayText);
2778
+ }
2779
+ .dsm-workbuddy-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
2780
+ .dsm-workbuddy-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
1857
2781
  `;
1858
2782
  //#endregion
1859
2783
  //#region src/client/searched-paths.ts
@@ -2233,6 +3157,14 @@ window.__ModuleLoader__.load({
2233
3157
  /** Region whose on/off checkbox write is in flight, so its box can't race. */
2234
3158
  const [togglingRegion, setTogglingRegion] = (0, react.useState)(void 0);
2235
3159
  /**
3160
+ * Region whose SIDEBAR-credit checkbox write is in flight.
3161
+ *
3162
+ * Tracked separately from {@link togglingRegion} because they are different
3163
+ * fields of the same region slot: one in-flight write must not disable the
3164
+ * other control, and both must stay serialized against the SAVE writers.
3165
+ */
3166
+ const [togglingCredits, setTogglingCredits] = (0, react.useState)(void 0);
3167
+ /**
2236
3168
  * Probe results, keyed by region then model id.
2237
3169
  *
2238
3170
  * Kept per region for the same reason drafts are: the two tabs are separate
@@ -2425,6 +3357,21 @@ window.__ModuleLoader__.load({
2425
3357
  };
2426
3358
  const activeRegionOn = regionOn(activeRegion);
2427
3359
  /**
3360
+ * Whether one region's sidebar credit line is shown — for the ACTIVE tab.
3361
+ *
3362
+ * Prefers the Host's answer, exactly like {@link regionOn} and for the same
3363
+ * measured reason: on the affected 0.1.7 deployment the browser's settings
3364
+ * mirror never picks up this plugin's writes, so a checkbox reading the mirror
3365
+ * rendered its previous value after a successful save and looked impossible to
3366
+ * turn off ("关闭不了"). The Host is the authority; the mirror is only a
3367
+ * fallback for a Host line that predates the field.
3368
+ */
3369
+ const creditsShownOn = (item) => {
3370
+ const fromHost = statusByRegion[item]?.showCreditsInMainUi;
3371
+ if (typeof fromHost === "boolean") return fromHost;
3372
+ return regionCreditsShownOf(settingsScope?.getSnapshot().value, item);
3373
+ };
3374
+ /**
2428
3375
  * Switch one region's provider off or on. The write carries the region's
2429
3376
  * whole slot through untouched — only `enabled` changes — so the user's
2430
3377
  * directory, model picks, image opt-ins and budgets survive a round trip.
@@ -2450,6 +3397,32 @@ window.__ModuleLoader__.load({
2450
3397
  if (mounted.current) setTogglingRegion(void 0);
2451
3398
  }
2452
3399
  };
3400
+ /**
3401
+ * Show or hide one region's sidebar credit line.
3402
+ *
3403
+ * This is the SECOND control for that line: the sidebar row also carries an
3404
+ * `×` that writes the same `regions.<region>.showCreditsInMainUi` flag, and
3405
+ * both go through the same landed-checked writer, so they can never disagree.
3406
+ * The checkbox stays because the sidebar is not always rendered (collapsed
3407
+ * rail, or a host that never mounts the footer slot).
3408
+ *
3409
+ * Writes the region's WHOLE slot — unchanged except for this one flag — so a
3410
+ * hide never drops the directory, model picks, budgets or pool preferences
3411
+ * that live alongside it.
3412
+ */
3413
+ const toggleCredits = async (item, shown) => {
3414
+ if (settingsScope === void 0) return;
3415
+ setTogglingCredits(item);
3416
+ try {
3417
+ const slot = nextRegionCreditsShown(settingsScope.getSnapshot().value, item, shown)[item];
3418
+ await writeRegionCreditsShown(settingsScope, item, shown, typeof slot === "object" && slot !== null ? slot : {});
3419
+ await refreshUsage(item);
3420
+ } catch (error) {
3421
+ if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
3422
+ } finally {
3423
+ if (mounted.current) setTogglingCredits(void 0);
3424
+ }
3425
+ };
2453
3426
  const refreshModels = async () => {
2454
3427
  setBusy(true);
2455
3428
  try {
@@ -2689,6 +3662,25 @@ window.__ModuleLoader__.load({
2689
3662
  searched
2690
3663
  });
2691
3664
  status.status === "error" || status.selectionExplicit;
3665
+ /**
3666
+ * Account-adding and account-migration controls, defined ONCE and rendered
3667
+ * in both sign-in states: inside the pool settings block when an account
3668
+ * exists, and in the signed-out branch when it does not (the pool section is
3669
+ * not rendered there at all). Two JSX copies would be free to drift, and the
3670
+ * two states are the same feature — a user who learns these controls in one
3671
+ * must find them in the same order in the other.
3672
+ */
3673
+ const accountTools = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OAuthSignIn, {
3674
+ t,
3675
+ region: activeRegion
3676
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountTransfer, {
3677
+ t,
3678
+ region: activeRegion,
3679
+ accounts: status.status === "error" ? [] : status.accounts.map((account) => ({
3680
+ id: account.id,
3681
+ accountName: account.accountName
3682
+ }))
3683
+ })] });
2692
3684
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
2693
3685
  className: `dsm-plugin-card${open ? " dsm-plugin-card-open" : ""}`,
2694
3686
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -2773,6 +3765,18 @@ window.__ModuleLoader__.load({
2773
3765
  className: "dsm-workbuddy-tab-off-notice",
2774
3766
  children: t("row.tabOffNotice")
2775
3767
  }) : null,
3768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3769
+ className: "dsm-workbuddy-credits-switch",
3770
+ title: activeRegion === "cn" ? t("row.showCreditsCnHint") : t("row.showCreditsGlobalHint"),
3771
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3772
+ type: "checkbox",
3773
+ checked: activeRegionOn ? creditsShownOn(activeRegion) : false,
3774
+ disabled: !activeRegionOn || togglingCredits === activeRegion || !canWrite || saving || poolBusy,
3775
+ onChange: (event) => {
3776
+ toggleCredits(activeRegion, event.target.checked);
3777
+ }
3778
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activeRegion === "cn" ? t("row.showCreditsCn") : t("row.showCreditsGlobal") })]
3779
+ }),
2776
3780
  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
3781
  className: "dsm-workbuddy-usage-hint",
2778
3782
  children: t("row.reloginHint")
@@ -2823,6 +3827,7 @@ window.__ModuleLoader__.load({
2823
3827
  switchAccount(accountId);
2824
3828
  },
2825
3829
  selectingAccount: switchingAccount,
3830
+ accountTools,
2826
3831
  siblingBusy: saving || togglingRegion !== void 0,
2827
3832
  onBusyChange: setPoolBusy,
2828
3833
  onSaved: async () => await refreshUsage(activeRegion) !== void 0,
@@ -3061,6 +4066,10 @@ window.__ModuleLoader__.load({
3061
4066
  children: busy ? t("row.accountsScanning") : t("row.accountsRescan")
3062
4067
  })
3063
4068
  }),
4069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4070
+ className: "dsm-workbuddy-pool-tools",
4071
+ children: accountTools
4072
+ }),
3064
4073
  searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchedPaths, {
3065
4074
  items: searched,
3066
4075
  t
@@ -3076,6 +4085,565 @@ window.__ModuleLoader__.load({
3076
4085
  });
3077
4086
  }
3078
4087
  //#endregion
4088
+ //#region src/client/popover.ts
4089
+ /**
4090
+ * The two shell behaviours the composer credit panel needs: hanging the panel
4091
+ * off its trigger, and dismissing it on an outside press.
4092
+ *
4093
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
4094
+ * — 该项目的 `client/popover.ts`,逐条沿用其判断:同样的上下放置、同样的
4095
+ * 视口收边、同样的 scroll(capture)/resize/自身尺寸变化重测、同样的
4096
+ * 「pointerdown 落在触发器与面板之外即关闭」。这些不是随手写的——那边
4097
+ * 2.12.0/2.13.0 的客户端加载失败与面板位置错乱都留下了记录,这里直接
4098
+ * 采用其最终结论。
4099
+ *
4100
+ * Implemented locally rather than imported from
4101
+ * `@deepseek-ai/dsh-client-ui-primitives`. The shell DOES register that package
4102
+ * for plugin bundles, so importing it is legal — this is a deliberate choice,
4103
+ * not a workaround: the two behaviours are ~40 lines of DOM geometry, and owning
4104
+ * them keeps this component renderable in the Node test suite without mocking a
4105
+ * package that itself reaches `*.module.css` and further DSH dependencies.
4106
+ *
4107
+ * NOTE ON A MISDIAGNOSIS, kept because it is the kind of thing that gets
4108
+ * re-litigated: the client load failure on the referring project was first
4109
+ * attributed to importing the primitives package. It was not that. The real
4110
+ * cause was `import { createPortal } from 'react-dom'` being BUNDLED rather than
4111
+ * externalized — the inlined development build opens with
4112
+ * `process.env.NODE_ENV` and threw `ReferenceError: process is not defined` in
4113
+ * the browser, at module-evaluation time, so the ENTIRE client half failed to
4114
+ * import. See `tsdown.config.ts` (`CLIENT_EXTERNALS`) for the fix and
4115
+ * `tests/client-runtime-imports.spec.ts` for the rule that now pins it.
4116
+ */
4117
+ /**
4118
+ * Clearance to keep from the top of the viewport.
4119
+ *
4120
+ * The shell reserves a strip at the top of the frame
4121
+ * (`--dsh-frame-top-clearance`) for the window chrome, so a panel clamped to
4122
+ * `margin` alone can still land under it. Absent variable or no `data-fullscreen`
4123
+ * distinction is handled the same way the shell does: fall back to `min`.
4124
+ */
4125
+ function overlayTopMargin(min) {
4126
+ if (typeof document === "undefined") return min;
4127
+ const root = document.documentElement;
4128
+ const clearance = Number.parseFloat(getComputedStyle(root).getPropertyValue("--dsh-frame-top-clearance"));
4129
+ if (Number.isNaN(clearance)) return min;
4130
+ return Math.max(min, (root.hasAttribute("data-fullscreen") ? 0 : clearance) + 20);
4131
+ }
4132
+ /** Keep a `position: fixed` floating element anchored to a trigger. */
4133
+ function useAnchoredPosition(options) {
4134
+ const { open, anchorRef, panelRef, side = "bottom", align = "start", gap, margin } = options;
4135
+ const [position, setPosition] = (0, react.useState)(null);
4136
+ (0, react.useLayoutEffect)(() => {
4137
+ if (!open) {
4138
+ setPosition(null);
4139
+ return;
4140
+ }
4141
+ const place = () => {
4142
+ const rect = anchorRef.current?.getBoundingClientRect();
4143
+ if (rect === void 0) return;
4144
+ const panel = panelRef.current;
4145
+ const width = panel?.offsetWidth ?? 0;
4146
+ const height = panel?.offsetHeight ?? 0;
4147
+ let left = align === "end" ? rect.right - width : rect.left;
4148
+ let top = side === "top" ? rect.top - gap - height : rect.bottom + gap;
4149
+ if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
4150
+ if (height > 0) top = Math.min(Math.max(top, overlayTopMargin(margin)), window.innerHeight - height - margin);
4151
+ setPosition({
4152
+ left,
4153
+ top
4154
+ });
4155
+ };
4156
+ place();
4157
+ window.addEventListener("scroll", place, true);
4158
+ window.addEventListener("resize", place);
4159
+ const panel = panelRef.current;
4160
+ let observer = null;
4161
+ if (typeof ResizeObserver !== "undefined" && panel !== null) {
4162
+ observer = new ResizeObserver(place);
4163
+ observer.observe(panel);
4164
+ }
4165
+ return () => {
4166
+ observer?.disconnect();
4167
+ window.removeEventListener("scroll", place, true);
4168
+ window.removeEventListener("resize", place);
4169
+ };
4170
+ }, [
4171
+ open,
4172
+ anchorRef,
4173
+ panelRef,
4174
+ side,
4175
+ align,
4176
+ gap,
4177
+ margin
4178
+ ]);
4179
+ return position;
4180
+ }
4181
+ /**
4182
+ * Close an open popover when a pointerdown lands outside it.
4183
+ *
4184
+ * `portal` counts as inside: the panel is portaled to `document.body`, so it is
4185
+ * not a descendant of `root` and a click on the panel itself would otherwise
4186
+ * read as "outside" and dismiss the popover the user is trying to use.
4187
+ */
4188
+ function useDismissOnOutsidePointer(root, open, setOpen, portal) {
4189
+ (0, react.useEffect)(() => {
4190
+ if (!open) return void 0;
4191
+ const closeOutside = (event) => {
4192
+ if (event.target instanceof Node && root.current?.contains(event.target) !== true && portal?.current?.contains(event.target) !== true) setOpen(false);
4193
+ };
4194
+ document.addEventListener("pointerdown", closeOutside);
4195
+ return () => {
4196
+ document.removeEventListener("pointerdown", closeOutside);
4197
+ };
4198
+ }, [
4199
+ root,
4200
+ open,
4201
+ setOpen,
4202
+ portal
4203
+ ]);
4204
+ }
4205
+ //#endregion
4206
+ //#region src/client/ComposerCredits.tsx
4207
+ /**
4208
+ * Credit readout for the composer tool row (`conversation.input.left`).
4209
+ *
4210
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
4211
+ * — 该项目的 `client/ComposerPoints.tsx`:读数形态(`<区域> · <余额>`)、
4212
+ * 点击弹出锚定面板、面板内是**账号表格**(点行即切换账号)、刷新按钮
4213
+ * 放在面板里而不是读数上、每账号读数**只在面板打开时抓**、5 分钟定时器、
4214
+ * 失败保留上次数值。本文件把「只有国内版」扩成「两个区域都支持」:那边
4215
+ * 的国际版是订阅制、没有可比数字,本插件的国际版有真实余额,所以按当前
4216
+ * 选中模型所属的区域显示,并在读数上标明是 WB CN 还是 WB AI。
4217
+ *
4218
+ * Shows the selected region's general credit balance, but ONLY while the
4219
+ * session's selected model belongs to this plugin (the gate decides that).
4220
+ *
4221
+ * The row itself is a bare clickable readout — `WB CN · 1,072` — with NO
4222
+ * refresh control: the manual refresh lives in the panel, because the row is
4223
+ * shared with the shell's own permission/agent/model controls and every extra
4224
+ * control there costs space that is not ours. Clicking it opens an anchored
4225
+ * panel: a TABLE with one row per account in that region and its balance, where
4226
+ * a row click switches to it.
4227
+ *
4228
+ * Two deliberate choices in that panel:
4229
+ *
4230
+ * - Per-account figures are fetched ONLY while the panel is open. The 5-minute
4231
+ * readout must not multiply its upstream reads by the account count — a
4232
+ * healthy balance should not pay for a table nobody is looking at.
4233
+ * - A row whose balance could not be read shows `—`, never `0`. The endpoint
4234
+ * omits the field in that case precisely so this component cannot render a
4235
+ * wrong claim about the account.
4236
+ */
4237
+ /**
4238
+ * Inject the readout's styles into the document head, once, on module load.
4239
+ *
4240
+ * Same idempotent pattern as the card's injection, and for the same reason: the
4241
+ * classes are attached to DOM this component owns, and `WORKBUDDY_CARD_CSS` only
4242
+ * loads when the settings card does — which is not guaranteed on a host where
4243
+ * the card is never rendered. The referring project shipped exactly that bug
4244
+ * (the trigger rendered as a default button box and the panel, without
4245
+ * `position: fixed`, had its measured left/top ignored and appeared nowhere).
4246
+ */
4247
+ if (typeof document !== "undefined") {
4248
+ const cssId = "dsh-connect-workbuddy/composer-credits.css";
4249
+ if (!document.querySelector(`style[data-plugin-css="${cssId}"]`)) {
4250
+ const styleTag = document.createElement("style");
4251
+ styleTag.dataset.plugin = "dsh-connect-workbuddy";
4252
+ styleTag.dataset.pluginCss = cssId;
4253
+ styleTag.textContent = WORKBUDDY_COMPOSER_CSS;
4254
+ document.head.appendChild(styleTag);
4255
+ }
4256
+ }
4257
+ /**
4258
+ * Automatic refresh period. Five minutes, matching a credit balance's actual
4259
+ * rate of change: it moves on the scale of model calls, not seconds, and each
4260
+ * read is a read-only upstream call.
4261
+ */
4262
+ const COMPOSER_CREDITS_REFRESH_INTERVAL_MS = 3e5;
4263
+ /**
4264
+ * Provider routes whose credit balance this readout can show, mapped to the
4265
+ * region to read it from.
4266
+ *
4267
+ * BOTH regions, unlike the referring project: its international side is
4268
+ * subscription-based and has no comparable balance, while WorkBuddy's
4269
+ * international accounts do carry one, so there is a real number to show on
4270
+ * either side. Mirrors `WORKBUDDY_PROVIDERS` on the host side; duplicated here
4271
+ * because the client bundle must not import the host entry.
4272
+ */
4273
+ const WORKBUDDY_COMPOSER_PROVIDERS = {
4274
+ "workbuddy": "cn",
4275
+ "workbuddy-global": "global"
4276
+ };
4277
+ /** The credit total out of one usage document, or undefined. */
4278
+ function creditsOf(usage) {
4279
+ if (usage.status !== "signed-in") return void 0;
4280
+ return usage.credits?.total;
4281
+ }
4282
+ /** Group the digits so a four-figure balance stays readable in a narrow row. */
4283
+ function formatCredits(value) {
4284
+ return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
4285
+ }
4286
+ function formatClock(value) {
4287
+ return new Date(value).toLocaleTimeString("zh-CN", {
4288
+ hour: "2-digit",
4289
+ minute: "2-digit"
4290
+ });
4291
+ }
4292
+ function ComposerCredits(props) {
4293
+ const { t, provider, region, settingsScope } = props;
4294
+ if (t === void 0) throw new Error("Composer credits readout requires its translation function");
4295
+ const owned = provider === void 0 ? void 0 : WORKBUDDY_COMPOSER_PROVIDERS[provider];
4296
+ const activeRegion = region ?? owned;
4297
+ const [usage, setUsage] = (0, react.useState)(void 0);
4298
+ const [accountCredits, setAccountCredits] = (0, react.useState)(void 0);
4299
+ const [busy, setBusy] = (0, react.useState)(false);
4300
+ const [switchingId, setSwitchingId] = (0, react.useState)(void 0);
4301
+ const [failed, setFailed] = (0, react.useState)(false);
4302
+ const [lastRefresh, setLastRefresh] = (0, react.useState)(void 0);
4303
+ const [open, setOpen] = (0, react.useState)(false);
4304
+ const inFlight = (0, react.useRef)(false);
4305
+ const mounted = (0, react.useRef)(true);
4306
+ const rootRef = (0, react.useRef)(null);
4307
+ const panelRef = (0, react.useRef)(null);
4308
+ (0, react.useEffect)(() => {
4309
+ mounted.current = true;
4310
+ return () => {
4311
+ mounted.current = false;
4312
+ };
4313
+ }, []);
4314
+ const position = useAnchoredPosition({
4315
+ open: open && activeRegion !== void 0,
4316
+ anchorRef: rootRef,
4317
+ panelRef,
4318
+ side: "top",
4319
+ gap: 8,
4320
+ margin: 12
4321
+ });
4322
+ useDismissOnOutsidePointer(rootRef, open, setOpen, panelRef);
4323
+ const fetchUsage = (0, react.useCallback)(async (signal) => {
4324
+ if (inFlight.current || activeRegion === void 0) return;
4325
+ inFlight.current = true;
4326
+ if (mounted.current) setBusy(true);
4327
+ try {
4328
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, activeRegion), {
4329
+ headers: { accept: "application/json" },
4330
+ credentials: "same-origin",
4331
+ ...signal === void 0 ? {} : { signal }
4332
+ });
4333
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
4334
+ const value = await response.json();
4335
+ if (!mounted.current || signal?.aborted === true) return;
4336
+ setUsage(value);
4337
+ setLastRefresh(Date.now());
4338
+ setFailed(false);
4339
+ } catch {
4340
+ if (mounted.current && signal?.aborted !== true) setFailed(true);
4341
+ } finally {
4342
+ inFlight.current = false;
4343
+ if (mounted.current) setBusy(false);
4344
+ }
4345
+ }, [activeRegion]);
4346
+ /**
4347
+ * Every account's balance in the region — one upstream read per account.
4348
+ *
4349
+ * Paid only while the panel is open (see the note at the top of this file).
4350
+ * A failure keeps whatever we already have rather than clearing it: the table
4351
+ * falls back to the usage document's account list, so a failed figure must not
4352
+ * remove the switch.
4353
+ */
4354
+ const fetchAccountCredits = (0, react.useCallback)(async (signal) => {
4355
+ if (activeRegion === void 0) return;
4356
+ try {
4357
+ const response = await fetch(withWorkBuddyRegion(WORKBUDDY_ACCOUNT_CREDITS_PATH, activeRegion), {
4358
+ headers: { accept: "application/json" },
4359
+ credentials: "same-origin",
4360
+ ...signal === void 0 ? {} : { signal }
4361
+ });
4362
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
4363
+ const data = await response.json();
4364
+ if (!mounted.current || signal?.aborted === true) return;
4365
+ if (Array.isArray(data.accounts)) setAccountCredits(data.accounts);
4366
+ } catch {}
4367
+ }, [activeRegion]);
4368
+ (0, react.useEffect)(() => {
4369
+ if (owned === void 0) return void 0;
4370
+ let cancelled = false;
4371
+ let timer;
4372
+ const controller = new AbortController();
4373
+ fetchUsage(controller.signal).finally(() => {
4374
+ if (cancelled) return;
4375
+ timer = window.setInterval(() => {
4376
+ fetchUsage(controller.signal);
4377
+ }, COMPOSER_CREDITS_REFRESH_INTERVAL_MS);
4378
+ });
4379
+ return () => {
4380
+ cancelled = true;
4381
+ if (timer !== void 0) window.clearInterval(timer);
4382
+ controller.abort();
4383
+ };
4384
+ }, [owned, fetchUsage]);
4385
+ (0, react.useEffect)(() => {
4386
+ if (!open || activeRegion === void 0) return void 0;
4387
+ const controller = new AbortController();
4388
+ fetchAccountCredits(controller.signal);
4389
+ return () => {
4390
+ controller.abort();
4391
+ };
4392
+ }, [
4393
+ open,
4394
+ activeRegion,
4395
+ fetchAccountCredits
4396
+ ]);
4397
+ (0, react.useEffect)(() => {
4398
+ if (!open) return void 0;
4399
+ const onKeyDown = (event) => {
4400
+ if (event.key === "Escape") setOpen(false);
4401
+ };
4402
+ document.addEventListener("keydown", onKeyDown);
4403
+ return () => {
4404
+ document.removeEventListener("keydown", onKeyDown);
4405
+ };
4406
+ }, [open]);
4407
+ /**
4408
+ * Switch to another account, then re-read both the usage document and the
4409
+ * table so the marker, the readout and the panel all move together.
4410
+ *
4411
+ * Goes through the shared `writeAccountSlot`, which is read-back checked and
4412
+ * carries the host-endpoint-first path: `set()` resolving is not proof the
4413
+ * write landed, and a refused write must report failure instead of showing a
4414
+ * marker on an account that was never selected.
4415
+ */
4416
+ const switchTo = async (accountId) => {
4417
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
4418
+ if (activeRegion === void 0 || switchingId !== void 0) return;
4419
+ if (accountId === (accountCredits?.find((account) => account.selected)?.id ?? (usage?.status === "signed-in" ? usage.accountId : void 0))) return;
4420
+ setSwitchingId(accountId);
4421
+ setFailed(false);
4422
+ try {
4423
+ await writeAccountSlot(settingsScope, activeRegion, accountId);
4424
+ await fetchUsage();
4425
+ await fetchAccountCredits();
4426
+ } catch {
4427
+ if (mounted.current) setFailed(true);
4428
+ } finally {
4429
+ if (mounted.current) setSwitchingId(void 0);
4430
+ }
4431
+ };
4432
+ if (owned === void 0) return null;
4433
+ const signedIn = usage?.status === "signed-in" ? usage : void 0;
4434
+ const total = signedIn === void 0 ? void 0 : creditsOf(signedIn);
4435
+ const signedOut = usage !== void 0 && usage.status === "signed-out";
4436
+ const valueText = total === void 0 ? "—" : formatCredits(total);
4437
+ const label = t(activeRegion === "cn" ? "composer.pointsCn" : "composer.pointsGlobal");
4438
+ /**
4439
+ * The table's rows: every account in this region, with its balance.
4440
+ *
4441
+ * The endpoint's answer is authoritative (it carries the figures). Before it
4442
+ * lands — or if it fails — fall back to the usage document's account list so
4443
+ * the switches are reachable immediately, showing `—` for the figure rather
4444
+ * than a spinner the user has to wait on.
4445
+ */
4446
+ const selectedId = accountCredits?.find((account) => account.selected)?.id ?? (signedIn === void 0 ? void 0 : signedIn.accountId);
4447
+ const rows = (accountCredits ?? (signedIn === void 0 ? [] : signedIn.accounts.map((account) => ({
4448
+ id: account.id,
4449
+ accountName: account.accountName,
4450
+ selected: account.selected
4451
+ })))).map((row) => {
4452
+ if (row.id !== selectedId || row.credits !== void 0 || total === void 0) return row;
4453
+ return {
4454
+ ...row,
4455
+ credits: total
4456
+ };
4457
+ });
4458
+ const canSwitch = settingsScope !== void 0 && settingsScope.getSnapshot().writable === true;
4459
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4460
+ ref: rootRef,
4461
+ className: "dsm-workbuddy-composer-credits",
4462
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4463
+ type: "button",
4464
+ className: "dsm-workbuddy-composer-credits-trigger",
4465
+ "aria-haspopup": "dialog",
4466
+ "aria-expanded": open,
4467
+ onClick: () => {
4468
+ setOpen(!open);
4469
+ },
4470
+ children: [
4471
+ label,
4472
+ " · ",
4473
+ valueText
4474
+ ]
4475
+ }), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4476
+ ref: panelRef,
4477
+ className: "dsm-workbuddy-composer-panel",
4478
+ style: position ?? {
4479
+ visibility: "hidden",
4480
+ left: 0,
4481
+ top: 0
4482
+ },
4483
+ role: "dialog",
4484
+ "aria-label": t("composer.panelTitle"),
4485
+ children: [
4486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4487
+ className: "dsm-workbuddy-composer-panel-head",
4488
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4489
+ className: "dsm-workbuddy-composer-panel-title",
4490
+ children: t(activeRegion === "cn" ? "composer.panelTitleCn" : "composer.panelTitleGlobal")
4491
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4492
+ type: "button",
4493
+ className: "dsm-workbuddy-composer-panel-refresh",
4494
+ disabled: busy || switchingId !== void 0,
4495
+ onClick: () => {
4496
+ fetchUsage();
4497
+ },
4498
+ children: busy ? t("composer.refreshing") : t("composer.refresh")
4499
+ })]
4500
+ }),
4501
+ signedOut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4502
+ className: "dsm-workbuddy-composer-panel-empty",
4503
+ children: t("composer.signedOut")
4504
+ }) : rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4505
+ className: "dsm-workbuddy-composer-panel-empty",
4506
+ children: t("composer.noAccounts")
4507
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
4508
+ className: "dsm-workbuddy-composer-panel-table",
4509
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
4510
+ scope: "col",
4511
+ children: t("composer.account")
4512
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
4513
+ scope: "col",
4514
+ className: "dsm-workbuddy-composer-panel-num",
4515
+ children: t("composer.credits")
4516
+ })] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row) => {
4517
+ const current = row.id === selectedId;
4518
+ const excluded = exclusionText(t, row.excludedBy);
4519
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
4520
+ className: current ? "dsm-workbuddy-composer-panel-row-current" : void 0,
4521
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
4522
+ scope: "row",
4523
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4524
+ type: "button",
4525
+ className: "dsm-workbuddy-composer-panel-switch",
4526
+ disabled: !canSwitch || switchingId !== void 0 || current,
4527
+ "aria-label": t("composer.switchTo", { account: row.accountName }),
4528
+ onClick: () => {
4529
+ switchTo(row.id);
4530
+ },
4531
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4532
+ className: "dsm-workbuddy-composer-panel-dot",
4533
+ "aria-hidden": "true"
4534
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4535
+ className: "dsm-workbuddy-composer-panel-who",
4536
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4537
+ className: "dsm-workbuddy-composer-panel-name",
4538
+ children: row.accountName
4539
+ }), excluded === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4540
+ className: "dsm-workbuddy-composer-panel-mark",
4541
+ children: excluded
4542
+ })]
4543
+ })]
4544
+ })
4545
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
4546
+ className: "dsm-workbuddy-composer-panel-num",
4547
+ children: row.credits === void 0 ? "—" : formatCredits(row.credits)
4548
+ })]
4549
+ }, row.id);
4550
+ }) })]
4551
+ }),
4552
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4553
+ className: "dsm-workbuddy-composer-panel-foot",
4554
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
4555
+ t("composer.lastRefresh"),
4556
+ " ",
4557
+ lastRefresh === void 0 ? "—" : formatClock(lastRefresh)
4558
+ ] })
4559
+ }),
4560
+ failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4561
+ className: "dsm-workbuddy-composer-panel-error",
4562
+ role: "status",
4563
+ children: t("composer.refreshFailed")
4564
+ }) : null
4565
+ ]
4566
+ }), document.body)]
4567
+ });
4568
+ }
4569
+ //#endregion
4570
+ //#region src/client/ComposerCreditsGate.tsx
4571
+ /**
4572
+ * Decides whether the composer credit readout belongs in this session, by
4573
+ * reading the Host's own `modelSelection` projection and the plugin's
4574
+ * per-region `showCreditsInMainUi` switch.
4575
+ *
4576
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
4577
+ * — 该项目的 `client/ComposerPointsGate.tsx`:从 `modelSelection` 投影读
4578
+ * 「本会话当前用哪个 provider」来决定要不要渲染,以及 `next` 优先于
4579
+ * `lastUsed` 的判定。这是本项目从侧边栏迁到输入框的核心机制。
4580
+ *
4581
+ * WHY the projection rather than this plugin's own state: the projection is
4582
+ * what the model picker writes and what the next request will use, so the
4583
+ * readout cannot disagree with the model actually in effect. `next` outranks
4584
+ * `lastUsed` because a selection that has been made but not yet sent is still
4585
+ * what the composer shows the user — the same precedence the shell itself uses.
4586
+ *
4587
+ * WHY the composer row rather than the sidebar foot: the sidebar foot is a
4588
+ * SINGLE root-scope row shared by every bundle, so two connector plugins each
4589
+ * parking a line there fight over the same width (measured on the referring
4590
+ * project: its line was squeezed to a clipped "Tr" beside this plugin's two
4591
+ * rows). The composer row is per-session and provider-scoped, so exactly one
4592
+ * plugin's credits are relevant at a time and nothing overlaps.
4593
+ *
4594
+ * Kept separate from {@link ComposerCredits} (and from the browser-plugin
4595
+ * entry) so the decision can be unit-tested without a Host: the entry imports
4596
+ * browser-only DSH packages the test environment cannot load, while this module
4597
+ * depends only on React and a narrow projection shape.
4598
+ */
4599
+ /**
4600
+ * The provider route the session is currently pointed at.
4601
+ *
4602
+ * `next` wins over `lastUsed`: a switch that has landed but not yet been sent
4603
+ * is still the selection the composer displays.
4604
+ */
4605
+ function selectedProviderOf(projection) {
4606
+ const provider = (projection?.next ?? projection?.lastUsed)?.provider;
4607
+ return typeof provider === "string" && provider !== "" ? provider : void 0;
4608
+ }
4609
+ /**
4610
+ * Which region's readout this session should show, or undefined for none.
4611
+ *
4612
+ * Returns undefined when the selected provider is not this plugin's — the
4613
+ * ownership test and the region choice are ONE lookup, so there is no second
4614
+ * copy of the rule to drift.
4615
+ *
4616
+ * The region's own switches are applied HERE rather than inside the component,
4617
+ * so a region that is switched off (or whose provider is off) starts no fetch
4618
+ * loop at all.
4619
+ */
4620
+ function composerCreditsRegionOf(projection, scope) {
4621
+ const provider = selectedProviderOf(projection);
4622
+ const region = provider === void 0 ? void 0 : WORKBUDDY_COMPOSER_PROVIDERS[provider];
4623
+ if (region === void 0) return void 0;
4624
+ const value = scope === void 0 ? void 0 : unwrapVolatileDeep(scope.getSnapshot().value);
4625
+ if (!regionCreditsShownOf(value, region)) return void 0;
4626
+ if (!regionEnabledOf(value, region)) return void 0;
4627
+ return region;
4628
+ }
4629
+ function ComposerCreditsGate(props) {
4630
+ const { t, settingsScope, useProjection } = props;
4631
+ const [, setRevision] = (0, react.useState)(0);
4632
+ (0, react.useEffect)(() => settingsScope?.subscribe(() => {
4633
+ setRevision((value) => value + 1);
4634
+ }), [settingsScope]);
4635
+ const projection = useProjection === void 0 ? void 0 : useProjection("modelSelection");
4636
+ const region = composerCreditsRegionOf(projection, settingsScope);
4637
+ if (region === void 0) return null;
4638
+ const provider = selectedProviderOf(projection);
4639
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ComposerCredits, {
4640
+ t,
4641
+ provider,
4642
+ region,
4643
+ ...settingsScope === void 0 ? {} : { settingsScope }
4644
+ });
4645
+ }
4646
+ //#endregion
3079
4647
  //#region src/client/locales.ts
3080
4648
  /**
3081
4649
  * Plugin-card copy registered under the settings.workbuddy locale namespace.
@@ -3179,6 +4747,37 @@ window.__ModuleLoader__.load({
3179
4747
  "row.probeFailedStatus": "✗ Test failed (HTTP {status})",
3180
4748
  "row.probeError": "Could not run the test: {message}",
3181
4749
  "row.cheer": "Star on GitHub",
4750
+ "row.oauthTitle": "Add an account by QR sign-in",
4751
+ "row.oauthStart": "Sign in with QR code",
4752
+ "row.oauthStarting": "Opening…",
4753
+ "row.oauthOpenHint": "Open the sign-in page and confirm — the account is added here automatically when you finish:",
4754
+ "row.oauthOpen": "Open sign-in page",
4755
+ "row.oauthOpened": "Opened. Waiting for you to finish…",
4756
+ "row.oauthWaiting": "Waiting for the sign-in to finish… ({seconds}s)",
4757
+ "row.oauthExpires": "The sign-in window expires in about {minutes} min.",
4758
+ "row.oauthCancel": "Cancel",
4759
+ "row.oauthCancelled": "Sign-in cancelled.",
4760
+ "row.oauthTimedOut": "The sign-in timed out — start again when ready.",
4761
+ "row.oauthDone": "Added “{accountName}”. Pick it above to use it.",
4762
+ "row.oauthError": "Sign-in failed: {message}",
4763
+ "row.oauthUnavailable": "QR sign-in is not offered by this build of the plugin.",
4764
+ "row.transferTitle": "Export / import accounts",
4765
+ "row.transferHint": "Move accounts between computers as a JSON file. The file carries the credentials — keep it private. The format is shared with other WorkBuddy credential managers.",
4766
+ "row.transferExport": "Export accounts",
4767
+ "row.transferExporting": "Exporting…",
4768
+ "row.transferExportPick": "Choose the accounts to export ({total} available):",
4769
+ "row.transferExportConfirm": "Export {count} account(s)",
4770
+ "row.transferExported": "Exported {count} account(s) to {fileName}.",
4771
+ "row.transferImport": "Import accounts",
4772
+ "row.transferImporting": "Importing…",
4773
+ "row.transferImportPick": "Choose a JSON file exported from this or a compatible tool:",
4774
+ "row.transferPreviewTotal": "{total} account(s) in the file — untick what should stay out. Accounts already stored here are overwritten by the import.",
4775
+ "row.transferImportConfirm": "Import {count} account(s)",
4776
+ "row.transferImported": "Imported {imported} account(s); {skipped} skipped (no token, or belongs to the other region).",
4777
+ "row.transferEntryUnknown": "unknown account",
4778
+ "row.transferNoToken": "no token in this record",
4779
+ "row.transferCancel": "Cancel",
4780
+ "row.transferError": "Transfer failed: {message}",
3182
4781
  "row.poolTitle": "Account pool",
3183
4782
  "row.poolNoCandidate": "No account in this pool can be used right now.",
3184
4783
  "row.poolNoCandidateHint": "Members are rate-limited, out of credits, rejected, or unreachable. Rate limits, drained quotas and transient failures return to the pool on their own — at the time the upstream states, or after a 30-minute fallback. Only a rejected credential needs a new sign-in.",
@@ -3276,7 +4875,38 @@ window.__ModuleLoader__.load({
3276
4875
  "row.poolCurrentBadge": "In use",
3277
4876
  "row.poolCurrentHeader": "In use: {account}",
3278
4877
  "row.poolNotMember": "Not in pool",
3279
- "row.poolUnsavedMembers": "Selection changed — save to apply"
4878
+ "row.poolUnsavedMembers": "Selection changed — save to apply",
4879
+ "row.poolRowTest": "Test",
4880
+ "row.poolRowRemove": "Remove",
4881
+ "row.poolRowRemoveHint": "Forget this account's plugin-stored credential. A desktop-app sign-in reappears on the next scan — only the plugin's own copy is deleted.",
4882
+ "row.poolRowRemoveAria": "Remove the stored credential of {accountName}",
4883
+ "row.poolRowRemoved": "{accountName}: removed from the plugin's storage.",
4884
+ "row.poolRowRemoveDesktopOnly": "{accountName}: nothing plugin-stored to remove — this account comes from the desktop app's own sign-in.",
4885
+ "row.showCreditsCn": "Show WB CN credits in the composer",
4886
+ "row.showCreditsGlobal": "Show WB AI credits in the composer",
4887
+ "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).",
4888
+ "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).",
4889
+ "composer.pointsCn": "WB CN",
4890
+ "composer.pointsGlobal": "WB AI",
4891
+ "composer.panelTitleCn": "WB CN credits",
4892
+ "composer.panelTitleGlobal": "WB AI credits",
4893
+ "composer.account": "Account",
4894
+ "composer.credits": "Credits",
4895
+ "composer.switchTo": "Switch to {account}",
4896
+ "composer.noAccounts": "No account in this region",
4897
+ "composer.signedOut": "Not signed in",
4898
+ "composer.refresh": "Refresh credits",
4899
+ "composer.refreshing": "Refreshing credits…",
4900
+ "composer.refreshFailed": "Could not refresh credits",
4901
+ "composer.lastRefresh": "Last refreshed",
4902
+ "composer.panelTitle": "WorkBuddy credits",
4903
+ "sidebar.creditsCn": "WB CN credits",
4904
+ "sidebar.creditsGlobal": "WB AI credits",
4905
+ "sidebar.refresh": "Refresh",
4906
+ "sidebar.refreshing": "Refreshing…",
4907
+ "sidebar.lastRefresh": "Last refreshed {time}",
4908
+ "sidebar.refreshFailed": "Refresh failed",
4909
+ "sidebar.signedOut": "Not signed in"
3280
4910
  };
3281
4911
  const zh = {
3282
4912
  "row.title": "接入使用 WorkBuddy 积分与模型(dsh-connect-workbuddy)",
@@ -3369,6 +4999,37 @@ window.__ModuleLoader__.load({
3369
4999
  "row.probeFailedStatus": "✗ 测试失败(HTTP {status})",
3370
5000
  "row.probeError": "无法执行测试:{message}",
3371
5001
  "row.cheer": "鼓励一下",
5002
+ "row.oauthTitle": "扫码添加账号",
5003
+ "row.oauthStart": "扫码登录",
5004
+ "row.oauthStarting": "正在打开…",
5005
+ "row.oauthOpenHint": "打开登录页并完成确认——完成后账号会自动出现在这里:",
5006
+ "row.oauthOpen": "打开登录页",
5007
+ "row.oauthOpened": "已打开,等待你完成登录…",
5008
+ "row.oauthWaiting": "正在等待登录完成…({seconds} 秒)",
5009
+ "row.oauthExpires": "登录窗口约 {minutes} 分钟后过期。",
5010
+ "row.oauthCancel": "取消",
5011
+ "row.oauthCancelled": "已取消登录。",
5012
+ "row.oauthTimedOut": "登录超时——请重新开始。",
5013
+ "row.oauthDone": "已添加「{accountName}」。在上方选中它即可使用。",
5014
+ "row.oauthError": "登录失败:{message}",
5015
+ "row.oauthUnavailable": "当前版本的插件不支持扫码登录。",
5016
+ "row.transferTitle": "导出 / 导入账号",
5017
+ "row.transferHint": "用 JSON 文件在电脑之间迁移账号。文件里是账号凭据——请妥善保管。该格式与同类 WorkBuddy 凭据管理工具通用。",
5018
+ "row.transferExport": "导出账号",
5019
+ "row.transferExporting": "正在导出…",
5020
+ "row.transferExportPick": "选择要导出的账号(共 {total} 个):",
5021
+ "row.transferExportConfirm": "导出 {count} 个账号",
5022
+ "row.transferExported": "已导出 {count} 个账号到 {fileName}。",
5023
+ "row.transferImport": "导入账号",
5024
+ "row.transferImporting": "正在导入…",
5025
+ "row.transferImportPick": "选择本插件或同类工具导出的 JSON 文件:",
5026
+ "row.transferPreviewTotal": "文件中共 {total} 个账号——不需要的请取消勾选。导入会覆盖这里已存储的同名账号。",
5027
+ "row.transferImportConfirm": "导入 {count} 个账号",
5028
+ "row.transferImported": "已导入 {imported} 个账号;跳过 {skipped} 个(缺 token,或属于另一个区域)。",
5029
+ "row.transferEntryUnknown": "未知账号",
5030
+ "row.transferNoToken": "此记录没有 token",
5031
+ "row.transferCancel": "取消",
5032
+ "row.transferError": "传输失败:{message}",
3372
5033
  "row.poolTitle": "账号池",
3373
5034
  "row.poolNoCandidate": "账号池里目前没有可用的账号。",
3374
5035
  "row.poolNoCandidateHint": "成员要么在限流冷却中、要么积分耗尽、要么凭据被拒、要么上游无响应。限流、积分耗尽与瞬时故障都会自动回池——按上游给出的恢复时间,没给时间就按 30 分钟兜底,不需要你操作;只有「凭据被拒」需要重新登录。",
@@ -3466,7 +5127,38 @@ window.__ModuleLoader__.load({
3466
5127
  "row.poolCurrentBadge": "当前使用",
3467
5128
  "row.poolCurrentHeader": "当前使用:{account}",
3468
5129
  "row.poolNotMember": "未入池",
3469
- "row.poolUnsavedMembers": "勾选已改动 —— 保存后生效"
5130
+ "row.poolUnsavedMembers": "勾选已改动 —— 保存后生效",
5131
+ "row.poolRowTest": "测试",
5132
+ "row.poolRowRemove": "删除",
5133
+ "row.poolRowRemoveHint": "删除插件保存的这个账号凭据。桌面 App 的登录不会被删除——下次扫描它还会出现;只有插件自己的副本会被移除。",
5134
+ "row.poolRowRemoveAria": "删除 {accountName} 的已存凭据",
5135
+ "row.poolRowRemoved": "{accountName}:已从插件存储中移除。",
5136
+ "row.poolRowRemoveDesktopOnly": "{accountName}:没有插件存储的凭据可删——这个账号来自桌面 App 自己的登录。",
5137
+ "row.showCreditsCn": "在对话框显示 WB CN 积分",
5138
+ "row.showCreditsGlobal": "在对话框显示 WB AI 积分",
5139
+ "row.showCreditsCnHint": "在对话输入框工具栏显示国内版(WB CN)剩余积分,仅当本会话使用 WB CN 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
5140
+ "row.showCreditsGlobalHint": "在对话输入框工具栏显示国际版(WB AI)剩余积分,仅当本会话使用 WB AI 模型时出现。点击可看各账号余额并手动刷新(每 5 分钟也会自动刷新)。",
5141
+ "composer.pointsCn": "WB CN",
5142
+ "composer.pointsGlobal": "WB AI",
5143
+ "composer.panelTitleCn": "WB CN 积分",
5144
+ "composer.panelTitleGlobal": "WB AI 积分",
5145
+ "composer.account": "账号",
5146
+ "composer.credits": "积分",
5147
+ "composer.switchTo": "切换到 {account}",
5148
+ "composer.noAccounts": "该区域暂无账号",
5149
+ "composer.signedOut": "未登录",
5150
+ "composer.refresh": "刷新积分",
5151
+ "composer.refreshing": "正在刷新积分…",
5152
+ "composer.refreshFailed": "积分刷新失败",
5153
+ "composer.lastRefresh": "最近刷新",
5154
+ "composer.panelTitle": "WorkBuddy 积分",
5155
+ "sidebar.creditsCn": "WB CN 积分",
5156
+ "sidebar.creditsGlobal": "WB AI 积分",
5157
+ "sidebar.refresh": "刷新",
5158
+ "sidebar.refreshing": "刷新中…",
5159
+ "sidebar.lastRefresh": "最近刷新 {time}",
5160
+ "sidebar.refreshFailed": "刷新失败",
5161
+ "sidebar.signedOut": "未登录"
3470
5162
  };
3471
5163
  //#endregion
3472
5164
  //#region src/client/index.tsx
@@ -3543,6 +5235,41 @@ window.__ModuleLoader__.load({
3543
5235
  };
3544
5236
  registerCard("plugins.bundle.config", "dsh-connect-workbuddy");
3545
5237
  registerCard("plugins.row.config", "dsh-connect-workbuddy#dsh-connect-workbuddy");
5238
+ /**
5239
+ * Credit readout in the composer tool row (`conversation.input.left`, a
5240
+ * session-scope list slot).
5241
+ *
5242
+ * 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
5243
+ * — 该项目的 `registerComposerPoints`:无条件注册插槽、由 gate 读
5244
+ * `modelSelection` 投影自行决定是否渲染。本项目原先放在侧边栏底部
5245
+ * (`sidebar.footer.action`),那里是**单个 root 级共享行**,两个连接器
5246
+ * 插件各占一行会互相挤压;输入框这行是 per-session 且按 provider 归属,
5247
+ * 天然只有一个插件的积分相关。
5248
+ *
5249
+ * Provider-scoped on purpose: it renders only while the session's selected
5250
+ * model belongs to this plugin, which is what keeps two connector plugins
5251
+ * from competing for one shared row. Registration is unconditional; the
5252
+ * gate reads the Host's `modelSelection` projection and returns null for
5253
+ * every other provider, so nothing is drawn and no fetch loop starts while
5254
+ * another provider is selected.
5255
+ */
5256
+ const registerComposerCredits = () => {
5257
+ try {
5258
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
5259
+ name: "conversation.input.left",
5260
+ id: "dsh-connect-workbuddy-composer-credits",
5261
+ order: 4,
5262
+ locale: namespace,
5263
+ inject: () => settingsScope === void 0 ? { t } : {
5264
+ t,
5265
+ settingsScope
5266
+ }
5267
+ }, ComposerCreditsGate));
5268
+ } catch (error) {
5269
+ console.error("[dsh-connect-workbuddy] composer credits slot failed to register (host provider unaffected):", error);
5270
+ }
5271
+ };
5272
+ registerComposerCredits();
3546
5273
  } catch (error) {
3547
5274
  console.error("[dsh-connect-workbuddy] client card failed to load (host provider unaffected):", error);
3548
5275
  }