dsh-connect-workbuddy 5.2.3 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.en.md +82 -0
- package/README.md +82 -0
- package/lib/bin.js +1 -1
- package/lib/client.js +572 -19
- package/lib/{host-heartbeat-CgYyUwpM.js → host-heartbeat-BHESQ6Ah.js} +2 -59
- package/lib/index.d.ts +106 -22
- package/lib/index.js +1891 -20
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -530,7 +530,7 @@ window.__ModuleLoader__.load({
|
|
|
530
530
|
headers: { "Content-Type": "application/json" },
|
|
531
531
|
body: JSON.stringify({
|
|
532
532
|
field,
|
|
533
|
-
value: { [region]: value }
|
|
533
|
+
value: region === void 0 ? value : { [region]: value }
|
|
534
534
|
})
|
|
535
535
|
});
|
|
536
536
|
} catch (error) {
|
|
@@ -544,6 +544,56 @@ window.__ModuleLoader__.load({
|
|
|
544
544
|
return (await response.json().catch(() => void 0))?.value;
|
|
545
545
|
}
|
|
546
546
|
/**
|
|
547
|
+
* Read the committed CodeBuddy-CLI switch out of a settings snapshot.
|
|
548
|
+
*
|
|
549
|
+
* Only an explicit `true` is ON, matching the schema's `default(false)`: this
|
|
550
|
+
* switch authorizes a child process that edits files and spends CodeBuddy
|
|
551
|
+
* quota, so an absent value must never read as consent.
|
|
552
|
+
*/
|
|
553
|
+
function cliBridgeEnabledOf(configured) {
|
|
554
|
+
const bridge = configured?.cliBridge;
|
|
555
|
+
return typeof bridge === "object" && bridge !== null && bridge.enabled === true;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Write the GLOBAL CodeBuddy-CLI switch, verifying it landed.
|
|
559
|
+
*
|
|
560
|
+
* Deliberately NOT a region field: one route, one budget, one switch. The two
|
|
561
|
+
* writers are the same as everywhere else — the plugin's Host endpoint first
|
|
562
|
+
* (authoritative merge, so the tuned caps beside `enabled` survive), the
|
|
563
|
+
* settings scope second — because this namespace's browser mirror is documented
|
|
564
|
+
* as stale on the affected 0.1.7 deployments, and a switch that merely *looks*
|
|
565
|
+
* flipped is exactly the "关闭不了" class of report.
|
|
566
|
+
*
|
|
567
|
+
* @param scope - the bound settings scope for this plugin's namespace.
|
|
568
|
+
* @param enabled - the desired state.
|
|
569
|
+
* @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
|
|
570
|
+
*/
|
|
571
|
+
async function writeCliBridgeEnabled(scope, enabled) {
|
|
572
|
+
let hostError;
|
|
573
|
+
try {
|
|
574
|
+
const authoritative = await saveViaHostEndpoint("cliBridge", void 0, { enabled });
|
|
575
|
+
if (authoritative !== void 0) try {
|
|
576
|
+
await scope.set("cliBridge", authoritative);
|
|
577
|
+
} catch {}
|
|
578
|
+
return;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
hostError = error;
|
|
581
|
+
}
|
|
582
|
+
const stored = unwrapVolatileDeep(scope.getSnapshot().value?.cliBridge);
|
|
583
|
+
const next = {
|
|
584
|
+
...typeof stored === "object" && stored !== null ? stored : {},
|
|
585
|
+
enabled
|
|
586
|
+
};
|
|
587
|
+
let delivered = false;
|
|
588
|
+
try {
|
|
589
|
+
delivered = await scope.set("cliBridge", next) !== false;
|
|
590
|
+
} catch {
|
|
591
|
+
delivered = false;
|
|
592
|
+
}
|
|
593
|
+
if (delivered && cliBridgeEnabledOf(scope.getSnapshot().value)) return;
|
|
594
|
+
throw hostError instanceof Error ? hostError : new WorkBuddySettingsWriteError("cliBridge", "neither the Host save endpoint nor the settings scope persisted the value");
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
547
597
|
* Write one volatile field, then confirm the value actually landed.
|
|
548
598
|
*
|
|
549
599
|
* ORDER MATTERS, and getting it wrong DELETES DATA. The two writers do not have
|
|
@@ -1014,6 +1064,29 @@ window.__ModuleLoader__.load({
|
|
|
1014
1064
|
*/
|
|
1015
1065
|
const DAYS_AT_MS = 2 * DAY_MS$1;
|
|
1016
1066
|
/**
|
|
1067
|
+
* The recovery form of the same duration: "约 2 小时后恢复".
|
|
1068
|
+
*
|
|
1069
|
+
* The bare duration answers "how long?", but beside a red dot on an account row
|
|
1070
|
+
* it left the most important half unsaid: what happens when it elapses. "约 2
|
|
1071
|
+
* 小时后" reads equally well as "the limit lifts" and as "it expires into
|
|
1072
|
+
* something else", and on a row whose whole job is to say whether the account
|
|
1073
|
+
* can serve, that ambiguity is the question the reader came with.
|
|
1074
|
+
*
|
|
1075
|
+
* Same numbers, same rounding, same empty-string contract as
|
|
1076
|
+
* {@link remainingText} — this only appends the verb. Kept as a separate
|
|
1077
|
+
* function rather than a parameter so the two callers that must stay verbless
|
|
1078
|
+
* (the card's "之后可再用" sentence, which already contains its own verb) cannot
|
|
1079
|
+
* accidentally grow a second one.
|
|
1080
|
+
*/
|
|
1081
|
+
function recoversInText(t, untilMs, nowMs) {
|
|
1082
|
+
const left = untilMs - nowMs;
|
|
1083
|
+
if (left <= 0) return "";
|
|
1084
|
+
if (left < MINUTE_MS$1) return t("row.recoversImminent");
|
|
1085
|
+
if (left < HOUR_MS$1) return t("row.recoversMinutes", { count: Math.round(left / MINUTE_MS$1) });
|
|
1086
|
+
if (left < DAYS_AT_MS) return t("row.recoversHours", { count: Math.round(left / HOUR_MS$1) });
|
|
1087
|
+
return t("row.recoversDays", { count: Math.round(left / DAY_MS$1) });
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1017
1090
|
* A short "约 N 分钟后 / 约 N 小时后 / 约 N 天后", or `''` once the moment passed.
|
|
1018
1091
|
*
|
|
1019
1092
|
* The empty string is the contract callers depend on: an elapsed cooldown means
|
|
@@ -1039,6 +1112,29 @@ window.__ModuleLoader__.load({
|
|
|
1039
1112
|
function outcomeOk(outcome) {
|
|
1040
1113
|
return outcome === "ok";
|
|
1041
1114
|
}
|
|
1115
|
+
/**
|
|
1116
|
+
* The MARK form of one probe outcome: the verdict alone, for a slot that cannot
|
|
1117
|
+
* hold an explanation (issue #108).
|
|
1118
|
+
*
|
|
1119
|
+
* `outcomeText` is the right function wherever the reader has room — the pool
|
|
1120
|
+
* card's probe column, the test log — and there the extra sentence earns its
|
|
1121
|
+
* width. The composer panel is a different slot: one line, beside an account
|
|
1122
|
+
* name, in a column about 180 px wide. `row.probeChannelRejected` is 88
|
|
1123
|
+
* characters in Chinese and 179 in English, so it could never fit; with
|
|
1124
|
+
* `nowrap` + `overflow:hidden` on the row it was cut mid-sentence, which the
|
|
1125
|
+
* reporter read as broken rendering.
|
|
1126
|
+
*
|
|
1127
|
+
* So this function returns the SHORT label where one is declared and falls back
|
|
1128
|
+
* to {@link outcomeText} otherwise. The full sentence is never lost: the row's
|
|
1129
|
+
* `title` carries `availabilityLine`, which keeps every word and the absolute
|
|
1130
|
+
* instant, and the pool card still prints it in full.
|
|
1131
|
+
*/
|
|
1132
|
+
function outcomeMarkText(t, outcome) {
|
|
1133
|
+
switch (outcome) {
|
|
1134
|
+
case "channel-rejected": return t("row.probeChannelRejectedShort");
|
|
1135
|
+
default: return outcomeText(t, outcome);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1042
1138
|
/** A short, localizable label for one probe outcome. */
|
|
1043
1139
|
function outcomeText(t, outcome) {
|
|
1044
1140
|
switch (outcome) {
|
|
@@ -1123,11 +1219,11 @@ window.__ModuleLoader__.load({
|
|
|
1123
1219
|
const probe = account.probe;
|
|
1124
1220
|
if (probe !== void 0) {
|
|
1125
1221
|
if (probe.retryAtMs !== void 0) {
|
|
1126
|
-
const left =
|
|
1127
|
-
return left === "" ?
|
|
1222
|
+
const left = recoversInText(t, probe.retryAtMs, nowMs);
|
|
1223
|
+
return left === "" ? outcomeMarkText(t, probe.outcome) : `${outcomeMarkText(t, probe.outcome)} · ${left}`;
|
|
1128
1224
|
}
|
|
1129
|
-
if (probe.outcome === "rate-limited" || probe.outcome === "model-busy" || probe.outcome === "out-of-credit") return `${
|
|
1130
|
-
return
|
|
1225
|
+
if (probe.outcome === "rate-limited" || probe.outcome === "model-busy" || probe.outcome === "out-of-credit") return `${outcomeMarkText(t, probe.outcome)} · ${t("row.poolRetryUnknown")}`;
|
|
1226
|
+
return outcomeMarkText(t, probe.outcome);
|
|
1131
1227
|
}
|
|
1132
1228
|
if (account.excludedBy !== void 0) return exclusionLabel(t, account.excludedBy);
|
|
1133
1229
|
}
|
|
@@ -3856,6 +3952,221 @@ window.__ModuleLoader__.load({
|
|
|
3856
3952
|
});
|
|
3857
3953
|
}
|
|
3858
3954
|
//#endregion
|
|
3955
|
+
//#region src/client/busy-icon.tsx
|
|
3956
|
+
/** The word for a level, in the card's language, for the tooltip. */
|
|
3957
|
+
const TITLES_ZH = {
|
|
3958
|
+
idle: "空闲",
|
|
3959
|
+
normal: "正常",
|
|
3960
|
+
busy: "轻微拥堵",
|
|
3961
|
+
congested: "极度拥堵",
|
|
3962
|
+
unavailable: "不可用"
|
|
3963
|
+
};
|
|
3964
|
+
const TITLES_EN = {
|
|
3965
|
+
idle: "Idle",
|
|
3966
|
+
normal: "Normal",
|
|
3967
|
+
busy: "Slightly congested",
|
|
3968
|
+
congested: "Extremely congested",
|
|
3969
|
+
unavailable: "Unavailable"
|
|
3970
|
+
};
|
|
3971
|
+
/**
|
|
3972
|
+
* The CLOTHES colour per level, as a CSS custom property.
|
|
3973
|
+
*
|
|
3974
|
+
* These are the ramp: green → yellow → orange → red. The hair and the skin
|
|
3975
|
+
* keep their own colours in every level, so a row is recognisably the same
|
|
3976
|
+
* person in a different outfit — which is what makes the COUNT readable as
|
|
3977
|
+
* "more people, not different people".
|
|
3978
|
+
*/
|
|
3979
|
+
const COLORS = {
|
|
3980
|
+
idle: "var(--dsm-workbuddy-busy-idle, #3aa76d)",
|
|
3981
|
+
normal: "var(--dsm-workbuddy-busy-normal, #d9b300)",
|
|
3982
|
+
busy: "var(--dsm-workbuddy-busy-busy, #e08a1e)",
|
|
3983
|
+
congested: "var(--dsm-workbuddy-busy-congested, #d93a2b)",
|
|
3984
|
+
unavailable: "var(--dsm-workbuddy-busy-unavailable, #8a8f98)"
|
|
3985
|
+
};
|
|
3986
|
+
/** How many people each level draws. */
|
|
3987
|
+
const PEOPLE = {
|
|
3988
|
+
normal: 1,
|
|
3989
|
+
busy: 2,
|
|
3990
|
+
congested: 3
|
|
3991
|
+
};
|
|
3992
|
+
/**
|
|
3993
|
+
* The icon for a level, or `undefined` when the level is not drawn.
|
|
3994
|
+
*
|
|
3995
|
+
* Only `unknown` is left out: it is the plugin's own "the probe did not
|
|
3996
|
+
* answer", and a badge that guesses is worse than no badge. Every level the
|
|
3997
|
+
* upstream DID report is drawn — `idle` included, in green, because "the model
|
|
3998
|
+
* is free right now" is useful information.
|
|
3999
|
+
*/
|
|
4000
|
+
function busyIconOf(level) {
|
|
4001
|
+
if (level === void 0) return void 0;
|
|
4002
|
+
if (level === "unknown") return void 0;
|
|
4003
|
+
return level;
|
|
4004
|
+
}
|
|
4005
|
+
/**
|
|
4006
|
+
* One person, on the icon's own grid: 7.9 units wide, 19.3 units tall.
|
|
4007
|
+
*
|
|
4008
|
+
* The five parts are the emoji's own — hair, face, shirt, trousers, shoes —
|
|
4009
|
+
* because a stick figure would not read as the same mark as the `🧍` the
|
|
4010
|
+
* picker shows.
|
|
4011
|
+
*
|
|
4012
|
+
* NO FACE FEATURES. At the 12px the model row actually uses, an eye is 0.2px
|
|
4013
|
+
* wide: drawing it produces a smudge, not a face. So the face is a plain
|
|
4014
|
+
* skin-toned shape and the character is carried by the silhouette.
|
|
4015
|
+
*/
|
|
4016
|
+
function person(x, key, shirt, shirtEdge, trousers) {
|
|
4017
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
|
|
4018
|
+
transform: `translate(${x} 0)`,
|
|
4019
|
+
children: [
|
|
4020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4021
|
+
d: "M2.7 3.1c0-1.6 1-2.6 2.5-2.6s2.5 1 2.5 2.6c0 .8-.2 1.4-.6 1.9-.2-1-.7-1.5-1.3-1.7-.5.6-1.3.8-2 .7-.7-.1-1.2-.6-1.3-1.4-.5.1-.8.2-.8.2z",
|
|
4022
|
+
fill: "#f0c04a"
|
|
4023
|
+
}),
|
|
4024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4025
|
+
d: "M3 3.4h3.4v2.1c0 1.15-.76 2-1.7 2s-1.7-.85-1.7-2z",
|
|
4026
|
+
fill: "#ffd9a0"
|
|
4027
|
+
}),
|
|
4028
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4029
|
+
d: "M4.4 6.6h.6v.9h-.6z",
|
|
4030
|
+
fill: "#f3c493"
|
|
4031
|
+
}),
|
|
4032
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4033
|
+
d: "M1.75 8.9c-.45.45-.65 1.25-.65 2.15v2.9c0 .4.3.7.7.7s.7-.3.7-.7v-2.8c0-.6.1-1 .25-1.35zM7.55 8.9c.45.45.65 1.25.65 2.15v2.9c0 .4-.3.7-.7.7s-.7-.3-.7-.7v-2.8c0-.6-.1-1-.25-1.35z",
|
|
4034
|
+
fill: "#ffd9a0"
|
|
4035
|
+
}),
|
|
4036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4037
|
+
d: "M2.8 7.7h3.7c.75 0 1.3.55 1.3 1.3v5.1H1.5V9c0-.75.55-1.3 1.3-1.3z",
|
|
4038
|
+
fill: shirt
|
|
4039
|
+
}),
|
|
4040
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4041
|
+
d: "M3.9 7.7l.75 1 .75-1",
|
|
4042
|
+
stroke: shirtEdge,
|
|
4043
|
+
strokeWidth: ".45",
|
|
4044
|
+
fill: "none",
|
|
4045
|
+
strokeLinejoin: "round"
|
|
4046
|
+
}),
|
|
4047
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4048
|
+
d: "M1.9 14.1h2.05l-.3 4.3H1.6zM5.85 14.1h2.05l.3 4.3H5.5z",
|
|
4049
|
+
fill: trousers
|
|
4050
|
+
}),
|
|
4051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4052
|
+
d: "M2.15 18.4h1.4v.9H1.7c-.3 0-.45-.3-.3-.6zM5.85 18.4h1.4l.55.55c.2.2 0 .35-.3.35H5.85z",
|
|
4053
|
+
fill: "#2f3136"
|
|
4054
|
+
})
|
|
4055
|
+
]
|
|
4056
|
+
}, key);
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* A ROCKET, for the `idle` level: nobody is queuing, so the model is free to
|
|
4060
|
+
* run at full speed.
|
|
4061
|
+
*
|
|
4062
|
+
* A different SHAPE, not a different colour, and that is the point: the level
|
|
4063
|
+
* has to be readable at 12px without reading a word, and a rocket next to one,
|
|
4064
|
+
* two or three people separates "nobody is waiting" from "one person is
|
|
4065
|
+
* waiting" instantly. The people count the queue; the rocket says there is
|
|
4066
|
+
* none.
|
|
4067
|
+
*
|
|
4068
|
+
* SOLID, not outlined: a stroked rocket at this size turns into a smudge. The
|
|
4069
|
+
* body is filled in the level's colour, the window is white so it reads at
|
|
4070
|
+
* 12px, and the flame is a fixed orange (a flame is a flame).
|
|
4071
|
+
*/
|
|
4072
|
+
function rocket(color) {
|
|
4073
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
4074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4075
|
+
d: "M6.1 15.6c.2 1.6.8 2.8 1.9 3.7 1.1-.9 1.7-2.1 1.9-3.7z",
|
|
4076
|
+
fill: "#f5a623"
|
|
4077
|
+
}),
|
|
4078
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4079
|
+
d: "M4.4 11.8 2 16.1h2.5zM11.6 11.8l2.4 4.3h-2.5z",
|
|
4080
|
+
fill: shadeOf(color)
|
|
4081
|
+
}),
|
|
4082
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4083
|
+
d: "M8 .9c2.7 2.9 4.2 6.6 4.2 10.4 0 1.7-.4 3.2-1 4.5H4.8c-.6-1.3-1-2.8-1-4.5 0-3.8 1.5-7.5 4.2-10.4z",
|
|
4084
|
+
fill: color
|
|
4085
|
+
}),
|
|
4086
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
4087
|
+
cx: "8",
|
|
4088
|
+
cy: "6.6",
|
|
4089
|
+
r: "2.1",
|
|
4090
|
+
fill: "#ffffff"
|
|
4091
|
+
}),
|
|
4092
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4093
|
+
d: "M6.4 3.6c-.7 1.2-1.2 2.6-1.4 4.1",
|
|
4094
|
+
stroke: "#ffffff",
|
|
4095
|
+
strokeOpacity: ".35",
|
|
4096
|
+
strokeWidth: ".7",
|
|
4097
|
+
fill: "none",
|
|
4098
|
+
strokeLinecap: "round"
|
|
4099
|
+
})
|
|
4100
|
+
] });
|
|
4101
|
+
}
|
|
4102
|
+
/** The shaded edge of the clothes, one step darker than the clothes. */
|
|
4103
|
+
function shadeOf(color) {
|
|
4104
|
+
return `color-mix(in srgb, ${color} 78%, #000)`;
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* The load-level icon for one model row.
|
|
4108
|
+
*
|
|
4109
|
+
* Returns `null` for a level that is not drawn — including `undefined`, which
|
|
4110
|
+
* means "not measured", and `unknown`, which must never be turned into a
|
|
4111
|
+
* claim the upstream did not make.
|
|
4112
|
+
*/
|
|
4113
|
+
function busyIcon(level, language = "zh") {
|
|
4114
|
+
const icon = busyIconOf(level);
|
|
4115
|
+
if (icon === void 0) return null;
|
|
4116
|
+
const color = COLORS[icon];
|
|
4117
|
+
const title = (language.toLowerCase().startsWith("en") ? TITLES_EN : TITLES_ZH)[icon];
|
|
4118
|
+
if (icon === "unavailable") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4119
|
+
className: "dsm-workbuddy-busy dsm-workbuddy-busy-unavailable",
|
|
4120
|
+
title,
|
|
4121
|
+
style: { "--dsm-workbuddy-busy-color": color },
|
|
4122
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
4123
|
+
viewBox: "0 0 19.3 19.3",
|
|
4124
|
+
width: 12,
|
|
4125
|
+
height: 12,
|
|
4126
|
+
"aria-hidden": "true",
|
|
4127
|
+
focusable: "false",
|
|
4128
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
|
|
4129
|
+
opacity: ".5",
|
|
4130
|
+
children: person(0, "p", "#8a8f98", "#6f747c", "#6f747c")
|
|
4131
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
4132
|
+
d: "M0.6 19.4 19.4 0.6",
|
|
4133
|
+
stroke: "currentColor",
|
|
4134
|
+
strokeWidth: "1.8",
|
|
4135
|
+
strokeLinecap: "round",
|
|
4136
|
+
fill: "none"
|
|
4137
|
+
})]
|
|
4138
|
+
})
|
|
4139
|
+
});
|
|
4140
|
+
if (icon === "idle") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4141
|
+
className: "dsm-workbuddy-busy dsm-workbuddy-busy-idle",
|
|
4142
|
+
title,
|
|
4143
|
+
style: { "--dsm-workbuddy-busy-color": color },
|
|
4144
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
4145
|
+
viewBox: "0 0 16 19.3",
|
|
4146
|
+
width: 12,
|
|
4147
|
+
height: 12,
|
|
4148
|
+
"aria-hidden": "true",
|
|
4149
|
+
focusable: "false",
|
|
4150
|
+
children: rocket(color)
|
|
4151
|
+
})
|
|
4152
|
+
});
|
|
4153
|
+
const count = PEOPLE[icon] ?? 1;
|
|
4154
|
+
const width = (count - 1) * 9.6 + 7.9;
|
|
4155
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4156
|
+
className: `dsm-workbuddy-busy dsm-workbuddy-busy-${icon}`,
|
|
4157
|
+
title,
|
|
4158
|
+
style: { "--dsm-workbuddy-busy-color": color },
|
|
4159
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
4160
|
+
viewBox: `0 0 ${width} 19.3`,
|
|
4161
|
+
width: 12,
|
|
4162
|
+
height: 12,
|
|
4163
|
+
"aria-hidden": "true",
|
|
4164
|
+
focusable: "false",
|
|
4165
|
+
children: Array.from({ length: count }, (_, index) => person(index * 9.6, `p${index}`, color, shadeOf(color), shadeOf(color)))
|
|
4166
|
+
})
|
|
4167
|
+
});
|
|
4168
|
+
}
|
|
4169
|
+
//#endregion
|
|
3859
4170
|
//#region src/client/promo-badge.ts
|
|
3860
4171
|
/**
|
|
3861
4172
|
* The UI's language, as the browser reports it.
|
|
@@ -4154,7 +4465,7 @@ window.__ModuleLoader__.load({
|
|
|
4154
4465
|
CONTRAST colour, so a ticked box painted itself as a heavy black pill — the
|
|
4155
4466
|
loudest object in a section made of plain 13px text. */
|
|
4156
4467
|
.dsm-workbuddy-pref input{flex:none;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
4157
|
-
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
4468
|
+
.dsm-workbuddy-pref-check,.dsm-workbuddy-image-tool-check,.dsm-workbuddy-cli-bridge-check{inline-size:15px;block-size:15px;margin:3px 0 0}
|
|
4158
4469
|
.dsm-workbuddy-pref input:disabled{cursor:default}
|
|
4159
4470
|
.dsm-workbuddy-pref:has(input:disabled){opacity:.6;cursor:default}
|
|
4160
4471
|
/* The NAME and what it does are TWO lines, not one wrapped blob: first what the
|
|
@@ -4291,6 +4602,9 @@ window.__ModuleLoader__.load({
|
|
|
4291
4602
|
one the brand colour stands in. "flex:none" keeps the badge whole while the
|
|
4292
4603
|
model name beside it truncates with an ellipsis. The plain background comes
|
|
4293
4604
|
first so an engine without color-mix still gets a readable chip. */
|
|
4605
|
+
.dsm-workbuddy-model-label{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;vertical-align:baseline}
|
|
4606
|
+
.dsm-workbuddy-busy{flex:none;display:inline-flex;align-items:center;block-size:12px;color:var(--dsm-workbuddy-busy-color);vertical-align:-2px}
|
|
4607
|
+
.dsm-workbuddy-busy svg{display:block;block-size:12px;inline-size:auto}
|
|
4294
4608
|
.dsm-workbuddy-model-promo{flex:none;display:inline-flex;align-items:center;padding:0 7px;border-radius:999px;font-size:10px;line-height:16px;font-weight:600;white-space:nowrap;color:var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe));background:rgba(255,255,255,.08);background:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 16%,transparent);border:1px solid transparent;border-color:color-mix(in srgb,var(--dsm-workbuddy-promo-color,var(--dsw-alias-brand-primary,#5686fe)) 40%,transparent)}
|
|
4295
4609
|
.dsm-workbuddy-model-id{color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4296
4610
|
.dsm-workbuddy-model-desc{margin:0;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
|
|
@@ -4858,9 +5172,20 @@ window.__ModuleLoader__.load({
|
|
|
4858
5172
|
.dsm-workbuddy-composer-panel-mark{
|
|
4859
5173
|
display:flex;flex-wrap:wrap;align-items:baseline;
|
|
4860
5174
|
font-size:11px;line-height:15px;
|
|
5175
|
+
/* WRAP, do not clip (issue #108). The switch above sets white-space:nowrap
|
|
5176
|
+
and overflow:hidden for the NAME, which must stay on one line and ellipsise.
|
|
5177
|
+
Inherited here, those two rules cut the reason mark mid-sentence with no
|
|
5178
|
+
ellipsis at all — the reporter saw "…不是这个账号—" and read it as broken
|
|
5179
|
+
rendering, because a hard cut carries no signal that more exists. The mark
|
|
5180
|
+
is the one part of this row whose length is NOT under the layout's control
|
|
5181
|
+
(it is a translated sentence, and the two locales differ by 2x), so it
|
|
5182
|
+
wraps instead: an extra line is a legible outcome, a severed stroke is not.
|
|
5183
|
+
overflow-wrap:anywhere because Chinese has no spaces to break at. */
|
|
5184
|
+
white-space:normal;
|
|
5185
|
+
overflow-wrap:anywhere;
|
|
4861
5186
|
}
|
|
4862
5187
|
.dsm-workbuddy-composer-panel-mark-current{color:var(--dsw-alias-state-success-primary,#22a06b)}
|
|
4863
|
-
.dsm-workbuddy-composer-panel-mark-reason{color:var(--dsw-alias-state-warn-primary,#f59e0b)}
|
|
5188
|
+
.dsm-workbuddy-composer-panel-mark-reason{color:var(--dsw-alias-state-warn-primary,#f59e0b);min-width:0}
|
|
4864
5189
|
.dsm-workbuddy-composer-panel-mark-current+.dsm-workbuddy-composer-panel-mark-reason::before{
|
|
4865
5190
|
content:" · ";color:var(--dsw-alias-label-dimmed,rgba(127,127,127,.6));
|
|
4866
5191
|
}
|
|
@@ -5369,6 +5694,14 @@ window.__ModuleLoader__.load({
|
|
|
5369
5694
|
*/
|
|
5370
5695
|
const [togglingCredits, setTogglingCredits] = (0, react.useState)(void 0);
|
|
5371
5696
|
/**
|
|
5697
|
+
* The OPTIONAL CodeBuddy CLI route switch (global: one switch, one budget).
|
|
5698
|
+
*
|
|
5699
|
+
* `undefined` until the first Host answer arrives, so the box renders the
|
|
5700
|
+
* committed value instead of guessing — the same rule as `creditsShownOn`.
|
|
5701
|
+
*/
|
|
5702
|
+
const [cliBridgeOn, setCliBridgeOn] = (0, react.useState)(void 0);
|
|
5703
|
+
const [cliBridgeBusy, setCliBridgeBusy] = (0, react.useState)(false);
|
|
5704
|
+
/**
|
|
5372
5705
|
* Probe results, keyed by region then model id.
|
|
5373
5706
|
*
|
|
5374
5707
|
* Kept per region for the same reason drafts are: the two tabs are separate
|
|
@@ -5796,6 +6129,39 @@ window.__ModuleLoader__.load({
|
|
|
5796
6129
|
if (mounted.current) setTogglingCredits(void 0);
|
|
5797
6130
|
}
|
|
5798
6131
|
};
|
|
6132
|
+
/**
|
|
6133
|
+
* Whether the CLI route is mounted.
|
|
6134
|
+
*
|
|
6135
|
+
* Host answer first, settings mirror second, for the same measured reason as
|
|
6136
|
+
* `imageToolOn`: this namespace's browser mirror goes stale on the affected
|
|
6137
|
+
* deployments, and here a stale `true` would claim a spending capability is on.
|
|
6138
|
+
*/
|
|
6139
|
+
const cliBridgeSwitchedOn = () => {
|
|
6140
|
+
if (cliBridgeOn !== void 0) return cliBridgeOn;
|
|
6141
|
+
const fromHost = statusByRegion[activeRegion]?.cliBridgeEnabled;
|
|
6142
|
+
if (typeof fromHost === "boolean") return fromHost;
|
|
6143
|
+
return cliBridgeEnabledOf(settingsScope?.getSnapshot().value);
|
|
6144
|
+
};
|
|
6145
|
+
/**
|
|
6146
|
+
* Flip the global CLI-route switch.
|
|
6147
|
+
*
|
|
6148
|
+
* The Host rebuilds only this route on the change (`cliBridge` is volatile),
|
|
6149
|
+
* so no provider stack is disposed and no in-flight request is interrupted.
|
|
6150
|
+
* Re-reads the usage afterwards so the box shows the COMMITTED value.
|
|
6151
|
+
*/
|
|
6152
|
+
const toggleCliBridge = async (enabled) => {
|
|
6153
|
+
if (settingsScope === void 0) return;
|
|
6154
|
+
setCliBridgeBusy(true);
|
|
6155
|
+
try {
|
|
6156
|
+
await writeCliBridgeEnabled(settingsScope, enabled);
|
|
6157
|
+
if (mounted.current) setCliBridgeOn(enabled);
|
|
6158
|
+
await refreshUsage(activeRegion);
|
|
6159
|
+
} catch (error) {
|
|
6160
|
+
if (mounted.current) setAccountError(error instanceof Error ? error.message : t("row.requestFailed"));
|
|
6161
|
+
} finally {
|
|
6162
|
+
if (mounted.current) setCliBridgeBusy(false);
|
|
6163
|
+
}
|
|
6164
|
+
};
|
|
5799
6165
|
const toggleImageTool = async (item, enabled) => {
|
|
5800
6166
|
if (settingsScope === void 0) return;
|
|
5801
6167
|
setTogglingCredits(item);
|
|
@@ -5853,6 +6219,15 @@ window.__ModuleLoader__.load({
|
|
|
5853
6219
|
};
|
|
5854
6220
|
const draft = drafts[activeRegion];
|
|
5855
6221
|
const visibleModels = draft?.models ?? (status.status === "signed-in" ? status.models : []);
|
|
6222
|
+
/**
|
|
6223
|
+
* The LIVE load levels, keyed by model id.
|
|
6224
|
+
*
|
|
6225
|
+
* Read off the Host's answer rather than from the model list: the list is the
|
|
6226
|
+
* SAVED roster the user edits, and a saved roster has no load level in it
|
|
6227
|
+
* (load is a fact about this minute, not a setting — `src/busy.ts`). A model
|
|
6228
|
+
* the probe did not answer for simply has no entry, and draws nothing.
|
|
6229
|
+
*/
|
|
6230
|
+
const busyLevels = status.status === "signed-in" ? status.busyLevels ?? {} : {};
|
|
5856
6231
|
const savedEnabledIds = status.status === "signed-in" ? new Set(status.enabledModelIds) : /* @__PURE__ */ new Set();
|
|
5857
6232
|
const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
|
|
5858
6233
|
const savedImageIds = status.status === "signed-in" ? new Set(status.imageModelIds) : /* @__PURE__ */ new Set();
|
|
@@ -6314,9 +6689,41 @@ window.__ModuleLoader__.load({
|
|
|
6314
6689
|
})]
|
|
6315
6690
|
})]
|
|
6316
6691
|
}) : null;
|
|
6692
|
+
/**
|
|
6693
|
+
* The CLI-route switch.
|
|
6694
|
+
*
|
|
6695
|
+
* GLOBAL, so it is not gated on the active tab's provider being on — the route
|
|
6696
|
+
* pulls its credential from whichever region still has a signed-in account.
|
|
6697
|
+
* Its own class (not `.dsm-workbuddy-pref-check`) for the same reason the image
|
|
6698
|
+
* box has one: the credit-switch suite's "exactly one switch" selector must
|
|
6699
|
+
* keep resolving to the credit box alone.
|
|
6700
|
+
*/
|
|
6701
|
+
const cliBridgePreference = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
6702
|
+
className: "dsm-workbuddy-pref",
|
|
6703
|
+
title: t("row.cliBridgeHint"),
|
|
6704
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
6705
|
+
type: "checkbox",
|
|
6706
|
+
className: "dsm-workbuddy-cli-bridge-check",
|
|
6707
|
+
checked: cliBridgeSwitchedOn(),
|
|
6708
|
+
disabled: !canWrite || saving || poolBusy || cliBridgeBusy,
|
|
6709
|
+
onChange: (event) => {
|
|
6710
|
+
toggleCliBridge(event.target.checked);
|
|
6711
|
+
}
|
|
6712
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6713
|
+
className: "dsm-workbuddy-pref-copy",
|
|
6714
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6715
|
+
className: "dsm-workbuddy-pref-label",
|
|
6716
|
+
children: t("row.cliBridgeLabel")
|
|
6717
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6718
|
+
className: "dsm-workbuddy-pref-hint",
|
|
6719
|
+
children: t("row.cliBridgeHint")
|
|
6720
|
+
})]
|
|
6721
|
+
})]
|
|
6722
|
+
});
|
|
6317
6723
|
const creditPreferences = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6318
6724
|
className: "dsm-workbuddy-prefs",
|
|
6319
6725
|
children: [
|
|
6726
|
+
cliBridgePreference,
|
|
6320
6727
|
imageToolPreference,
|
|
6321
6728
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
6322
6729
|
className: "dsm-workbuddy-pref",
|
|
@@ -6665,17 +7072,21 @@ window.__ModuleLoader__.load({
|
|
|
6665
7072
|
}
|
|
6666
7073
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6667
7074
|
className: "dsm-workbuddy-model-copy",
|
|
6668
|
-
children: [
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
7075
|
+
children: [
|
|
7076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7077
|
+
className: "dsm-workbuddy-model-name",
|
|
7078
|
+
children: [model.name, model.creditMultiplier === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7079
|
+
className: "dsm-workbuddy-model-name-rate",
|
|
7080
|
+
children: [
|
|
7081
|
+
"(",
|
|
7082
|
+
model.creditMultiplier.toFixed(2),
|
|
7083
|
+
"x)"
|
|
7084
|
+
]
|
|
7085
|
+
})]
|
|
7086
|
+
}),
|
|
7087
|
+
promoChip(model),
|
|
7088
|
+
busyIcon(busyLevels[model.id], uiLanguage())
|
|
7089
|
+
]
|
|
6679
7090
|
})]
|
|
6680
7091
|
}),
|
|
6681
7092
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -7860,6 +8271,110 @@ window.__ModuleLoader__.load({
|
|
|
7860
8271
|
});
|
|
7861
8272
|
}
|
|
7862
8273
|
//#endregion
|
|
8274
|
+
//#region src/client/WorkBuddyModelLabel.tsx
|
|
8275
|
+
/**
|
|
8276
|
+
* The load-level badge in the HOST's model picker.
|
|
8277
|
+
*
|
|
8278
|
+
* WHY A SLOT. The host renders the model name as a plain string, so the only
|
|
8279
|
+
* thing a provider can put there is a character — and a character's colour is
|
|
8280
|
+
* the emoji font's business, not ours. The host therefore offers exactly one
|
|
8281
|
+
* seam for this: the `conversation.input.model.label` slot, whose contract is
|
|
8282
|
+
* "render non-interactive inline content such as a coloured name or badge"
|
|
8283
|
+
* while the picker keeps owning clicks, focus, check marks and the rest. The
|
|
8284
|
+
* shipped example is the official DeepSeek "1017" release badge — a coloured
|
|
8285
|
+
* marker next to a model name — which is the same job.
|
|
8286
|
+
*
|
|
8287
|
+
* WHY A SEPARATE FETCH. The slot is rendered by the host, outside the plugin's
|
|
8288
|
+
* card, so it cannot read the card's React state. It therefore asks the Host
|
|
8289
|
+
* for the load levels itself. The levels are a fact about this minute, so the
|
|
8290
|
+
* answer is cached briefly: a picker that re-fetches on every re-render would
|
|
8291
|
+
* be wasteful, and a level that is a few seconds old is still true.
|
|
8292
|
+
*
|
|
8293
|
+
* WHY THE HOST STILL OWNS THE NAME. The contract says to keep `model.name` as
|
|
8294
|
+
* the text and render the addition beside it, so this component returns the
|
|
8295
|
+
* name followed by the badge — it never replaces the name, and it renders
|
|
8296
|
+
* nothing at all for a provider that is not ours.
|
|
8297
|
+
*/
|
|
8298
|
+
/** The provider ids this plugin registers, and the region each one serves. */
|
|
8299
|
+
const PROVIDER_REGIONS = /* @__PURE__ */ new Map([["workbuddy", "cn"], ["workbuddy-global", "global"]]);
|
|
8300
|
+
/**
|
|
8301
|
+
* The region a provider id serves, or `undefined` for a provider that is not
|
|
8302
|
+
* this plugin's.
|
|
8303
|
+
*
|
|
8304
|
+
* Exported because the mapping is the part that can be silently wrong: asking
|
|
8305
|
+
* the Host without the region answers for the DEFAULT region, which would put
|
|
8306
|
+
* one gateway's load on the other gateway's models.
|
|
8307
|
+
*/
|
|
8308
|
+
function regionOfProvider(providerId) {
|
|
8309
|
+
return PROVIDER_REGIONS.get(providerId);
|
|
8310
|
+
}
|
|
8311
|
+
/** How long a fetched level set is trusted, in milliseconds. */
|
|
8312
|
+
const CACHE_MS = 6e4;
|
|
8313
|
+
/** The levels per region, by model id, plus when each set was read. */
|
|
8314
|
+
const caches = /* @__PURE__ */ new Map();
|
|
8315
|
+
/**
|
|
8316
|
+
* The levels, read from the Host once and reused briefly.
|
|
8317
|
+
*
|
|
8318
|
+
* `undefined` means "not known" — the caller renders nothing rather than
|
|
8319
|
+
* guessing, because a badge that claims a level the upstream never stated is
|
|
8320
|
+
* worse than no badge.
|
|
8321
|
+
*/
|
|
8322
|
+
async function levelsNow(region) {
|
|
8323
|
+
const cached = caches.get(region);
|
|
8324
|
+
if (cached !== void 0 && Date.now() - cached.at < CACHE_MS) return cached.levels;
|
|
8325
|
+
try {
|
|
8326
|
+
const response = await fetch(withWorkBuddyRegion(WORKBUDDY_USAGE_PATH, region), {
|
|
8327
|
+
headers: { accept: "application/json" },
|
|
8328
|
+
credentials: "same-origin"
|
|
8329
|
+
});
|
|
8330
|
+
if (!response.ok) return void 0;
|
|
8331
|
+
const levels = (await response.json()).busyLevels;
|
|
8332
|
+
if (levels === void 0 || levels === null) return void 0;
|
|
8333
|
+
caches.set(region, {
|
|
8334
|
+
levels,
|
|
8335
|
+
at: Date.now()
|
|
8336
|
+
});
|
|
8337
|
+
return levels;
|
|
8338
|
+
} catch {
|
|
8339
|
+
return;
|
|
8340
|
+
}
|
|
8341
|
+
}
|
|
8342
|
+
/**
|
|
8343
|
+
* The label for one model row in the host's picker.
|
|
8344
|
+
*
|
|
8345
|
+
* @param props - The host's model identity and the placement of this label.
|
|
8346
|
+
* @returns The catalog name, followed by the load badge when there is one.
|
|
8347
|
+
*/
|
|
8348
|
+
function WorkBuddyModelLabel(props) {
|
|
8349
|
+
const { provider, model } = props;
|
|
8350
|
+
const region = regionOfProvider(provider.id);
|
|
8351
|
+
if (region === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
|
|
8352
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkBuddyModelLabelInner, {
|
|
8353
|
+
modelId: model.id,
|
|
8354
|
+
name: model.name,
|
|
8355
|
+
region
|
|
8356
|
+
});
|
|
8357
|
+
}
|
|
8358
|
+
/** The stateful half: fetches the levels, then renders the badge. */
|
|
8359
|
+
function WorkBuddyModelLabelInner(props) {
|
|
8360
|
+
const { modelId, name, region } = props;
|
|
8361
|
+
const [level, setLevel] = (0, react.useState)(void 0);
|
|
8362
|
+
(0, react.useEffect)(() => {
|
|
8363
|
+
let live = true;
|
|
8364
|
+
levelsNow(region).then((levels) => {
|
|
8365
|
+
if (!live || levels === void 0) return;
|
|
8366
|
+
setLevel(levels[modelId]);
|
|
8367
|
+
});
|
|
8368
|
+
return () => {
|
|
8369
|
+
live = false;
|
|
8370
|
+
};
|
|
8371
|
+
}, [modelId, region]);
|
|
8372
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8373
|
+
className: "dsm-workbuddy-model-label",
|
|
8374
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), level === void 0 ? null : busyIcon(level, "zh")]
|
|
8375
|
+
});
|
|
8376
|
+
}
|
|
8377
|
+
//#endregion
|
|
7863
8378
|
//#region src/client/WorkBuddyQuotaNotice.tsx
|
|
7864
8379
|
/**
|
|
7865
8380
|
* The supplement this plugin adds to a quota failure (issue #63).
|
|
@@ -8098,6 +8613,7 @@ window.__ModuleLoader__.load({
|
|
|
8098
8613
|
"row.probeCredentialRejected": "✗ WorkBuddy refused this account's token — sign in again or pick another account",
|
|
8099
8614
|
"row.probePolicyRejected": "✗ WorkBuddy's server refused this request under its content policy — re-signing in won't help; try the desktop app or another region/account",
|
|
8100
8615
|
"row.probeChannelRejected": "✗ The gateway refused the PROMPT itself, not this account — the opening of a system message matched its third-party-agent filter, so another account or region answers the same way",
|
|
8616
|
+
"row.probeChannelRejectedShort": "✗ Gateway refused the prompt",
|
|
8101
8617
|
"row.probeNotFound": "✗ The upstream does not know this model",
|
|
8102
8618
|
"row.probeUnavailable": "✗ The upstream could not be reached — no usable answer came back",
|
|
8103
8619
|
"row.probeFailed": "✗ Test failed",
|
|
@@ -8195,6 +8711,10 @@ window.__ModuleLoader__.load({
|
|
|
8195
8711
|
"row.remainingMinutes": "in about {count} min",
|
|
8196
8712
|
"row.remainingHours": "in about {count} h",
|
|
8197
8713
|
"row.remainingDays": "in about {count} d",
|
|
8714
|
+
"row.recoversImminent": "recovering any moment now",
|
|
8715
|
+
"row.recoversMinutes": "recovers in about {count} min",
|
|
8716
|
+
"row.recoversHours": "recovers in about {count} h",
|
|
8717
|
+
"row.recoversDays": "recovers in about {count} d",
|
|
8198
8718
|
"row.poolProbeJustNow": "just now",
|
|
8199
8719
|
"row.poolProbeMinutesAgo": "{count} min ago",
|
|
8200
8720
|
"row.poolProbeHoursAgo": "{count} h ago",
|
|
@@ -8284,6 +8804,8 @@ window.__ModuleLoader__.load({
|
|
|
8284
8804
|
"row.poolRowRemoved": "{accountName}: hidden from the list. 「Detect accounts again」 re-adds it; the desktop app stays signed in.",
|
|
8285
8805
|
"row.poolRowRemoving": "Removing the account…",
|
|
8286
8806
|
"row.poolRowRemoveDesktopOnly": "{accountName}: hidden from the list — this account comes from the desktop app's own sign-in, which is left untouched. 「Detect accounts again」 re-adds it.",
|
|
8807
|
+
"row.cliBridgeLabel": "Let the local CodeBuddy CLI do the work",
|
|
8808
|
+
"row.cliBridgeHint": "Adds the codebuddy_ask tool and the /codebuddy command. Each call spends CodeBuddy quota (and, with injectResult on, one DSH turn). Off by default.",
|
|
8287
8809
|
"row.imageToolLabel": "Let the model draw images",
|
|
8288
8810
|
"row.imageToolHint": "Adds the workbuddy_draw_image tool, which the MODEL may call on its own — about 5.71 credits per image, with no per-call confirmation. Off means the tool is not registered at all.",
|
|
8289
8811
|
"row.showCreditsCn": "Show credits in the composer toolbar",
|
|
@@ -8457,6 +8979,7 @@ window.__ModuleLoader__.load({
|
|
|
8457
8979
|
"row.probeCredentialRejected": "✗ WorkBuddy 拒绝了这个账号的 token——请重新登录或换一个账号",
|
|
8458
8980
|
"row.probePolicyRejected": "✗ WorkBuddy 服务端按内容策略拒绝了这次请求——重新登录无效;可在桌面端验证或切换区域/账号",
|
|
8459
8981
|
"row.probeChannelRejected": "✗ 网关拦的是提示词本身,不是这个账号——某条 system 消息的开头命中了它的第三方 agent 过滤,换账号或换区域都会得到同一个答案(测试能过、聊天被拒,正是这个原因)",
|
|
8982
|
+
"row.probeChannelRejectedShort": "✗ 网关拦的是提示词",
|
|
8460
8983
|
"row.probeNotFound": "✗ 上游不认识这个模型",
|
|
8461
8984
|
"row.probeUnavailable": "✗ 连不上上游——上游没有给出可用答复",
|
|
8462
8985
|
"row.probeFailed": "✗ 测试失败",
|
|
@@ -8554,6 +9077,10 @@ window.__ModuleLoader__.load({
|
|
|
8554
9077
|
"row.remainingMinutes": "约 {count} 分钟后",
|
|
8555
9078
|
"row.remainingHours": "约 {count} 小时后",
|
|
8556
9079
|
"row.remainingDays": "约 {count} 天后",
|
|
9080
|
+
"row.recoversImminent": "即将恢复",
|
|
9081
|
+
"row.recoversMinutes": "约 {count} 分钟后恢复",
|
|
9082
|
+
"row.recoversHours": "约 {count} 小时后恢复",
|
|
9083
|
+
"row.recoversDays": "约 {count} 天后恢复",
|
|
8557
9084
|
"row.poolProbeJustNow": "刚刚",
|
|
8558
9085
|
"row.poolProbeMinutesAgo": "{count} 分钟前",
|
|
8559
9086
|
"row.poolProbeHoursAgo": "{count} 小时前",
|
|
@@ -8643,6 +9170,8 @@ window.__ModuleLoader__.load({
|
|
|
8643
9170
|
"row.poolRowRemoved": "{accountName}:已从列表中隐藏。点「重新检测账号」会重新加入,桌面 App 的登录不受影响。",
|
|
8644
9171
|
"row.poolRowRemoving": "正在删除账号…",
|
|
8645
9172
|
"row.poolRowRemoveDesktopOnly": "{accountName}:已从列表中隐藏——这个账号来自桌面 App 自己的登录,该登录未被改动。点「重新检测账号」会重新加入。",
|
|
9173
|
+
"row.cliBridgeLabel": "允许调用本机 CodeBuddy CLI 干活",
|
|
9174
|
+
"row.cliBridgeHint": "开启后多出 codebuddy_ask 工具与 /codebuddy 指令;每次调用花 CodeBuddy 配额(injectResult 开着时还会再花一次 DSH token)。默认关闭。",
|
|
8646
9175
|
"row.imageToolLabel": "允许模型自主生图",
|
|
8647
9176
|
"row.imageToolHint": "开启后模型可以自己调用 workbuddy_draw_image 画图——约 5.71 积分/张,没有逐次确认。关闭时该工具根本不会注册。",
|
|
8648
9177
|
"row.showCreditsCn": "在输入框工具栏显示积分读数",
|
|
@@ -8799,6 +9328,29 @@ window.__ModuleLoader__.load({
|
|
|
8799
9328
|
}
|
|
8800
9329
|
};
|
|
8801
9330
|
/**
|
|
9331
|
+
* The load badge in the HOST's model picker.
|
|
9332
|
+
*
|
|
9333
|
+
* The picker renders the model name as a plain string, so a provider can
|
|
9334
|
+
* only put a CHARACTER there — and a character's colour is the emoji font's
|
|
9335
|
+
* business, not ours. The host therefore offers exactly one seam for this:
|
|
9336
|
+
* the `conversation.input.model.label` slot, whose contract is "render
|
|
9337
|
+
* non-interactive inline content such as a coloured name or badge" while
|
|
9338
|
+
* the picker keeps owning clicks, focus and check marks. The shipped
|
|
9339
|
+
* example is the official DeepSeek "1017" release badge, which is the same
|
|
9340
|
+
* job — a coloured marker beside a model name.
|
|
9341
|
+
*
|
|
9342
|
+
* Registered unconditionally: the component itself renders nothing for a
|
|
9343
|
+
* provider that is not ours, so there is nothing to gate on.
|
|
9344
|
+
*/
|
|
9345
|
+
const registerModelLabel = () => {
|
|
9346
|
+
try {
|
|
9347
|
+
return ctx.slots.inject("conversation.input.model.label", () => ctx.slots.register({ name: "conversation.input.model.label" }, WorkBuddyModelLabel));
|
|
9348
|
+
} catch (error) {
|
|
9349
|
+
console.error("[dsh-connect-workbuddy] model label slot failed to register (host provider unaffected):", error);
|
|
9350
|
+
return;
|
|
9351
|
+
}
|
|
9352
|
+
};
|
|
9353
|
+
/**
|
|
8802
9354
|
* The SAME readout in the ambient dock below the composer card (issue #62).
|
|
8803
9355
|
*
|
|
8804
9356
|
* Registered unconditionally, like the composer seat, because the CHOICE
|
|
@@ -8902,7 +9454,8 @@ window.__ModuleLoader__.load({
|
|
|
8902
9454
|
...WORKBUDDY_CARD_KEYS.map((key) => registerCard("plugins.row.config", `${key}#${key}`)),
|
|
8903
9455
|
registerComposerCredits(),
|
|
8904
9456
|
registerStatsCredits(),
|
|
8905
|
-
registerQuotaNotice()
|
|
9457
|
+
registerQuotaNotice(),
|
|
9458
|
+
registerModelLabel()
|
|
8906
9459
|
].filter((disposer) => disposer !== void 0);
|
|
8907
9460
|
/**
|
|
8908
9461
|
* Disposers of the slots registered for the CURRENT binding.
|