dsh-workbuddy-bridge 0.2.0 → 0.3.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/README.md +55 -54
- package/README_en.md +61 -63
- package/lib/bin.js +1 -1
- package/lib/client.js +671 -133
- package/lib/index.d.ts +175 -1
- package/lib/index.js +581 -86
- package/lib/{variants-DP0Ho0Tc.js → variants-3SmUZ0e_.js} +40 -18
- package/locale/README.md +10 -0
- package/package.json +40 -28
package/lib/client.js
CHANGED
|
@@ -45,15 +45,34 @@ window.__ModuleLoader__.load({
|
|
|
45
45
|
function formatPercent(percent) {
|
|
46
46
|
return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A credit amount, at the precision the ledger actually carries.
|
|
50
|
+
*
|
|
51
|
+
* Credits are recorded to two decimals — the desktop app accumulates stage
|
|
52
|
+
* increments with `Math.round(value * 100) / 100` — so trailing zeros carry no
|
|
53
|
+
* information: `0.50` is `0.5` and `3.00` is `3`. This mirrors that app's own
|
|
54
|
+
* `formatCredit` (integers bare, otherwise two decimals with trailing zeros
|
|
55
|
+
* stripped), so a figure read here matches the one the WorkBuddy client shows
|
|
56
|
+
* for the same request.
|
|
57
|
+
*
|
|
58
|
+
* Deliberately *not* `Intl`-formatted: this is a ledger quantity at a fixed
|
|
59
|
+
* scale, not a locale-sensitive count, and a thousands separator on a balance
|
|
60
|
+
* like `5143` would make it differ from the account's own display.
|
|
61
|
+
*/
|
|
62
|
+
function formatCredit(credit) {
|
|
63
|
+
if (!Number.isFinite(credit)) return "";
|
|
64
|
+
if (Number.isInteger(credit)) return String(credit);
|
|
65
|
+
return credit.toFixed(2).replace(/\.?0+$/u, "");
|
|
66
|
+
}
|
|
48
67
|
//#endregion
|
|
49
68
|
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/workbuddy.module.css.js
|
|
50
|
-
const css$
|
|
51
|
-
const tagId$
|
|
52
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
69
|
+
const css$3 = "._8BbhmW_page,._8BbhmW_cards{flex-direction:column;gap:12px;display:flex}._8BbhmW_toggleRow{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:flex-start;gap:16px;padding:12px 0;font-size:13px;line-height:1.5;display:flex}._8BbhmW_toggleLabel{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}._8BbhmW_toggleHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}._8BbhmW_body{flex-direction:column;padding:4px 0 12px;display:flex}._8BbhmW_section{flex-direction:column;gap:6px;margin:12px 0;display:flex}._8BbhmW_title{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:500;line-height:1.5}._8BbhmW_row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;padding:12px 0;display:flex}._8BbhmW_summaryRow{flex-wrap:wrap;gap:4px;height:auto;min-height:28px}._8BbhmW_summaryTitle{flex:1;min-width:0}._8BbhmW_statusLine{color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;font-size:12px;line-height:1.5;display:inline-flex}._8BbhmW_text{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.5}._8BbhmW_dim{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}._8BbhmW_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:1.5}._8BbhmW_package{flex-direction:column;gap:8px;display:flex}._8BbhmW_packageLabel{color:var(--dsw-alias-label-secondary);justify-content:space-between;gap:8px;font-size:13px;line-height:1.5;display:flex}._8BbhmW_track{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;height:8px;overflow:hidden}._8BbhmW_fill{border-radius:inherit;background:var(--dsw-alias-brand-primary);height:100%}._8BbhmW_list{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;gap:6px;min-width:0;max-height:280px;margin:0;padding:14px;display:grid;overflow:auto}._8BbhmW_modelRow{justify-content:space-between;align-items:flex-start;gap:8px;display:flex}._8BbhmW_modelMain{align-items:center;gap:8px;min-width:0;display:flex}._8BbhmW_modelStack{flex-direction:column;gap:2px;min-width:0;display:flex}._8BbhmW_name{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}._8BbhmW_modelEnd{text-align:right;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-direction:column;gap:2px;font-size:12px;line-height:1.5;display:flex}._8BbhmW_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}._8BbhmW_badges{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._8BbhmW_sectionActions{justify-content:flex-end;display:flex}._8BbhmW_probeRow{justify-content:space-between;align-items:center;gap:8px;display:flex}._8BbhmW_probeEnd{flex:none;align-items:center;gap:8px;display:inline-flex}._8BbhmW_assist{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-module-platform);flex-direction:column;gap:8px;padding:12px;display:flex}._8BbhmW_prompt{min-width:0;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;flex:220px;margin:0;font-size:12px;line-height:1.6}._8BbhmW_promptRow{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-3);flex-wrap:wrap;align-items:flex-start;gap:8px;padding:8px 9px 8px 11px;display:flex}";
|
|
70
|
+
const tagId$3 = "dsh-workbuddy-bridge/workbuddy.module.css";
|
|
71
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
53
72
|
const tag = document.createElement("style");
|
|
54
73
|
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
55
|
-
tag.dataset.pluginCss = tagId$
|
|
56
|
-
tag.textContent = css$
|
|
74
|
+
tag.dataset.pluginCss = tagId$3;
|
|
75
|
+
tag.textContent = css$3;
|
|
57
76
|
document.head.appendChild(tag);
|
|
58
77
|
}
|
|
59
78
|
var workbuddy_module_css_default = {
|
|
@@ -320,16 +339,24 @@ window.__ModuleLoader__.load({
|
|
|
320
339
|
});
|
|
321
340
|
}
|
|
322
341
|
/**
|
|
323
|
-
* Reasoning
|
|
342
|
+
* Reasoning levels: one row per reasoning model, and what its switch does.
|
|
324
343
|
*
|
|
325
|
-
*
|
|
326
|
-
* - **
|
|
344
|
+
* Three deliberate UX rules:
|
|
345
|
+
* - **The list covers every reasoning model, not just the detectable ones.**
|
|
346
|
+
* Showing only the detectable rows was what made the panel unable to answer
|
|
347
|
+
* "why does this model have no levels?" — the models whose answer was already
|
|
348
|
+
* known were exactly the ones missing from the list;
|
|
349
|
+
* - **one press detects.** The row states the current answer and the button
|
|
327
350
|
* beside it runs the check; the cost is stated once, above the list, instead
|
|
328
351
|
* of being asked again per row. A confirmation whose only other option is
|
|
329
352
|
* "cancel" costs a click and answers nothing;
|
|
330
|
-
* - a
|
|
331
|
-
*
|
|
332
|
-
*
|
|
353
|
+
* - **a settled row says so.** A declared set cannot be improved by detecting,
|
|
354
|
+
* so its button names that instead of offering a run that would spend credit
|
|
355
|
+
* to learn nothing.
|
|
356
|
+
*
|
|
357
|
+
* A `non-validating` result is presented as an observation about the parameter
|
|
358
|
+
* ("this model does not check it"), never as a statement that a level is
|
|
359
|
+
* unsupported.
|
|
333
360
|
*
|
|
334
361
|
* The tab bar owns the heading, so this panel renders no heading of its own.
|
|
335
362
|
*/
|
|
@@ -350,10 +377,11 @@ window.__ModuleLoader__.load({
|
|
|
350
377
|
busy,
|
|
351
378
|
probe.running
|
|
352
379
|
]);
|
|
353
|
-
if (probe.
|
|
380
|
+
if (probe.models.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
354
381
|
className: workbuddy_module_css_default.text,
|
|
355
382
|
children: t("probeResultEmpty")
|
|
356
383
|
});
|
|
384
|
+
const detected = probe.models.filter((entry) => entry.probedAt !== void 0);
|
|
357
385
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
358
386
|
className: workbuddy_module_css_default.section,
|
|
359
387
|
children: [
|
|
@@ -363,9 +391,10 @@ window.__ModuleLoader__.load({
|
|
|
363
391
|
}),
|
|
364
392
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
365
393
|
className: workbuddy_module_css_default.list,
|
|
366
|
-
children: probe.
|
|
367
|
-
const
|
|
368
|
-
const name = models?.find((model) => model.id === id)?.name ??
|
|
394
|
+
children: probe.models.map((entry) => {
|
|
395
|
+
const id = entry.id;
|
|
396
|
+
const name = models?.find((model) => model.id === id)?.name ?? entry.name ?? id;
|
|
397
|
+
const hasLevels = entry.efforts.length > 0;
|
|
369
398
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
370
399
|
className: workbuddy_module_css_default.modelStack,
|
|
371
400
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -375,27 +404,27 @@ window.__ModuleLoader__.load({
|
|
|
375
404
|
children: name
|
|
376
405
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
377
406
|
className: workbuddy_module_css_default.probeEnd,
|
|
378
|
-
children: [
|
|
379
|
-
tone:
|
|
380
|
-
children:
|
|
407
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
408
|
+
tone: hasLevels ? "success" : "neutral",
|
|
409
|
+
children: hasLevels ? entry.efforts.join(" / ") : t(entry.detectable ? entry.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown" : "probeResultDeclaredNone")
|
|
381
410
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
382
411
|
size: "sm",
|
|
383
|
-
disabled: probe.running ||
|
|
412
|
+
disabled: busy || probe.running || !entry.detectable,
|
|
384
413
|
onClick: () => {
|
|
385
414
|
setRunningModel(id);
|
|
386
415
|
onDetect(id);
|
|
387
416
|
},
|
|
388
|
-
children: runningModel === id ? t("probeRunning", { model: name }) : t(
|
|
417
|
+
children: runningModel === id && entry.detectable ? t("probeRunning", { model: name }) : !entry.detectable ? t("probeActionDeclared") : t(entry.probedAt === void 0 ? "probeStart" : "probeRedetect")
|
|
389
418
|
})]
|
|
390
419
|
})]
|
|
391
|
-
}),
|
|
420
|
+
}), entry.probedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
392
421
|
className: workbuddy_module_css_default.meta,
|
|
393
|
-
children: t("probeResultAt", { time: formatTime(
|
|
422
|
+
children: t("probeResultAt", { time: formatTime(entry.probedAt) })
|
|
394
423
|
})]
|
|
395
424
|
}, id);
|
|
396
425
|
})
|
|
397
426
|
}),
|
|
398
|
-
|
|
427
|
+
detected.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
399
428
|
className: workbuddy_module_css_default.sectionActions,
|
|
400
429
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
401
430
|
size: "sm",
|
|
@@ -495,6 +524,29 @@ window.__ModuleLoader__.load({
|
|
|
495
524
|
if (status === "signed-out" || status === "signed-in") return true;
|
|
496
525
|
return status === "error" && typeof wrapped["message"] === "string";
|
|
497
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Whether a parsed credit response really is a credit document.
|
|
529
|
+
*
|
|
530
|
+
* Same reasoning as {@link isWorkBuddyWebStatus}: a 200 promises nothing about
|
|
531
|
+
* the body. Stricter about the payload than the status check because this one
|
|
532
|
+
* *is* the data — every entry is read and formatted — so a map holding a string
|
|
533
|
+
* or a `NaN` where a number belongs would reach the label. Entries that are not
|
|
534
|
+
* finite non-negative numbers are dropped rather than rejecting the whole
|
|
535
|
+
* document: one malformed row must not blank every other message's label.
|
|
536
|
+
*/
|
|
537
|
+
function isWorkBuddySessionCredits(value) {
|
|
538
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
539
|
+
const wrapped = value;
|
|
540
|
+
if (typeof wrapped["sessionId"] !== "string") return false;
|
|
541
|
+
const credits = wrapped["credits"];
|
|
542
|
+
if (typeof credits !== "object" || credits === null || Array.isArray(credits)) return false;
|
|
543
|
+
const record = credits;
|
|
544
|
+
for (const key of Object.keys(record)) {
|
|
545
|
+
const credit = record[key];
|
|
546
|
+
if (typeof credit !== "number" || !Number.isFinite(credit) || credit < 0) delete record[key];
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
498
550
|
//#endregion
|
|
499
551
|
//#region src/client/use-status.ts
|
|
500
552
|
/**
|
|
@@ -520,7 +572,7 @@ window.__ModuleLoader__.load({
|
|
|
520
572
|
* document rather than in place of it.
|
|
521
573
|
*/
|
|
522
574
|
/** How often the status document is re-read while a variant is signed in. */
|
|
523
|
-
const POLL_INTERVAL_MS = 6e4;
|
|
575
|
+
const POLL_INTERVAL_MS$2 = 6e4;
|
|
524
576
|
/**
|
|
525
577
|
* Own one variant's status document.
|
|
526
578
|
* @param variant - which product's routes and copy to use.
|
|
@@ -661,7 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
661
713
|
const timer = window.setInterval(() => {
|
|
662
714
|
if (signedOut.current) return;
|
|
663
715
|
read(controller.signal);
|
|
664
|
-
}, POLL_INTERVAL_MS);
|
|
716
|
+
}, POLL_INTERVAL_MS$2);
|
|
665
717
|
return () => {
|
|
666
718
|
window.clearInterval(timer);
|
|
667
719
|
controller.abort();
|
|
@@ -935,21 +987,68 @@ window.__ModuleLoader__.load({
|
|
|
935
987
|
})
|
|
936
988
|
});
|
|
937
989
|
}
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/shared/paths.ts
|
|
992
|
+
/** Node-free constants and types shared by the Host and browser halves. */
|
|
993
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
994
|
+
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/status";
|
|
995
|
+
/**
|
|
996
|
+
* Plugin-owned probe control endpoint.
|
|
997
|
+
*
|
|
998
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
999
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
1000
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
1001
|
+
* also requires the in-memory key the browser half receives with the status
|
|
1002
|
+
* document.
|
|
1003
|
+
*/
|
|
1004
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/probe";
|
|
1005
|
+
/**
|
|
1006
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
1007
|
+
*
|
|
1008
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
1009
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
1010
|
+
* independently, and a shared expression is one build-config drift away from
|
|
1011
|
+
* the desk asking a route the host never mounted.
|
|
1012
|
+
*/
|
|
1013
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/ai/status";
|
|
1014
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/ai/probe";
|
|
1015
|
+
/**
|
|
1016
|
+
* Plugin-owned per-message credit route.
|
|
1017
|
+
*
|
|
1018
|
+
* A read-only GET taking `?sessionId=…`, answering `messageId → credit` for one
|
|
1019
|
+
* Session. It is its own route rather than a field of the status document
|
|
1020
|
+
* because the two have different lifetimes: the status document is per
|
|
1021
|
+
* *account* and is re-read when the account changes, while credit accounting
|
|
1022
|
+
* is per *Session* and grows as messages are sent.
|
|
1023
|
+
*
|
|
1024
|
+
* **One route for both variants, on purpose.** The accounting is attributed to
|
|
1025
|
+
* a DSH `messageId` before it is stored, and a message id belongs to one
|
|
1026
|
+
* Session regardless of which provider served it — a conversation can switch
|
|
1027
|
+
* between the two WorkBuddy products (or away from both) mid-thread. A route
|
|
1028
|
+
* per variant would force the message row to know which provider answered a
|
|
1029
|
+
* message *it is merely labelling*, and would strand every message whose turn
|
|
1030
|
+
* ran under the other variant.
|
|
1031
|
+
*
|
|
1032
|
+
* No key is required: the route only reads what this plugin already observed on
|
|
1033
|
+
* its own loopback stream, carries no credential material, and answers loopback
|
|
1034
|
+
* requests through the same Host/Origin guard as the status route.
|
|
1035
|
+
*/
|
|
1036
|
+
const WORKBUDDY_CREDIT_PATH = "/plugins/dsh-workbuddy-bridge/credit";
|
|
938
1037
|
/** Both variants, in display order. */
|
|
939
1038
|
const CARD_VARIANTS = [{
|
|
940
1039
|
id: "workbuddy",
|
|
941
1040
|
titleKey: "title",
|
|
942
1041
|
signedOutKey: "signedOutHint",
|
|
943
|
-
statusPath:
|
|
944
|
-
probePath:
|
|
1042
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
1043
|
+
probePath: WORKBUDDY_PROBE_PATH,
|
|
945
1044
|
appName: "WorkBuddy",
|
|
946
1045
|
unavailableKey: "assistUnavailableCN"
|
|
947
1046
|
}, {
|
|
948
1047
|
id: "workbuddy-ai",
|
|
949
1048
|
titleKey: "titleAI",
|
|
950
1049
|
signedOutKey: "signedOutHintAI",
|
|
951
|
-
statusPath:
|
|
952
|
-
probePath:
|
|
1050
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
1051
|
+
probePath: WORKBUDDY_AI_PROBE_PATH,
|
|
953
1052
|
appName: "WorkBuddy AI",
|
|
954
1053
|
unavailableKey: "assistUnavailableAI"
|
|
955
1054
|
}];
|
|
@@ -998,6 +1097,11 @@ window.__ModuleLoader__.load({
|
|
|
998
1097
|
exactRemaining: "{remain} / {size} remaining",
|
|
999
1098
|
creditPackageUnknownSize: "{remain} remaining",
|
|
1000
1099
|
creditsError: "Credit unavailable: {message}",
|
|
1100
|
+
creditConsumed: "Used {credit}",
|
|
1101
|
+
creditConsumedTitle: "Credit this message consumed, as reported by WorkBuddy",
|
|
1102
|
+
creditRemainingUnlimited: "Unlimited",
|
|
1103
|
+
creditRemainingUnknown: "Credit unknown",
|
|
1104
|
+
creditBalanceTitle: "Remaining credit on your {product} account",
|
|
1001
1105
|
freeModel: "Free",
|
|
1002
1106
|
badgeLimitedFree: "Limited-time free",
|
|
1003
1107
|
badgeNightDiscount: "Night discount",
|
|
@@ -1015,21 +1119,27 @@ window.__ModuleLoader__.load({
|
|
|
1015
1119
|
probeRunning: "Detecting {model}…",
|
|
1016
1120
|
probeClear: "Clear results",
|
|
1017
1121
|
probeResultNotValidating: "This model does not check the effort parameter",
|
|
1018
|
-
probeResultUnknown: "
|
|
1122
|
+
probeResultUnknown: "Not detected yet",
|
|
1123
|
+
probeResultDeclaredNone: "The provider declares no levels",
|
|
1019
1124
|
probeResultAt: "Detected {time}",
|
|
1020
|
-
probeResultEmpty: "No models
|
|
1125
|
+
probeResultEmpty: "No reasoning models are being served right now.",
|
|
1126
|
+
probeActionDeclared: "Declared",
|
|
1021
1127
|
probeFailed: "Detection failed: {message}",
|
|
1022
1128
|
probeLabel: "Reasoning levels",
|
|
1023
1129
|
probePanelLevels: "Supported levels",
|
|
1024
|
-
|
|
1130
|
+
probePanelNoLevels: "No level can be selected",
|
|
1025
1131
|
probePanelNote: "Detection sends a few requests to this model and may consume a small amount of credit.",
|
|
1026
1132
|
probePanelDetect: "Detect",
|
|
1027
1133
|
probePanelDetecting: "Detecting…",
|
|
1028
1134
|
probePanelRedetect: "Detect again",
|
|
1135
|
+
probePanelNotNeeded: "Already declared — nothing to detect",
|
|
1029
1136
|
probePanelNotValidating: "This model does not check the effort parameter.",
|
|
1137
|
+
probePanelDeclaredNone: "This model declares no selectable levels.",
|
|
1030
1138
|
probePanelFailed: "Detection did not finish. You can run it again.",
|
|
1031
|
-
probeTooltipIdle: "
|
|
1032
|
-
|
|
1139
|
+
probeTooltipIdle: "This model offers no levels yet — click to detect them",
|
|
1140
|
+
probeTooltipRunning: "Detecting…",
|
|
1141
|
+
probeTooltipLevels: "Detected levels: {levels}",
|
|
1142
|
+
probeTooltipDeclared: "Levels declared by the provider: {levels}",
|
|
1033
1143
|
probeTooltipNotValidating: "This model ignores the reasoning-level parameter",
|
|
1034
1144
|
probeTooltipFailed: "Detection did not finish · click to run it again",
|
|
1035
1145
|
authFile: "WorkBuddy auth file",
|
|
@@ -1102,6 +1212,11 @@ window.__ModuleLoader__.load({
|
|
|
1102
1212
|
exactRemaining: "剩余 {remain} / {size}",
|
|
1103
1213
|
creditPackageUnknownSize: "剩余 {remain}",
|
|
1104
1214
|
creditsError: "积分查询失败:{message}",
|
|
1215
|
+
creditConsumed: "共消耗 {credit}",
|
|
1216
|
+
creditConsumedTitle: "本条消息消耗的积分(由 WorkBuddy 返回)",
|
|
1217
|
+
creditRemainingUnlimited: "不限额",
|
|
1218
|
+
creditRemainingUnknown: "积分未知",
|
|
1219
|
+
creditBalanceTitle: "{product} 账号剩余积分",
|
|
1105
1220
|
freeModel: "免费",
|
|
1106
1221
|
badgeLimitedFree: "限时免费",
|
|
1107
1222
|
badgeNightDiscount: "夜间折扣",
|
|
@@ -1119,21 +1234,27 @@ window.__ModuleLoader__.load({
|
|
|
1119
1234
|
probeRunning: "正在检测 {model}…",
|
|
1120
1235
|
probeClear: "清除结果",
|
|
1121
1236
|
probeResultNotValidating: "该模型不校验档位参数",
|
|
1122
|
-
probeResultUnknown: "
|
|
1237
|
+
probeResultUnknown: "尚未检测",
|
|
1238
|
+
probeResultDeclaredNone: "上游未声明档位",
|
|
1123
1239
|
probeResultAt: "检测于 {time}",
|
|
1124
|
-
probeResultEmpty: "
|
|
1240
|
+
probeResultEmpty: "当前没有提供可选档位的推理模型。",
|
|
1241
|
+
probeActionDeclared: "已声明",
|
|
1125
1242
|
probeFailed: "检测失败:{message}",
|
|
1126
1243
|
probeLabel: "推理档位",
|
|
1127
1244
|
probePanelLevels: "支持的档位",
|
|
1128
|
-
|
|
1245
|
+
probePanelNoLevels: "无可选档位",
|
|
1129
1246
|
probePanelNote: "检测会向该模型发送若干请求,可能消耗少量积分。",
|
|
1130
1247
|
probePanelDetect: "检测",
|
|
1131
1248
|
probePanelDetecting: "检测中…",
|
|
1132
1249
|
probePanelRedetect: "重新检测",
|
|
1250
|
+
probePanelNotNeeded: "上游已声明档位,无需检测",
|
|
1133
1251
|
probePanelNotValidating: "该模型不校验档位参数。",
|
|
1252
|
+
probePanelDeclaredNone: "上游未声明可选档位。",
|
|
1134
1253
|
probePanelFailed: "检测未完成,可以再检测一次。",
|
|
1135
|
-
probeTooltipIdle: "
|
|
1136
|
-
|
|
1254
|
+
probeTooltipIdle: "该模型暂无档位,点击可检测",
|
|
1255
|
+
probeTooltipRunning: "正在检测…",
|
|
1256
|
+
probeTooltipLevels: "检测出的档位:{levels}",
|
|
1257
|
+
probeTooltipDeclared: "上游声明的档位:{levels}",
|
|
1137
1258
|
probeTooltipNotValidating: "该模型忽略推理档位参数",
|
|
1138
1259
|
probeTooltipFailed: "检测未完成 · 点击可再检测一次",
|
|
1139
1260
|
authFile: "WorkBuddy 登录文件",
|
|
@@ -1279,13 +1400,13 @@ window.__ModuleLoader__.load({
|
|
|
1279
1400
|
}
|
|
1280
1401
|
//#endregion
|
|
1281
1402
|
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/probe-control.module.css.js
|
|
1282
|
-
const css = ".EVr7sG_wrapper{align-items:center;display:inline-flex;position:relative}.EVr7sG_trigger{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:999px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:relative}.EVr7sG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.EVr7sG_trigger:disabled{cursor:default;color:var(--dsw-alias-label-dimmed)}.EVr7sG_trigger:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}.EVr7sG_panel{isolation:isolate;z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(280px,100vw - 24px);max-width:min(360px,100vw - 24px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;padding:12px;font-size:12px;line-height:1.5;position:fixed}.EVr7sG_panel:before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:\"\";pointer-events:none;position:absolute;inset:0}.EVr7sG_panelTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:1.5;display:flex}.EVr7sG_panelTitleIcon{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;display:inline-flex}.EVr7sG_panelTitleText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.EVr7sG_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin:8px 0 10px}.EVr7sG_levelsLabel{color:var(--dsw-alias-label-tertiary);margin:0 0 2px;font-size:12px;line-height:1.5}.EVr7sG_levels{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:500;line-height:1.5}.EVr7sG_note,.EVr7sG_dim{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.6}.EVr7sG_dim{margin-top:2px}.EVr7sG_panelActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}";
|
|
1283
|
-
const tagId = "dsh-workbuddy-bridge/probe-control.module.css";
|
|
1284
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1403
|
+
const css$2 = ".EVr7sG_wrapper{align-items:center;display:inline-flex;position:relative}.EVr7sG_trigger{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:999px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:relative}.EVr7sG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.EVr7sG_trigger:disabled{cursor:default;color:var(--dsw-alias-label-dimmed)}.EVr7sG_trigger:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}.EVr7sG_panel{isolation:isolate;z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(280px,100vw - 24px);max-width:min(360px,100vw - 24px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;padding:12px;font-size:12px;line-height:1.5;position:fixed}.EVr7sG_panel:before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:\"\";pointer-events:none;position:absolute;inset:0}.EVr7sG_panelTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:1.5;display:flex}.EVr7sG_panelTitleIcon{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;display:inline-flex}.EVr7sG_panelTitleText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.EVr7sG_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin:8px 0 10px}.EVr7sG_levelsLabel{color:var(--dsw-alias-label-tertiary);margin:0 0 2px;font-size:12px;line-height:1.5}.EVr7sG_levels{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:500;line-height:1.5}.EVr7sG_note,.EVr7sG_dim{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.6}.EVr7sG_dim{margin-top:2px}.EVr7sG_panelActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}";
|
|
1404
|
+
const tagId$2 = "dsh-workbuddy-bridge/probe-control.module.css";
|
|
1405
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
1285
1406
|
const tag = document.createElement("style");
|
|
1286
1407
|
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
1287
|
-
tag.dataset.pluginCss = tagId;
|
|
1288
|
-
tag.textContent = css;
|
|
1408
|
+
tag.dataset.pluginCss = tagId$2;
|
|
1409
|
+
tag.textContent = css$2;
|
|
1289
1410
|
document.head.appendChild(tag);
|
|
1290
1411
|
}
|
|
1291
1412
|
var probe_control_module_css_default = {
|
|
@@ -1358,55 +1479,110 @@ window.__ModuleLoader__.load({
|
|
|
1358
1479
|
/** Gap between the control and the popover, and the viewport margin it keeps. */
|
|
1359
1480
|
const POPOVER_GAP = 8;
|
|
1360
1481
|
const POPOVER_MARGIN = 12;
|
|
1361
|
-
/**
|
|
1362
|
-
function
|
|
1482
|
+
/** One model's row out of the probe section's single list. */
|
|
1483
|
+
function effortModelFor(status, model) {
|
|
1363
1484
|
if (status.status !== "signed-in") return void 0;
|
|
1364
|
-
return status.probe?.
|
|
1485
|
+
return status.probe?.models.find((entry) => entry.id === model);
|
|
1365
1486
|
}
|
|
1366
1487
|
/**
|
|
1367
|
-
* What hovering the icon says
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1488
|
+
* What hovering the icon says.
|
|
1489
|
+
*
|
|
1490
|
+
* The bulb answers one question — *can I pick a thinking level on this model?* —
|
|
1491
|
+
* so the tooltip leads with that answer and only then explains it. Three states,
|
|
1492
|
+
* and the distinction between the last two is the whole point:
|
|
1493
|
+
*
|
|
1494
|
+
* - levels are offered: name them, and where they came from;
|
|
1495
|
+
* - nothing is offered but a detection could still find some: say so, and that
|
|
1496
|
+
* pressing is what asks;
|
|
1497
|
+
* - nothing is offered and a detection already proved there is nothing to find:
|
|
1498
|
+
* say *that*, rather than implying the question is still open. Reporting a
|
|
1499
|
+
* completed answer as "not checked yet" was the old copy's defect.
|
|
1500
|
+
*
|
|
1501
|
+
* A remembered failure is the last resort: it describes this control's own run,
|
|
1502
|
+
* while `entry` describes the model, and the model is what the user asked about.
|
|
1379
1503
|
*/
|
|
1380
|
-
function tooltipText(t,
|
|
1381
|
-
if (state.busy) return t("
|
|
1382
|
-
const
|
|
1383
|
-
if (
|
|
1384
|
-
if (
|
|
1385
|
-
if (
|
|
1504
|
+
function tooltipText(t, state) {
|
|
1505
|
+
if (state.busy) return t("probeTooltipRunning");
|
|
1506
|
+
const entry = state.entry;
|
|
1507
|
+
if (entry !== void 0) {
|
|
1508
|
+
if (entry.efforts.length > 0) return t(entry.source === "declared" ? "probeTooltipDeclared" : "probeTooltipLevels", { levels: entry.efforts.join(" / ") });
|
|
1509
|
+
if (entry.validation === "non-validating") return t("probeTooltipNotValidating");
|
|
1510
|
+
if (entry.detectable) return t("probeTooltipIdle");
|
|
1386
1511
|
return t("probeTooltipFailed");
|
|
1387
1512
|
}
|
|
1388
|
-
return state.failed ? t("probeTooltipFailed") : t("probeTooltipIdle"
|
|
1513
|
+
return state.failed ? t("probeTooltipFailed") : t("probeTooltipIdle");
|
|
1389
1514
|
}
|
|
1390
|
-
/** The levels this model
|
|
1391
|
-
function levelsLine(
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1515
|
+
/** The levels this model offers as one line; undefined when there are none. */
|
|
1516
|
+
function levelsLine(entry) {
|
|
1517
|
+
if (entry === void 0 || entry.efforts.length === 0) return void 0;
|
|
1518
|
+
return entry.efforts.join(" / ");
|
|
1394
1519
|
}
|
|
1395
|
-
/** Model-independent shell: resolves the selection, then delegates per
|
|
1520
|
+
/** Model-independent shell: resolves the selection, then delegates per variant. */
|
|
1396
1521
|
function WorkBuddyProbeControl({ directory, t }) {
|
|
1397
1522
|
const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
|
|
1398
1523
|
const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
1399
1524
|
const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
|
|
1400
|
-
const card = selection
|
|
1401
|
-
if (card === void 0 || selection
|
|
1402
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1403
|
-
model: selection.model,
|
|
1525
|
+
const card = selection == null ? void 0 : cardVariantFor(selection.provider);
|
|
1526
|
+
if (card === void 0 || selection == null) return null;
|
|
1527
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VariantProbe, {
|
|
1404
1528
|
card,
|
|
1529
|
+
model: selection.model,
|
|
1405
1530
|
t
|
|
1406
|
-
},
|
|
1531
|
+
}, card.id);
|
|
1407
1532
|
}
|
|
1408
|
-
|
|
1533
|
+
/**
|
|
1534
|
+
* One variant's status document, held above the model-keyed layer.
|
|
1535
|
+
*
|
|
1536
|
+
* Living here means a model switch is a prop update rather than a remount, so
|
|
1537
|
+
* the document read for the previous model is already in hand and the new
|
|
1538
|
+
* model's row is rendered from it on the first frame — no gap, and no request.
|
|
1539
|
+
*
|
|
1540
|
+
* The reads that *do* belong here are the ones keyed to the account: the
|
|
1541
|
+
* mount-time load, the reconcile poll (a detection run in another conversation
|
|
1542
|
+
* or in the settings card has to reach this control), and the focus refresh.
|
|
1543
|
+
*/
|
|
1544
|
+
function VariantProbe({ card, model, t }) {
|
|
1409
1545
|
const [status, setStatus] = (0, react.useState)();
|
|
1546
|
+
const mounted = (0, react.useRef)(false);
|
|
1547
|
+
/** Number of the newest read that may write; assigned when a read starts. */
|
|
1548
|
+
const readSeq = (0, react.useRef)(0);
|
|
1549
|
+
const refresh = (0, react.useCallback)(async (signal) => {
|
|
1550
|
+
const seq = ++readSeq.current;
|
|
1551
|
+
const response = await fetch(card.statusPath, {
|
|
1552
|
+
credentials: "same-origin",
|
|
1553
|
+
headers: { accept: "application/json" },
|
|
1554
|
+
...signal === void 0 ? {} : { signal }
|
|
1555
|
+
});
|
|
1556
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1557
|
+
const value = await response.json().catch(() => void 0);
|
|
1558
|
+
if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
|
|
1559
|
+
if (mounted.current && signal?.aborted !== true && seq === readSeq.current) setStatus(value);
|
|
1560
|
+
}, [card.statusPath, t]);
|
|
1561
|
+
(0, react.useEffect)(() => {
|
|
1562
|
+
mounted.current = true;
|
|
1563
|
+
const controller = new AbortController();
|
|
1564
|
+
const load = () => {
|
|
1565
|
+
refresh(controller.signal).catch(() => {});
|
|
1566
|
+
};
|
|
1567
|
+
load();
|
|
1568
|
+
const timer = window.setInterval(load, RECONCILE_MS);
|
|
1569
|
+
window.addEventListener("focus", load);
|
|
1570
|
+
return () => {
|
|
1571
|
+
mounted.current = false;
|
|
1572
|
+
controller.abort();
|
|
1573
|
+
window.clearInterval(timer);
|
|
1574
|
+
window.removeEventListener("focus", load);
|
|
1575
|
+
};
|
|
1576
|
+
}, [refresh]);
|
|
1577
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
|
|
1578
|
+
model,
|
|
1579
|
+
status,
|
|
1580
|
+
probePath: card.probePath,
|
|
1581
|
+
refresh,
|
|
1582
|
+
t
|
|
1583
|
+
}, model);
|
|
1584
|
+
}
|
|
1585
|
+
function ModelProbe({ model, status, probePath, refresh, t }) {
|
|
1410
1586
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1411
1587
|
/**
|
|
1412
1588
|
* Whether the panel is open.
|
|
@@ -1422,12 +1598,11 @@ window.__ModuleLoader__.load({
|
|
|
1422
1598
|
* The outcome of the run this control just performed.
|
|
1423
1599
|
*
|
|
1424
1600
|
* Kept so a fresh answer is shown immediately, without waiting for the next
|
|
1425
|
-
* status read;
|
|
1601
|
+
* status read; the document's own row is what stands when there is none.
|
|
1426
1602
|
*/
|
|
1427
1603
|
const [fresh, setFresh] = (0, react.useState)();
|
|
1428
1604
|
const inFlight = (0, react.useRef)(false);
|
|
1429
1605
|
const mounted = (0, react.useRef)(false);
|
|
1430
|
-
const readSeq = (0, react.useRef)(0);
|
|
1431
1606
|
/** The control itself: the popover's anchor and the "inside" test for dismissal. */
|
|
1432
1607
|
const rootRef = (0, react.useRef)(null);
|
|
1433
1608
|
/**
|
|
@@ -1436,41 +1611,29 @@ window.__ModuleLoader__.load({
|
|
|
1436
1611
|
* which would otherwise read a click on the panel as a click outside it.
|
|
1437
1612
|
*/
|
|
1438
1613
|
const panelRef = (0, react.useRef)(null);
|
|
1439
|
-
const refresh = (0, react.useCallback)(async (signal) => {
|
|
1440
|
-
const seq = ++readSeq.current;
|
|
1441
|
-
const response = await fetch(card.statusPath, {
|
|
1442
|
-
credentials: "same-origin",
|
|
1443
|
-
headers: { accept: "application/json" },
|
|
1444
|
-
...signal === void 0 ? {} : { signal }
|
|
1445
|
-
});
|
|
1446
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1447
|
-
const value = await response.json().catch(() => void 0);
|
|
1448
|
-
if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
|
|
1449
|
-
if (mounted.current && signal?.aborted !== true && seq === readSeq.current) setStatus(value);
|
|
1450
|
-
}, [card.statusPath, t]);
|
|
1451
1614
|
(0, react.useEffect)(() => {
|
|
1452
1615
|
mounted.current = true;
|
|
1453
|
-
const controller = new AbortController();
|
|
1454
|
-
const load = () => {
|
|
1455
|
-
refresh(controller.signal).catch(() => {});
|
|
1456
|
-
};
|
|
1457
|
-
load();
|
|
1458
|
-
const timer = window.setInterval(load, RECONCILE_MS);
|
|
1459
|
-
window.addEventListener("focus", load);
|
|
1460
1616
|
return () => {
|
|
1461
1617
|
mounted.current = false;
|
|
1462
|
-
controller.abort();
|
|
1463
|
-
window.clearInterval(timer);
|
|
1464
|
-
window.removeEventListener("focus", load);
|
|
1465
1618
|
};
|
|
1466
|
-
}, [
|
|
1619
|
+
}, []);
|
|
1467
1620
|
const probe = status?.status === "signed-in" ? status.probe : void 0;
|
|
1468
1621
|
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1622
|
+
const entry = status === void 0 ? void 0 : effortModelFor(status, model);
|
|
1623
|
+
/**
|
|
1624
|
+
* The bulb is shown for every reasoning model the host reports, and hidden
|
|
1625
|
+
* only where the host says there is nothing to say.
|
|
1626
|
+
*
|
|
1627
|
+
* The old gate was `candidates.includes(model) || hasRecord`, i.e. "this model
|
|
1628
|
+
* could be probed, or has been". That is a statement about *detection*, and it
|
|
1629
|
+
* left the bulb off exactly the models whose switch already worked — a
|
|
1630
|
+
* declared set made a model ineligible, so the best-configured models were the
|
|
1631
|
+
* ones with no control beside them, and the user read that as a bug.
|
|
1632
|
+
*/
|
|
1633
|
+
const visible = entry !== void 0;
|
|
1471
1634
|
(0, react.useEffect)(() => {
|
|
1472
|
-
if (
|
|
1473
|
-
}, [
|
|
1635
|
+
if (entry?.validation !== void 0) setFailed(false);
|
|
1636
|
+
}, [entry?.validation]);
|
|
1474
1637
|
(0, react.useEffect)(() => {
|
|
1475
1638
|
setOpen(false);
|
|
1476
1639
|
setFresh(void 0);
|
|
@@ -1482,7 +1645,7 @@ window.__ModuleLoader__.load({
|
|
|
1482
1645
|
setBusy(true);
|
|
1483
1646
|
setFailed(false);
|
|
1484
1647
|
try {
|
|
1485
|
-
const response = await fetch(
|
|
1648
|
+
const response = await fetch(probePath, {
|
|
1486
1649
|
method: "POST",
|
|
1487
1650
|
credentials: "same-origin",
|
|
1488
1651
|
headers: {
|
|
@@ -1495,14 +1658,20 @@ window.__ModuleLoader__.load({
|
|
|
1495
1658
|
})
|
|
1496
1659
|
});
|
|
1497
1660
|
const body = await response.json();
|
|
1498
|
-
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1661
|
+
const validation = body.validation;
|
|
1662
|
+
if (!response.ok || body.state !== "ok" || validation !== "validating" && validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
|
|
1663
|
+
if (mounted.current) {
|
|
1664
|
+
const validating = validation === "validating";
|
|
1665
|
+
setFresh({
|
|
1666
|
+
id: model,
|
|
1667
|
+
name: model,
|
|
1668
|
+
efforts: validating ? body.efforts : [],
|
|
1669
|
+
source: validating ? "observed" : "none",
|
|
1670
|
+
detectable: true,
|
|
1671
|
+
validation,
|
|
1672
|
+
probedAt: Date.now()
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1506
1675
|
refresh().catch(() => {});
|
|
1507
1676
|
} catch {
|
|
1508
1677
|
if (mounted.current) setFailed(true);
|
|
@@ -1523,16 +1692,18 @@ window.__ModuleLoader__.load({
|
|
|
1523
1692
|
setOpen(false);
|
|
1524
1693
|
}, panelRef);
|
|
1525
1694
|
if (!visible) return null;
|
|
1526
|
-
const text = tooltipText(t,
|
|
1695
|
+
const text = tooltipText(t, {
|
|
1527
1696
|
busy,
|
|
1528
|
-
|
|
1697
|
+
entry,
|
|
1529
1698
|
failed
|
|
1530
1699
|
});
|
|
1531
|
-
const
|
|
1532
|
-
const shown = fresh ?? result;
|
|
1700
|
+
const shown = fresh ?? entry;
|
|
1533
1701
|
const levels = levelsLine(shown);
|
|
1534
1702
|
const notValidating = shown?.validation === "non-validating";
|
|
1535
|
-
const lit =
|
|
1703
|
+
const lit = levels !== void 0;
|
|
1704
|
+
const detectable = shown?.detectable === true;
|
|
1705
|
+
const triggerDisabled = busy || probe?.running === true || key === void 0;
|
|
1706
|
+
const actionDisabled = !detectable || triggerDisabled;
|
|
1536
1707
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1537
1708
|
className: probe_control_module_css_default.wrapper,
|
|
1538
1709
|
ref: rootRef,
|
|
@@ -1547,7 +1718,7 @@ window.__ModuleLoader__.load({
|
|
|
1547
1718
|
"aria-label": text,
|
|
1548
1719
|
"aria-busy": busy,
|
|
1549
1720
|
"aria-expanded": open,
|
|
1550
|
-
disabled,
|
|
1721
|
+
disabled: triggerDisabled,
|
|
1551
1722
|
onClick: () => {
|
|
1552
1723
|
setOpen(!open);
|
|
1553
1724
|
},
|
|
@@ -1582,17 +1753,21 @@ window.__ModuleLoader__.load({
|
|
|
1582
1753
|
className: probe_control_module_css_default.levels,
|
|
1583
1754
|
role: "status",
|
|
1584
1755
|
"aria-live": "polite",
|
|
1585
|
-
children: levels ?? t("
|
|
1756
|
+
children: levels ?? t("probePanelNoLevels")
|
|
1586
1757
|
}),
|
|
1587
1758
|
notValidating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1588
1759
|
className: probe_control_module_css_default.dim,
|
|
1589
1760
|
children: t("probePanelNotValidating")
|
|
1590
1761
|
}) : null,
|
|
1762
|
+
!notValidating && levels === void 0 && !detectable && shown !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1763
|
+
className: probe_control_module_css_default.dim,
|
|
1764
|
+
children: t("probePanelDeclaredNone")
|
|
1765
|
+
}) : null,
|
|
1591
1766
|
failed && shown === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1592
1767
|
className: probe_control_module_css_default.dim,
|
|
1593
1768
|
children: t("probePanelFailed")
|
|
1594
1769
|
}) : null,
|
|
1595
|
-
levels === void 0 && !notValidating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1770
|
+
detectable && levels === void 0 && !notValidating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1596
1771
|
className: probe_control_module_css_default.note,
|
|
1597
1772
|
children: t("probePanelNote")
|
|
1598
1773
|
}) : null,
|
|
@@ -1601,11 +1776,11 @@ window.__ModuleLoader__.load({
|
|
|
1601
1776
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1602
1777
|
size: "sm",
|
|
1603
1778
|
variant: "primary",
|
|
1604
|
-
disabled,
|
|
1779
|
+
disabled: actionDisabled,
|
|
1605
1780
|
onClick: () => {
|
|
1606
1781
|
detect();
|
|
1607
1782
|
},
|
|
1608
|
-
children: busy ? t("probePanelDetecting") : levels === void 0 ? t("probePanelDetect") : t("probePanelRedetect")
|
|
1783
|
+
children: busy ? t("probePanelDetecting") : !detectable ? t("probePanelNotNeeded") : levels === void 0 ? t("probePanelDetect") : t("probePanelRedetect")
|
|
1609
1784
|
})
|
|
1610
1785
|
})
|
|
1611
1786
|
]
|
|
@@ -1657,6 +1832,321 @@ window.__ModuleLoader__.load({
|
|
|
1657
1832
|
});
|
|
1658
1833
|
}
|
|
1659
1834
|
//#endregion
|
|
1835
|
+
//#region src/client/use-session-credits.ts
|
|
1836
|
+
/**
|
|
1837
|
+
* One Session's per-message credit accounting, as the browser half reads it.
|
|
1838
|
+
*
|
|
1839
|
+
* The Host half observes what each response cost on the upstream stream and
|
|
1840
|
+
* attributes it to a DSH message id; this hook fetches that map and keeps it
|
|
1841
|
+
* fresh. Two surfaces share it — the composer balance label and the per-message
|
|
1842
|
+
* credit row — so the fetch policy lives here once rather than in both.
|
|
1843
|
+
*
|
|
1844
|
+
* **A failure is silent, and that is the intended degradation.** The number is
|
|
1845
|
+
* an annotation on a message that already rendered; a failed read must never
|
|
1846
|
+
* turn into an error strip over a conversation, and an unreadable map must
|
|
1847
|
+
* never be confused with "these messages were free". So a failure leaves the
|
|
1848
|
+
* last good map in place and the affected rows simply show no credit.
|
|
1849
|
+
*
|
|
1850
|
+
* @module dsh-workbuddy-bridge/client/use-session-credits
|
|
1851
|
+
*/
|
|
1852
|
+
/** How often the map is re-read while a Session is open, in milliseconds. */
|
|
1853
|
+
const POLL_INTERVAL_MS$1 = 3e4;
|
|
1854
|
+
/**
|
|
1855
|
+
* Read and poll one Session's credit map.
|
|
1856
|
+
*
|
|
1857
|
+
* @param path - the variant's credit route.
|
|
1858
|
+
* @param sessionId - the Session to read for; the hook is inert without one.
|
|
1859
|
+
* @param enabled - whether the reading surface is currently mounted. The
|
|
1860
|
+
* composer label and the message row both come and go, and this stops the poll
|
|
1861
|
+
* when neither is showing.
|
|
1862
|
+
*/
|
|
1863
|
+
function useWorkBuddySessionCredits(path, sessionId, enabled) {
|
|
1864
|
+
const [credits, setCredits] = (0, react.useState)();
|
|
1865
|
+
const mounted = (0, react.useRef)(true);
|
|
1866
|
+
/** Number of the newest read allowed to write; assigned when a read starts. */
|
|
1867
|
+
const readSeq = (0, react.useRef)(0);
|
|
1868
|
+
const read = (0, react.useCallback)(async (signal) => {
|
|
1869
|
+
if (sessionId === void 0 || sessionId === "") return;
|
|
1870
|
+
const seq = ++readSeq.current;
|
|
1871
|
+
try {
|
|
1872
|
+
const response = await fetch(`${path}?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
1873
|
+
credentials: "same-origin",
|
|
1874
|
+
headers: { accept: "application/json" },
|
|
1875
|
+
signal
|
|
1876
|
+
});
|
|
1877
|
+
if (!response.ok) return;
|
|
1878
|
+
const value = await response.json().catch(() => void 0);
|
|
1879
|
+
if (!isWorkBuddySessionCredits(value)) return;
|
|
1880
|
+
if (!mounted.current || signal.aborted || seq !== readSeq.current) return;
|
|
1881
|
+
setCredits(value.credits);
|
|
1882
|
+
} catch {}
|
|
1883
|
+
}, [path, sessionId]);
|
|
1884
|
+
(0, react.useEffect)(() => {
|
|
1885
|
+
mounted.current = true;
|
|
1886
|
+
if (!enabled || sessionId === void 0 || sessionId === "") return;
|
|
1887
|
+
const controller = new AbortController();
|
|
1888
|
+
read(controller.signal);
|
|
1889
|
+
const timer = window.setInterval(() => {
|
|
1890
|
+
read(controller.signal);
|
|
1891
|
+
}, POLL_INTERVAL_MS$1);
|
|
1892
|
+
const onFocus = () => {
|
|
1893
|
+
read(controller.signal);
|
|
1894
|
+
};
|
|
1895
|
+
window.addEventListener("focus", onFocus);
|
|
1896
|
+
return () => {
|
|
1897
|
+
mounted.current = false;
|
|
1898
|
+
controller.abort();
|
|
1899
|
+
window.clearInterval(timer);
|
|
1900
|
+
window.removeEventListener("focus", onFocus);
|
|
1901
|
+
};
|
|
1902
|
+
}, [
|
|
1903
|
+
enabled,
|
|
1904
|
+
read,
|
|
1905
|
+
sessionId
|
|
1906
|
+
]);
|
|
1907
|
+
return { credits };
|
|
1908
|
+
}
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/credit-label.module.css.js
|
|
1911
|
+
const css$1 = "._9Bx4Wa_label{white-space:nowrap;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);color:var(--dsw-alias-label-tertiary);padding-left:4px;line-height:1}";
|
|
1912
|
+
const tagId$1 = "dsh-workbuddy-bridge/credit-label.module.css";
|
|
1913
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
1914
|
+
const tag = document.createElement("style");
|
|
1915
|
+
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
1916
|
+
tag.dataset.pluginCss = tagId$1;
|
|
1917
|
+
tag.textContent = css$1;
|
|
1918
|
+
document.head.appendChild(tag);
|
|
1919
|
+
}
|
|
1920
|
+
var credit_label_module_css_default = { "label": "_9Bx4Wa_label" };
|
|
1921
|
+
//#endregion
|
|
1922
|
+
//#region src/client/credit-label.tsx
|
|
1923
|
+
/**
|
|
1924
|
+
* The per-message credit label, in the assistant message's action row.
|
|
1925
|
+
*
|
|
1926
|
+
* WorkBuddy's own client puts "共消耗 X" under a finished answer; this is the
|
|
1927
|
+
* same fact for the same reason — the cost of a message is not visible anywhere
|
|
1928
|
+
* else, and a user comparing models is asking exactly "what did that one cost
|
|
1929
|
+
* me?".
|
|
1930
|
+
*
|
|
1931
|
+
* **It belongs to the WorkBuddy models and is shown only for them.** When the
|
|
1932
|
+
* session's current selection is a model from another provider, the label
|
|
1933
|
+
* renders nothing and reads nothing: a WorkBuddy credit figure next to a
|
|
1934
|
+
* DeepSeek-account answer would describe a ledger that answer never touched.
|
|
1935
|
+
* Gating on the *current* selection (rather than on each message's own
|
|
1936
|
+
* provider) is deliberate — the user asked for exactly this, and it keeps the
|
|
1937
|
+
* row's meaning consistent with the model picker sitting in the same UI.
|
|
1938
|
+
*
|
|
1939
|
+
* **It renders nothing when there is nothing to say.** The plugin only shows a
|
|
1940
|
+
* number it actually observed on the upstream stream (see
|
|
1941
|
+
* `src/llm/credit-log.ts`), so a message predating the feature, a failed
|
|
1942
|
+
* stream, or an upstream answer that carried no cost produces an empty row
|
|
1943
|
+
* rather than a confident `0`. That choice is deliberate and load-bearing: a
|
|
1944
|
+
* fabricated zero is worse than a blank, because the user cannot tell it apart
|
|
1945
|
+
* from free.
|
|
1946
|
+
*
|
|
1947
|
+
* Styling copies the sibling `endInfo` cluster — tertiary label tone at the
|
|
1948
|
+
* secondary font size — so the label reads as part of the same row as the clock
|
|
1949
|
+
* and the usage pill rather than as something bolted on.
|
|
1950
|
+
*
|
|
1951
|
+
* @module dsh-workbuddy-bridge/client/credit-label
|
|
1952
|
+
*/
|
|
1953
|
+
/** Show what one message cost, when this session runs a WorkBuddy model. */
|
|
1954
|
+
function WorkBuddyCreditLabel({ messageId, sessionId, directory, t }) {
|
|
1955
|
+
const selection = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => directory.subscribe(listener), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory])).current;
|
|
1956
|
+
const onWorkBuddy = selection !== null && cardVariantFor(selection.provider) !== void 0;
|
|
1957
|
+
const { credits } = useWorkBuddySessionCredits(WORKBUDDY_CREDIT_PATH, sessionId, onWorkBuddy && sessionId !== "");
|
|
1958
|
+
if (!onWorkBuddy) return null;
|
|
1959
|
+
const credit = credits?.[messageId];
|
|
1960
|
+
if (credit === void 0) return null;
|
|
1961
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1962
|
+
className: credit_label_module_css_default.label,
|
|
1963
|
+
title: t("creditConsumedTitle"),
|
|
1964
|
+
children: t("creditConsumed", { credit: formatCredit(credit) })
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
//#endregion
|
|
1968
|
+
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/credit-balance.module.css.js
|
|
1969
|
+
const css = ".Rbiksq_readout{white-space:nowrap;height:28px;color:var(--dsw-alias-label-tertiary);align-items:center;gap:4px;display:inline-flex}.Rbiksq_glyph{flex:none}.Rbiksq_amount{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);font-variant-numeric:tabular-nums;line-height:1}";
|
|
1970
|
+
const tagId = "dsh-workbuddy-bridge/credit-balance.module.css";
|
|
1971
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1972
|
+
const tag = document.createElement("style");
|
|
1973
|
+
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
1974
|
+
tag.dataset.pluginCss = tagId;
|
|
1975
|
+
tag.textContent = css;
|
|
1976
|
+
document.head.appendChild(tag);
|
|
1977
|
+
}
|
|
1978
|
+
var credit_balance_module_css_default = {
|
|
1979
|
+
"amount": "Rbiksq_amount",
|
|
1980
|
+
"glyph": "Rbiksq_glyph",
|
|
1981
|
+
"readout": "Rbiksq_readout"
|
|
1982
|
+
};
|
|
1983
|
+
//#endregion
|
|
1984
|
+
//#region src/client/credit-balance.tsx
|
|
1985
|
+
/**
|
|
1986
|
+
* Remaining credit, in the composer row immediately left of the model picker.
|
|
1987
|
+
*
|
|
1988
|
+
* This is the one place the figure is worth carrying: choosing a model is the
|
|
1989
|
+
* moment the balance decides anything, and the card's own Credits tab is two
|
|
1990
|
+
* clicks away from where the choice is made. It answers "how much have I got?"
|
|
1991
|
+
* next to the control that spends it.
|
|
1992
|
+
*
|
|
1993
|
+
* **It is a readout, not a control.** A coin glyph followed by the number, in
|
|
1994
|
+
* the same 28px row as the reasoning-level bulb beside it. The words "remaining"
|
|
1995
|
+
* / "剩余" are deliberately absent: at this size a sentence competes with the
|
|
1996
|
+
* number it is labelling, and the glyph already says *what* the number is. The
|
|
1997
|
+
* full sentence survives as the `title` for anyone who hovers, and the card's
|
|
1998
|
+
* Credits tab remains the place that spells the figure out.
|
|
1999
|
+
*
|
|
2000
|
+
* **It is shown only while the session runs a WorkBuddy model.** The balance
|
|
2001
|
+
* belongs to the WorkBuddy account, so it is offered exactly where that account
|
|
2002
|
+
* is the one being spent; a session on any other provider renders nothing and
|
|
2003
|
+
* reads nothing. That keeps the composer row describing the model sitting in
|
|
2004
|
+
* it, and avoids putting a second provider's ledger next to a model it cannot
|
|
2005
|
+
* pay for.
|
|
2006
|
+
*
|
|
2007
|
+
* **It renders nothing rather than a guess.** Three states produce no label: the
|
|
2008
|
+
* selection is not a WorkBuddy model, the status read has not answered yet, and
|
|
2009
|
+
* the read failed. A blanket `0` or a `—` would be read as a balance, and being
|
|
2010
|
+
* wrong about a balance is worse than being quiet — the same rule the message
|
|
2011
|
+
* label follows.
|
|
2012
|
+
*
|
|
2013
|
+
* The seat is `conversation.input.right`, a `list` slot whose occupant becomes
|
|
2014
|
+
* the immediate flex sibling of the model seat. Registering there rather than
|
|
2015
|
+
* at `conversation.input.model` is deliberate and is pinned by
|
|
2016
|
+
* `tests/redlines.spec.ts`: that seat is `single` and already holds the shipped
|
|
2017
|
+
* `ModelSelect`, so a second registration at the same priority throws and takes
|
|
2018
|
+
* the host's own picker down with it.
|
|
2019
|
+
*
|
|
2020
|
+
* @module dsh-workbuddy-bridge/client/credit-balance
|
|
2021
|
+
*/
|
|
2022
|
+
/**
|
|
2023
|
+
* How often the balance is re-read while the composer is mounted.
|
|
2024
|
+
*
|
|
2025
|
+
* Two minutes rather than the status card's one: each read is a real billing
|
|
2026
|
+
* request upstream, the figure only moves when credit is spent, and a window
|
|
2027
|
+
* regaining focus forces a fresh read anyway.
|
|
2028
|
+
*/
|
|
2029
|
+
const POLL_INTERVAL_MS = 12e4;
|
|
2030
|
+
/** Read one variant's status route and reduce it to a balance. */
|
|
2031
|
+
async function readBalance(card, signal) {
|
|
2032
|
+
try {
|
|
2033
|
+
const response = await fetch(card.statusPath, {
|
|
2034
|
+
credentials: "same-origin",
|
|
2035
|
+
headers: { accept: "application/json" },
|
|
2036
|
+
signal
|
|
2037
|
+
});
|
|
2038
|
+
if (!response.ok) return { kind: "unknown" };
|
|
2039
|
+
const value = await response.json().catch(() => void 0);
|
|
2040
|
+
if (!isWorkBuddyWebStatus(value) || value.status !== "signed-in") return { kind: "unknown" };
|
|
2041
|
+
const credits = value.credits;
|
|
2042
|
+
if (credits === void 0) return { kind: "unknown" };
|
|
2043
|
+
return credits.unlimited === true ? { kind: "unlimited" } : {
|
|
2044
|
+
kind: "total",
|
|
2045
|
+
total: credits.total
|
|
2046
|
+
};
|
|
2047
|
+
} catch {
|
|
2048
|
+
return { kind: "unknown" };
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Show the signed-in WorkBuddy account's remaining credit beside the picker.
|
|
2053
|
+
*
|
|
2054
|
+
* The account is not per-session, so this reads the status route rather than
|
|
2055
|
+
* the credit route: the balance belongs to whoever is signed in, while the
|
|
2056
|
+
* per-message costs belong to the conversation.
|
|
2057
|
+
*/
|
|
2058
|
+
function WorkBuddyCreditBalance({ directory, t }) {
|
|
2059
|
+
const selection = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => directory.subscribe(listener), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory])).current;
|
|
2060
|
+
const card = selection === null ? void 0 : cardVariantFor(selection.provider);
|
|
2061
|
+
const [balance, setBalance] = (0, react.useState)({ kind: "unknown" });
|
|
2062
|
+
const mounted = (0, react.useRef)(true);
|
|
2063
|
+
(0, react.useEffect)(() => {
|
|
2064
|
+
mounted.current = true;
|
|
2065
|
+
if (card === void 0) {
|
|
2066
|
+
setBalance({ kind: "unknown" });
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
const controller = new AbortController();
|
|
2070
|
+
const read = async () => {
|
|
2071
|
+
const next = await readBalance(card, controller.signal);
|
|
2072
|
+
if (!mounted.current || controller.signal.aborted) return;
|
|
2073
|
+
setBalance(next);
|
|
2074
|
+
};
|
|
2075
|
+
read();
|
|
2076
|
+
const timer = window.setInterval(() => {
|
|
2077
|
+
read();
|
|
2078
|
+
}, POLL_INTERVAL_MS);
|
|
2079
|
+
const onFocus = () => {
|
|
2080
|
+
read();
|
|
2081
|
+
};
|
|
2082
|
+
window.addEventListener("focus", onFocus);
|
|
2083
|
+
return () => {
|
|
2084
|
+
mounted.current = false;
|
|
2085
|
+
controller.abort();
|
|
2086
|
+
window.clearInterval(timer);
|
|
2087
|
+
window.removeEventListener("focus", onFocus);
|
|
2088
|
+
};
|
|
2089
|
+
}, [card]);
|
|
2090
|
+
if (card === void 0 || balance.kind === "unknown") return null;
|
|
2091
|
+
const figure = balance.kind === "unlimited" ? t("creditRemainingUnlimited") : formatCredit(balance.total);
|
|
2092
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2093
|
+
className: credit_balance_module_css_default.readout,
|
|
2094
|
+
title: t("creditBalanceTitle", { product: card.appName }),
|
|
2095
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CoinGlyph, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2096
|
+
className: credit_balance_module_css_default.amount,
|
|
2097
|
+
children: figure
|
|
2098
|
+
})]
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* The credit glyph: a stack of three coins, drawn as outlines.
|
|
2103
|
+
*
|
|
2104
|
+
* This repository draws its own ink when the host has nothing to lend, and the
|
|
2105
|
+
* host's 188 icons contain no coin, credit, wallet or currency shape at all
|
|
2106
|
+
* (checked against the installed `dsh-client-ui-primitives`) — so this is
|
|
2107
|
+
* artwork of ours, exactly as the reasoning-level bulb beside it is.
|
|
2108
|
+
*
|
|
2109
|
+
* **Stroked, not filled.** A filled stack was drawn first and rejected on
|
|
2110
|
+
* sight: the bulb reads as a thin outline even though it is one filled path, so
|
|
2111
|
+
* a solid glyph beside it looked like a different icon set. The line width is
|
|
2112
|
+
* the host's own `1` on this grid, which is what makes the two read as peers.
|
|
2113
|
+
*
|
|
2114
|
+
* **Flatter than a cylinder.** The construction is the host's `database` idiom —
|
|
2115
|
+
* a full top ellipse, straight walls, and a *front-half* arc per level below —
|
|
2116
|
+
* so the ellipse's minor axis is what separates "a stack of coins" from "a
|
|
2117
|
+
* database". At `ry = 1.25` it reads as coins; at `1.6` it reads as the
|
|
2118
|
+
* database glyph with extra lines (both were rendered at 64px to check).
|
|
2119
|
+
*
|
|
2120
|
+
* Drawn on the host's own 16-unit grid with `viewBox="0 0 16 16"`, at the same
|
|
2121
|
+
* 16px as the bulb, because the two sit in one row and are read together: a
|
|
2122
|
+
* glyph at a different size would make the row look assembled from two sources.
|
|
2123
|
+
* `currentColor` throughout, so both themes come from the row's colour token —
|
|
2124
|
+
* no literal colour and no `[data-ds-dark-theme]` branch here.
|
|
2125
|
+
*/
|
|
2126
|
+
function CoinGlyph() {
|
|
2127
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2128
|
+
width: "16",
|
|
2129
|
+
height: "16",
|
|
2130
|
+
viewBox: "0 0 16 16",
|
|
2131
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2132
|
+
fill: "none",
|
|
2133
|
+
stroke: "currentColor",
|
|
2134
|
+
strokeWidth: "1",
|
|
2135
|
+
"aria-hidden": "true",
|
|
2136
|
+
className: credit_balance_module_css_default.glyph,
|
|
2137
|
+
children: [
|
|
2138
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ellipse", {
|
|
2139
|
+
cx: "8",
|
|
2140
|
+
cy: "4.7",
|
|
2141
|
+
rx: "4.7",
|
|
2142
|
+
ry: "1.25"
|
|
2143
|
+
}),
|
|
2144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.3 4.7V10.7A4.7 1.25 0 0 0 12.7 10.7V4.7" }),
|
|
2145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.3 7.7A4.7 1.25 0 0 0 12.7 7.7" })
|
|
2146
|
+
]
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
//#endregion
|
|
1660
2150
|
//#region src/client/config-controller.ts
|
|
1661
2151
|
/**
|
|
1662
2152
|
* A boolean field. Staged as text like every other field, so one save covers
|
|
@@ -1769,6 +2259,29 @@ window.__ModuleLoader__.load({
|
|
|
1769
2259
|
const PROBE_SEAT = "conversation.input.right";
|
|
1770
2260
|
/** Entry id within that seat, so the registration is named in exactly one place. */
|
|
1771
2261
|
const PROBE_SEAT_ID = "workbuddy-probe";
|
|
2262
|
+
/**
|
|
2263
|
+
* The remaining-credit label's entry id, in the same `list` seat as the probe
|
|
2264
|
+
* control.
|
|
2265
|
+
*
|
|
2266
|
+
* Sharing {@link PROBE_SEAT} rather than taking a seat of its own is what keeps
|
|
2267
|
+
* the balance flush against the model picker: `conversation.input.right` renders
|
|
2268
|
+
* as the immediate previous flex sibling of `conversation.input.model`, and two
|
|
2269
|
+
* entries in one `list` seat keep that position. Both are ordered, so the
|
|
2270
|
+
* balance can sit closer to the picker than the probe icon.
|
|
2271
|
+
*/
|
|
2272
|
+
const BALANCE_SEAT_ID = "workbuddy-credit-balance";
|
|
2273
|
+
/**
|
|
2274
|
+
* The assistant message's action strip — the row holding copy, the plugin
|
|
2275
|
+
* actions, branch and the usage pill.
|
|
2276
|
+
*
|
|
2277
|
+
* A `list` seat declared by `dsh-client-ui-chat`, scoped to the session, whose
|
|
2278
|
+
* owner currency is `{ messageId }`. The shipped Like/Dislike entry registers
|
|
2279
|
+
* into it too, which is why this is the supported place for a per-message
|
|
2280
|
+
* annotation: it is the row WorkBuddy's own client puts "共消耗" in.
|
|
2281
|
+
*/
|
|
2282
|
+
const MESSAGE_ACTIONS_SEAT = "conversation.chat.assistant-actions";
|
|
2283
|
+
/** Entry id for the per-message credit label. */
|
|
2284
|
+
const CREDIT_LABEL_SEAT_ID = "workbuddy-credit-label";
|
|
1772
2285
|
function apply(ctx) {
|
|
1773
2286
|
const t = ctx.locale.bind(NS);
|
|
1774
2287
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
@@ -1786,15 +2299,40 @@ window.__ModuleLoader__.load({
|
|
|
1786
2299
|
inject: () => config.inject()
|
|
1787
2300
|
}, WorkBuddyConfigPage))), "dsh-workbuddy-bridge: configuration page");
|
|
1788
2301
|
ctx.inject(["modelDirectories"], (scope) => {
|
|
1789
|
-
scope.slots.inject(PROBE_SEAT, () =>
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2302
|
+
scope.slots.inject(PROBE_SEAT, () => {
|
|
2303
|
+
const releaseProbe = scope.slots.register({
|
|
2304
|
+
name: PROBE_SEAT,
|
|
2305
|
+
id: PROBE_SEAT_ID,
|
|
2306
|
+
inject: (sessionId) => ({
|
|
2307
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2308
|
+
t
|
|
2309
|
+
})
|
|
2310
|
+
}, WorkBuddyProbeControl);
|
|
2311
|
+
const releaseBalance = scope.slots.register({
|
|
2312
|
+
name: PROBE_SEAT,
|
|
2313
|
+
id: BALANCE_SEAT_ID,
|
|
2314
|
+
order: -10,
|
|
2315
|
+
inject: (sessionId) => ({
|
|
2316
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2317
|
+
t
|
|
2318
|
+
})
|
|
2319
|
+
}, WorkBuddyCreditBalance);
|
|
2320
|
+
return () => {
|
|
2321
|
+
releaseBalance();
|
|
2322
|
+
releaseProbe();
|
|
2323
|
+
};
|
|
2324
|
+
});
|
|
1797
2325
|
});
|
|
2326
|
+
ctx.inject(["modelDirectories"], (scope) => scope.slots.inject(MESSAGE_ACTIONS_SEAT, () => scope.slots.register({
|
|
2327
|
+
name: MESSAGE_ACTIONS_SEAT,
|
|
2328
|
+
id: CREDIT_LABEL_SEAT_ID,
|
|
2329
|
+
locale: NS,
|
|
2330
|
+
inject: (sessionId) => ({
|
|
2331
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2332
|
+
sessionId,
|
|
2333
|
+
t
|
|
2334
|
+
})
|
|
2335
|
+
}, WorkBuddyCreditLabel)));
|
|
1798
2336
|
}
|
|
1799
2337
|
//#endregion
|
|
1800
2338
|
exports.BUNDLE_NAME = BUNDLE_NAME;
|