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/CHANGELOG.md +78 -0
- package/README.en.md +4 -0
- package/README.md +4 -0
- package/docs/assets/dsh-connect-workbuddy-composer-credits.png +0 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +1754 -27
- package/lib/{host-heartbeat-Cg2ahl1n.js → host-heartbeat-C4TXu8ew.js} +367 -25
- package/lib/index.d.ts +263 -2
- package/lib/index.js +895 -10
- package/package.json +1 -1
- package/screenshots.json +2 -1
package/lib/client.js
CHANGED
|
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react_dom = require("react-dom");
|
|
9
10
|
//#region src/status-paths.ts
|
|
10
11
|
/**
|
|
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
|
}
|