dsh-workbuddy-connect 0.3.2 → 0.5.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.en.md +40 -10
- package/README.md +42 -12
- package/lib/bin.js +71 -23
- package/lib/client.js +1033 -45
- package/lib/index.d.ts +619 -13
- package/lib/index.js +1196 -126
- package/lib/variants-GgLTJgZs.js +2321 -0
- package/package.json +11 -5
- package/lib/host-heartbeat-BSsKUoB8.js +0 -1198
package/lib/client.js
CHANGED
|
@@ -10,9 +10,47 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
/** Node-free constants and types shared by the Host and browser halves. */
|
|
11
11
|
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
12
12
|
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
|
|
13
|
+
/**
|
|
14
|
+
* Plugin-owned probe control endpoint.
|
|
15
|
+
*
|
|
16
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
17
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
18
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
19
|
+
* also requires the in-process key the browser half receives with the status
|
|
20
|
+
* document.
|
|
21
|
+
*/
|
|
22
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
23
|
+
/**
|
|
24
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
25
|
+
*
|
|
26
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
27
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
28
|
+
* independently, and a shared expression is one build-config drift away from
|
|
29
|
+
* the desk asking a route the host never mounted.
|
|
30
|
+
*/
|
|
31
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
32
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
13
33
|
//#endregion
|
|
14
34
|
//#region src/client/WorkBuddyPluginCard.tsx
|
|
15
35
|
/** WorkBuddy status card contributed to Harness Plugin configuration. */
|
|
36
|
+
/** CN WorkBuddy; the plugin's long-standing card and default. */
|
|
37
|
+
const CN_CARD_VARIANT = {
|
|
38
|
+
id: "workbuddy",
|
|
39
|
+
titleKey: "title",
|
|
40
|
+
introKey: "intro",
|
|
41
|
+
signedOutKey: "signedOutHint",
|
|
42
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
43
|
+
probePath: WORKBUDDY_PROBE_PATH
|
|
44
|
+
};
|
|
45
|
+
/** Both cards, in display order. */
|
|
46
|
+
const CARD_VARIANTS = [CN_CARD_VARIANT, {
|
|
47
|
+
id: "workbuddy-ai",
|
|
48
|
+
titleKey: "titleAI",
|
|
49
|
+
introKey: "introAI",
|
|
50
|
+
signedOutKey: "signedOutHintAI",
|
|
51
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
52
|
+
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
53
|
+
}];
|
|
16
54
|
const POLL_INTERVAL_MS = 6e4;
|
|
17
55
|
const cardStyle = {
|
|
18
56
|
overflow: "hidden",
|
|
@@ -82,7 +120,7 @@ window.__ModuleLoader__.load({
|
|
|
82
120
|
fontWeight: 500,
|
|
83
121
|
color: "var(--dsw-alias-label-primary)"
|
|
84
122
|
};
|
|
85
|
-
const buttonStyle = {
|
|
123
|
+
const buttonStyle$1 = {
|
|
86
124
|
boxSizing: "border-box",
|
|
87
125
|
minHeight: 34,
|
|
88
126
|
padding: "6px 14px",
|
|
@@ -148,10 +186,19 @@ window.__ModuleLoader__.load({
|
|
|
148
186
|
background: "var(--dsw-alias-state-success-subtle, rgba(34, 160, 107, 0.12))",
|
|
149
187
|
color: "var(--dsw-alias-state-success-primary, #22a06b)"
|
|
150
188
|
};
|
|
151
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Localize an upstream promotional badge label, with an unknown-badge fallback.
|
|
191
|
+
*
|
|
192
|
+
* The CN catalog spells badges in Chinese (`限时免费`, `夜间折扣`); the
|
|
193
|
+
* international document's `modelPromotions` carries English (`Free now`). Both
|
|
194
|
+
* are mapped so the same promotion reads consistently in either UI language,
|
|
195
|
+
* and anything else passes through verbatim — an unrecognized badge is still
|
|
196
|
+
* information the upstream chose to show.
|
|
197
|
+
*/
|
|
152
198
|
function modelBadgeLabel(badge, t) {
|
|
153
199
|
if (badge === "限时免费") return t("badgeLimitedFree");
|
|
154
200
|
if (badge === "夜间折扣") return t("badgeNightDiscount");
|
|
201
|
+
if (badge === "Free now") return t("badgeFreeNow");
|
|
155
202
|
return badge;
|
|
156
203
|
}
|
|
157
204
|
const progressTrackStyle = {
|
|
@@ -160,6 +207,82 @@ window.__ModuleLoader__.load({
|
|
|
160
207
|
borderRadius: 999,
|
|
161
208
|
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
|
|
162
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* Inline confirmation box for a paid detection. Replaces the previous
|
|
212
|
+
* `window.confirm`: the decision is one line plus two buttons, and a modal
|
|
213
|
+
* alert for that is heavier than the action it guards.
|
|
214
|
+
*/
|
|
215
|
+
const confirmBoxStyle = {
|
|
216
|
+
display: "flex",
|
|
217
|
+
flexDirection: "column",
|
|
218
|
+
gap: 10,
|
|
219
|
+
padding: "10px 12px",
|
|
220
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
221
|
+
borderRadius: 8,
|
|
222
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
223
|
+
};
|
|
224
|
+
const confirmRowStyle$1 = {
|
|
225
|
+
display: "flex",
|
|
226
|
+
justifyContent: "flex-end",
|
|
227
|
+
gap: 8
|
|
228
|
+
};
|
|
229
|
+
/** One probeable model's row: name on the left, state and action on the right. */
|
|
230
|
+
const probeRowStyle = {
|
|
231
|
+
display: "flex",
|
|
232
|
+
alignItems: "center",
|
|
233
|
+
justifyContent: "space-between",
|
|
234
|
+
gap: 12
|
|
235
|
+
};
|
|
236
|
+
const probeRowEndStyle = {
|
|
237
|
+
display: "inline-flex",
|
|
238
|
+
alignItems: "center",
|
|
239
|
+
gap: 8,
|
|
240
|
+
flex: "0 0 auto"
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Tab strip for the card body. Kept visually light — a full pill would compete
|
|
244
|
+
* with the section headings, and the card is already the densest surface the
|
|
245
|
+
* plugin owns.
|
|
246
|
+
*/
|
|
247
|
+
const tabBarStyle = {
|
|
248
|
+
display: "flex",
|
|
249
|
+
gap: 4,
|
|
250
|
+
marginTop: 4,
|
|
251
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)"
|
|
252
|
+
};
|
|
253
|
+
const tabStyle = {
|
|
254
|
+
padding: "6px 12px",
|
|
255
|
+
border: 0,
|
|
256
|
+
borderBottom: "2px solid transparent",
|
|
257
|
+
background: "transparent",
|
|
258
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
259
|
+
font: "inherit",
|
|
260
|
+
fontSize: 13,
|
|
261
|
+
lineHeight: "20px",
|
|
262
|
+
cursor: "pointer"
|
|
263
|
+
};
|
|
264
|
+
const tabActiveStyle = {
|
|
265
|
+
borderBottom: "2px solid var(--dsw-alias-brand-primary)",
|
|
266
|
+
color: "var(--dsw-alias-label-primary)",
|
|
267
|
+
fontWeight: 600
|
|
268
|
+
};
|
|
269
|
+
const tabPanelStyle = {
|
|
270
|
+
display: "flex",
|
|
271
|
+
flexDirection: "column",
|
|
272
|
+
gap: 18,
|
|
273
|
+
paddingTop: 16
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Primary action of the inline confirmation. Fill and text colour come from the
|
|
277
|
+
* theme as a pair: `brand-primary` is a light accent here, so pairing it with a
|
|
278
|
+
* hardcoded white would render white-on-white.
|
|
279
|
+
*/
|
|
280
|
+
const primaryButtonStyle$1 = {
|
|
281
|
+
...buttonStyle$1,
|
|
282
|
+
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
283
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
284
|
+
color: "var(--dsw-alias-label-primary-foreground)"
|
|
285
|
+
};
|
|
163
286
|
function progressFillStyle(percent) {
|
|
164
287
|
return {
|
|
165
288
|
width: `${Math.max(0, Math.min(100, percent))}%`,
|
|
@@ -239,18 +362,202 @@ window.__ModuleLoader__.load({
|
|
|
239
362
|
children: t("freeModel")
|
|
240
363
|
}) : null]
|
|
241
364
|
})]
|
|
242
|
-
}), model.credits === void 0 ?
|
|
365
|
+
}), model.credits === void 0 ? model.rateUnknown === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
366
|
+
style: modelRateStyle,
|
|
367
|
+
children: t("rateUnknown")
|
|
368
|
+
}) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
243
369
|
style: modelRateStyle,
|
|
244
370
|
children: t("rate", { rate: model.credits })
|
|
245
371
|
})]
|
|
246
372
|
});
|
|
247
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Context capacity, listed in full.
|
|
376
|
+
*
|
|
377
|
+
* Every model the upstream reports a capacity for, largest first. A one-line
|
|
378
|
+
* summary with the exceptions on hover was tried and rejected: capacity is
|
|
379
|
+
* reference data you scan by model, and hiding most of it behind a hover made
|
|
380
|
+
* the common case (a model you already have in mind) the hard one to look up.
|
|
381
|
+
*
|
|
382
|
+
* Purely a report of the upstream's own numbers. The plugin offers no tier
|
|
383
|
+
* picker: the CN catalog declares one capacity per model and publishes no
|
|
384
|
+
* alternatives, so a menu there would mean inventing client-side policy. The
|
|
385
|
+
* international document does declare alternatives (`supportedLengths`), and
|
|
386
|
+
* they are shown as a secondary figure rather than merged into one number —
|
|
387
|
+
* the default is the budget actually requested, while the larger value is a
|
|
388
|
+
* ceiling the upstream would accept.
|
|
389
|
+
*/
|
|
390
|
+
function ContextTable({ models, t }) {
|
|
391
|
+
const known = (models ?? []).filter((model) => model.contextWindow !== void 0).sort((a, b) => b.contextWindow - a.contextWindow);
|
|
392
|
+
if (known.length === 0) return null;
|
|
393
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
394
|
+
style: quotaListStyle,
|
|
395
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
396
|
+
style: quotaTitleStyle,
|
|
397
|
+
children: t("contextHeading")
|
|
398
|
+
}), known.map((model) => {
|
|
399
|
+
const capacity = model.contextWindow;
|
|
400
|
+
const alternative = model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
|
|
401
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
402
|
+
style: quotaLabelStyle,
|
|
403
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
404
|
+
style: modelOfferStyle,
|
|
405
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
406
|
+
style: { textAlign: "right" },
|
|
407
|
+
children: formatTokens(capacity)
|
|
408
|
+
}), alternative === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
409
|
+
style: modelRateStyle,
|
|
410
|
+
children: t("contextUpTo", { size: formatTokens(alternative) })
|
|
411
|
+
})]
|
|
412
|
+
})]
|
|
413
|
+
}, model.id);
|
|
414
|
+
})]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Compact token count for display: the catalog's own round numbers (`200000`,
|
|
419
|
+
* `1000000`) read better as `200K` / `1M`, and no precision is lost because
|
|
420
|
+
* these values are always whole thousands.
|
|
421
|
+
*/
|
|
422
|
+
function formatTokens(tokens) {
|
|
423
|
+
if (tokens >= 1e6 && tokens % 1e6 === 0) return `${tokens / 1e6}M`;
|
|
424
|
+
if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
|
|
425
|
+
return String(tokens);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Reasoning-effort detection section: consent switches, per-model detection,
|
|
429
|
+
* and the recorded observations.
|
|
430
|
+
*
|
|
431
|
+
* Two deliberate UX rules from the plan (§3.1, §3.2):
|
|
432
|
+
* - the confirmation is shown *before* any request, and its copy states the
|
|
433
|
+
* credit caveat;
|
|
434
|
+
* - a `non-validating` result is presented as an observation about the
|
|
435
|
+
* parameter ("this model does not check it"), never as a statement that a
|
|
436
|
+
* level is unsupported.
|
|
437
|
+
*/
|
|
438
|
+
function ProbeSection({ probe, t, onDetect, onClear, busy }) {
|
|
439
|
+
const [pending, setPending] = (0, react.useState)();
|
|
440
|
+
const [runningModel, setRunningModel] = (0, react.useState)();
|
|
441
|
+
(0, react.useEffect)(() => {
|
|
442
|
+
if (pending !== void 0 && !probe.candidates.includes(pending)) setPending(void 0);
|
|
443
|
+
}, [pending, probe.candidates]);
|
|
444
|
+
const runningArmed = (0, react.useRef)(false);
|
|
445
|
+
(0, react.useEffect)(() => {
|
|
446
|
+
if (runningModel === void 0) return;
|
|
447
|
+
if (busy || probe.running) {
|
|
448
|
+
runningArmed.current = true;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (!runningArmed.current) return;
|
|
452
|
+
runningArmed.current = false;
|
|
453
|
+
setRunningModel(void 0);
|
|
454
|
+
}, [
|
|
455
|
+
runningModel,
|
|
456
|
+
busy,
|
|
457
|
+
probe.running
|
|
458
|
+
]);
|
|
459
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
460
|
+
style: quotaListStyle,
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
463
|
+
style: quotaTitleStyle,
|
|
464
|
+
children: t("probeHeading")
|
|
465
|
+
}),
|
|
466
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
467
|
+
style: bodyStyle,
|
|
468
|
+
children: t("probeIntro")
|
|
469
|
+
}),
|
|
470
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
471
|
+
style: bodyStyle,
|
|
472
|
+
children: t("probeConsentHint")
|
|
473
|
+
}),
|
|
474
|
+
probe.running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
475
|
+
style: bodyStyle,
|
|
476
|
+
children: t("probeRunningGeneric")
|
|
477
|
+
}) : null,
|
|
478
|
+
probe.candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
479
|
+
style: bodyStyle,
|
|
480
|
+
children: t("probeResultEmpty")
|
|
481
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
482
|
+
style: quotaGroupStyle,
|
|
483
|
+
children: probe.candidates.map((id) => {
|
|
484
|
+
const result = probe.results.find((entry) => entry.id === id);
|
|
485
|
+
const name = result?.name ?? id;
|
|
486
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
487
|
+
style: modelOfferStyle,
|
|
488
|
+
children: [
|
|
489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
490
|
+
style: probeRowStyle,
|
|
491
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
492
|
+
style: probeRowEndStyle,
|
|
493
|
+
children: [result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
494
|
+
style: modelBadgeChipStyle,
|
|
495
|
+
children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : t(result.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown")
|
|
496
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
497
|
+
type: "button",
|
|
498
|
+
style: buttonStyle$1,
|
|
499
|
+
disabled: probe.running || busy,
|
|
500
|
+
onClick: () => {
|
|
501
|
+
setPending(id);
|
|
502
|
+
},
|
|
503
|
+
children: runningModel === id ? t("probeRunning", { model: id }) : t(result === void 0 ? "probeStart" : "probeRedetect")
|
|
504
|
+
})]
|
|
505
|
+
})]
|
|
506
|
+
}),
|
|
507
|
+
result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
508
|
+
style: modelRateStyle,
|
|
509
|
+
children: t("probeResultAt", { time: formatTime(result.probedAt) })
|
|
510
|
+
}),
|
|
511
|
+
pending === id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
512
|
+
style: confirmBoxStyle,
|
|
513
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
514
|
+
style: bodyStyle,
|
|
515
|
+
children: t("probeConfirmBody", { model: name })
|
|
516
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
517
|
+
style: confirmRowStyle$1,
|
|
518
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
519
|
+
type: "button",
|
|
520
|
+
style: buttonStyle$1,
|
|
521
|
+
onClick: () => {
|
|
522
|
+
setPending(void 0);
|
|
523
|
+
},
|
|
524
|
+
children: t("cancel")
|
|
525
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
526
|
+
type: "button",
|
|
527
|
+
style: primaryButtonStyle$1,
|
|
528
|
+
disabled: probe.running || busy,
|
|
529
|
+
onClick: () => {
|
|
530
|
+
setRunningModel(id);
|
|
531
|
+
setPending(void 0);
|
|
532
|
+
onDetect(id);
|
|
533
|
+
},
|
|
534
|
+
children: t("probeConfirmAction")
|
|
535
|
+
})]
|
|
536
|
+
})]
|
|
537
|
+
}) : null
|
|
538
|
+
]
|
|
539
|
+
}, id);
|
|
540
|
+
})
|
|
541
|
+
}),
|
|
542
|
+
probe.results.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
543
|
+
type: "button",
|
|
544
|
+
style: buttonStyle$1,
|
|
545
|
+
disabled: busy,
|
|
546
|
+
onClick: () => {
|
|
547
|
+
onClear();
|
|
548
|
+
},
|
|
549
|
+
children: t("probeClear")
|
|
550
|
+
})
|
|
551
|
+
]
|
|
552
|
+
});
|
|
553
|
+
}
|
|
248
554
|
/** Render WorkBuddy sign-in state and credit as one expandable card. */
|
|
249
|
-
function WorkBuddyPluginCard({ t }) {
|
|
555
|
+
function WorkBuddyPluginCard({ t, variant = CN_CARD_VARIANT }) {
|
|
250
556
|
if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
|
|
251
557
|
const [open, setOpen] = (0, react.useState)(false);
|
|
252
558
|
const [status, setStatus] = (0, react.useState)({ status: "signed-out" });
|
|
253
559
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
560
|
+
const [tab, setTab] = (0, react.useState)("status");
|
|
254
561
|
const mounted = (0, react.useRef)(true);
|
|
255
562
|
(0, react.useEffect)(() => {
|
|
256
563
|
mounted.current = true;
|
|
@@ -260,7 +567,7 @@ window.__ModuleLoader__.load({
|
|
|
260
567
|
}, []);
|
|
261
568
|
const refresh = (0, react.useCallback)(async (signal) => {
|
|
262
569
|
try {
|
|
263
|
-
const response = await fetch(
|
|
570
|
+
const response = await fetch(variant.statusPath, {
|
|
264
571
|
headers: { accept: "application/json" },
|
|
265
572
|
credentials: "same-origin",
|
|
266
573
|
...signal === void 0 ? {} : { signal }
|
|
@@ -274,7 +581,7 @@ window.__ModuleLoader__.load({
|
|
|
274
581
|
message: error instanceof Error ? error.message : t("requestFailed")
|
|
275
582
|
});
|
|
276
583
|
}
|
|
277
|
-
}, [t]);
|
|
584
|
+
}, [t, variant.statusPath]);
|
|
278
585
|
(0, react.useEffect)(() => {
|
|
279
586
|
if (!open) return;
|
|
280
587
|
const controller = new AbortController();
|
|
@@ -306,8 +613,98 @@ window.__ModuleLoader__.load({
|
|
|
306
613
|
if (mounted.current) setBusy(false);
|
|
307
614
|
}
|
|
308
615
|
};
|
|
309
|
-
|
|
310
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Ask the host to re-read the credential and re-fetch this variant's catalog.
|
|
618
|
+
*
|
|
619
|
+
* Shares the probe route's key and guards: it is a write that spends an
|
|
620
|
+
* upstream request, so it does not belong on the read-only status GET. A
|
|
621
|
+
* failure is surfaced through the refreshed document's `catalog.error` rather
|
|
622
|
+
* than thrown away, so the reason survives the round trip.
|
|
623
|
+
*/
|
|
624
|
+
const refreshModels = (0, react.useCallback)(async () => {
|
|
625
|
+
const key = status.status === "signed-in" ? status.probeKey : void 0;
|
|
626
|
+
if (key === void 0) return;
|
|
627
|
+
setBusy(true);
|
|
628
|
+
try {
|
|
629
|
+
const response = await fetch(variant.probePath, {
|
|
630
|
+
method: "POST",
|
|
631
|
+
headers: {
|
|
632
|
+
"Content-Type": "application/json",
|
|
633
|
+
"X-WorkBuddy-Probe-Key": key
|
|
634
|
+
},
|
|
635
|
+
credentials: "same-origin",
|
|
636
|
+
body: JSON.stringify({ action: "refresh" })
|
|
637
|
+
});
|
|
638
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
if (mounted.current) setStatus((previous) => ({
|
|
641
|
+
status: "error",
|
|
642
|
+
message: error instanceof Error ? error.message : t("requestFailed")
|
|
643
|
+
}));
|
|
644
|
+
return;
|
|
645
|
+
} finally {
|
|
646
|
+
if (mounted.current) setBusy(false);
|
|
647
|
+
}
|
|
648
|
+
await refresh();
|
|
649
|
+
}, [
|
|
650
|
+
refresh,
|
|
651
|
+
status,
|
|
652
|
+
t,
|
|
653
|
+
variant.probePath
|
|
654
|
+
]);
|
|
655
|
+
/**
|
|
656
|
+
* Run one control action and refresh the card's state afterwards.
|
|
657
|
+
*
|
|
658
|
+
* The key travels in a header, not the body: it authorizes the write, and
|
|
659
|
+
* the host never accepts a prompt, a sentinel, or a model outside its own
|
|
660
|
+
* catalog from here.
|
|
661
|
+
*/
|
|
662
|
+
const control = (0, react.useCallback)(async (action) => {
|
|
663
|
+
const key = status.status === "signed-in" ? status.probeKey : void 0;
|
|
664
|
+
if (key === void 0) return;
|
|
665
|
+
setBusy(true);
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch(variant.probePath, {
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: {
|
|
670
|
+
"Content-Type": "application/json",
|
|
671
|
+
"X-WorkBuddy-Probe-Key": key
|
|
672
|
+
},
|
|
673
|
+
credentials: "same-origin",
|
|
674
|
+
body: JSON.stringify(action)
|
|
675
|
+
});
|
|
676
|
+
const value = await response.json().catch(() => void 0);
|
|
677
|
+
if (!response.ok) {
|
|
678
|
+
const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
|
|
679
|
+
throw new Error(message);
|
|
680
|
+
}
|
|
681
|
+
await refresh();
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (mounted.current) setStatus((previous) => ({
|
|
684
|
+
status: "error",
|
|
685
|
+
message: error instanceof Error ? error.message : t("requestFailed")
|
|
686
|
+
}));
|
|
687
|
+
} finally {
|
|
688
|
+
if (mounted.current) setBusy(false);
|
|
689
|
+
}
|
|
690
|
+
}, [
|
|
691
|
+
refresh,
|
|
692
|
+
status,
|
|
693
|
+
t,
|
|
694
|
+
variant.probePath
|
|
695
|
+
]);
|
|
696
|
+
/**
|
|
697
|
+
* Start a detection. Confirmation happens inline in the section, so this is
|
|
698
|
+
* only ever called after the user has already agreed.
|
|
699
|
+
*/
|
|
700
|
+
const confirmDetect = (0, react.useCallback)((modelId) => {
|
|
701
|
+
control({
|
|
702
|
+
action: "probe",
|
|
703
|
+
model: modelId
|
|
704
|
+
});
|
|
705
|
+
}, [control]);
|
|
706
|
+
const title = t(variant.titleKey);
|
|
707
|
+
const label = status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).trimEnd().replace(/[::]$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
|
|
311
708
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
312
709
|
style: cardStyle,
|
|
313
710
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -325,7 +722,7 @@ window.__ModuleLoader__.load({
|
|
|
325
722
|
children: title
|
|
326
723
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
327
724
|
style: descriptionStyle,
|
|
328
|
-
children: t(
|
|
725
|
+
children: t(variant.introKey)
|
|
329
726
|
})]
|
|
330
727
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
331
728
|
"aria-hidden": "true",
|
|
@@ -353,7 +750,7 @@ window.__ModuleLoader__.load({
|
|
|
353
750
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
354
751
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
355
752
|
type: "button",
|
|
356
|
-
style: buttonStyle,
|
|
753
|
+
style: buttonStyle$1,
|
|
357
754
|
disabled: busy,
|
|
358
755
|
onClick: () => {
|
|
359
756
|
manualRefresh();
|
|
@@ -366,42 +763,110 @@ window.__ModuleLoader__.load({
|
|
|
366
763
|
style: bodyStyle,
|
|
367
764
|
children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
|
|
368
765
|
}),
|
|
369
|
-
status.
|
|
370
|
-
style:
|
|
371
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
372
|
-
style:
|
|
373
|
-
children: [
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
size: account.size,
|
|
384
|
-
t
|
|
385
|
-
}, `${account.packageName}-${String(index)}`))]
|
|
766
|
+
status.catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
767
|
+
style: rowStyle,
|
|
768
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
769
|
+
style: bodyStyle,
|
|
770
|
+
children: [status.catalog.source === "live" && status.catalog.fetchedAt !== void 0 ? t("catalogLive", { time: formatTime(status.catalog.fetchedAt) }) : status.catalog.source === "saved" && status.catalog.fetchedAt !== void 0 ? t("catalogSaved", { time: formatTime(status.catalog.fetchedAt) }) : t("catalogFallback"), status.catalog.appVersion === void 0 ? "" : ` · ${t("catalogAppVersion", { version: status.catalog.appVersion })}`]
|
|
771
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
772
|
+
type: "button",
|
|
773
|
+
style: buttonStyle$1,
|
|
774
|
+
disabled: busy,
|
|
775
|
+
onClick: () => {
|
|
776
|
+
refreshModels();
|
|
777
|
+
},
|
|
778
|
+
children: busy ? t("refreshingModels") : t("refreshModels")
|
|
779
|
+
})]
|
|
386
780
|
}),
|
|
387
|
-
status.
|
|
781
|
+
status.catalog?.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
388
782
|
style: errorStyle,
|
|
389
|
-
children: t("
|
|
783
|
+
children: t("catalogError", { message: status.catalog.error })
|
|
390
784
|
}),
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
786
|
+
role: "tablist",
|
|
787
|
+
style: tabBarStyle,
|
|
788
|
+
children: [
|
|
789
|
+
"status",
|
|
790
|
+
"context",
|
|
791
|
+
"details"
|
|
792
|
+
].map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
793
|
+
type: "button",
|
|
794
|
+
role: "tab",
|
|
795
|
+
"aria-selected": tab === id,
|
|
796
|
+
onClick: () => {
|
|
797
|
+
setTab(id);
|
|
798
|
+
},
|
|
799
|
+
style: {
|
|
800
|
+
...tabStyle,
|
|
801
|
+
...tab === id ? tabActiveStyle : {}
|
|
802
|
+
},
|
|
803
|
+
children: t(id === "status" ? "tabStatus" : id === "context" ? "tabContext" : "tabDetails")
|
|
804
|
+
}, id))
|
|
805
|
+
}),
|
|
806
|
+
tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
807
|
+
style: tabPanelStyle,
|
|
808
|
+
children: [
|
|
809
|
+
status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
810
|
+
style: quotaListStyle,
|
|
811
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
812
|
+
style: rowStyle,
|
|
813
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
814
|
+
style: quotaTitleStyle,
|
|
815
|
+
children: t("creditsHeading")
|
|
816
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
817
|
+
style: bodyStyle,
|
|
818
|
+
children: t("creditsTotal", { total: formatNumber(status.credits.total) })
|
|
819
|
+
})]
|
|
820
|
+
})
|
|
821
|
+
}),
|
|
822
|
+
status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
823
|
+
style: errorStyle,
|
|
824
|
+
children: t("creditsError", { message: status.creditsError })
|
|
825
|
+
}),
|
|
826
|
+
status.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeSection, {
|
|
827
|
+
probe: status.probe,
|
|
828
|
+
t,
|
|
829
|
+
busy,
|
|
830
|
+
onDetect: confirmDetect,
|
|
831
|
+
onClear: () => {
|
|
832
|
+
control({ action: "clear" });
|
|
833
|
+
}
|
|
834
|
+
})
|
|
835
|
+
]
|
|
836
|
+
}) : tab === "context" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
837
|
+
style: tabPanelStyle,
|
|
838
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
|
|
839
|
+
models: status.models,
|
|
398
840
|
t
|
|
399
|
-
}
|
|
841
|
+
})
|
|
842
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
843
|
+
style: tabPanelStyle,
|
|
844
|
+
children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
845
|
+
style: quotaListStyle,
|
|
846
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
847
|
+
style: quotaTitleStyle,
|
|
848
|
+
children: t("creditsDetailHeading")
|
|
849
|
+
}), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
|
|
850
|
+
label: account.packageName,
|
|
851
|
+
remain: account.remain,
|
|
852
|
+
size: account.size,
|
|
853
|
+
t
|
|
854
|
+
}, `${account.packageName}-${String(index)}`))]
|
|
855
|
+
}), status.models === void 0 || status.models.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
856
|
+
style: quotaListStyle,
|
|
857
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
858
|
+
style: quotaTitleStyle,
|
|
859
|
+
children: t("modelsHeading")
|
|
860
|
+
}), status.models.filter((model) => model.free === true || (model.badges?.length ?? 0) > 0).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOfferRow, {
|
|
861
|
+
model,
|
|
862
|
+
t
|
|
863
|
+
}, model.id))]
|
|
864
|
+
})]
|
|
400
865
|
})
|
|
401
866
|
] }) : null,
|
|
402
867
|
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
403
|
-
style: bodyStyle,
|
|
404
|
-
children: t(
|
|
868
|
+
style: status.reason === void 0 ? bodyStyle : errorStyle,
|
|
869
|
+
children: status.reason ?? t(variant.signedOutKey)
|
|
405
870
|
}) : null,
|
|
406
871
|
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
407
872
|
style: errorStyle,
|
|
@@ -412,19 +877,441 @@ window.__ModuleLoader__.load({
|
|
|
412
877
|
});
|
|
413
878
|
}
|
|
414
879
|
//#endregion
|
|
880
|
+
//#region src/client/WorkBuddyProbeControl.tsx
|
|
881
|
+
/**
|
|
882
|
+
* Per-model reasoning-effort entry beside the Composer's model selector.
|
|
883
|
+
*
|
|
884
|
+
* Interaction follows the Fast Mode control `dsh-codex-connect` ships in this
|
|
885
|
+
* same seat, which is the established shape for composer chrome here:
|
|
886
|
+
*
|
|
887
|
+
* - a **static inline label** next to the icon names the feature ("Reasoning
|
|
888
|
+
* levels"), set smaller and dimmer than the surrounding chrome so it reads as
|
|
889
|
+
* an annotation on the icon. It never carries state: the verified levels
|
|
890
|
+
* already appear in the model dropdown (the adapter exposes them as
|
|
891
|
+
* selectable efforts), so repeating them here would duplicate the real answer
|
|
892
|
+
* and make the label's width jump as results change.
|
|
893
|
+
* - a **hover/focus tooltip** carries the state and the click's purpose, the way
|
|
894
|
+
* Fast Mode's tooltip explains its current speed.
|
|
895
|
+
* - the **confirmation** is a small bubble anchored to the control, not a
|
|
896
|
+
* `window.confirm`. Probing spends real credit, so a confirmation stays — but
|
|
897
|
+
* it belongs next to the thing it acts on, sized to one line plus two small
|
|
898
|
+
* buttons.
|
|
899
|
+
*
|
|
900
|
+
* @module dsh-workbuddy-connect/client/probe-control
|
|
901
|
+
*/
|
|
902
|
+
/**
|
|
903
|
+
* The card (and therefore the routes) a selected provider belongs to.
|
|
904
|
+
*
|
|
905
|
+
* The control serves both WorkBuddy providers from one seat, so the provider id
|
|
906
|
+
* is what selects the status and probe endpoints. Returning `undefined` for any
|
|
907
|
+
* other provider is what keeps the icon off every non-WorkBuddy model.
|
|
908
|
+
*/
|
|
909
|
+
function cardVariantFor(provider) {
|
|
910
|
+
return CARD_VARIANTS.find((card) => card.id === provider);
|
|
911
|
+
}
|
|
912
|
+
/** How often the control re-checks state when the window regains focus. */
|
|
913
|
+
const RECONCILE_MS = 6e4;
|
|
914
|
+
const wrapperStyle = {
|
|
915
|
+
display: "inline-flex",
|
|
916
|
+
position: "relative",
|
|
917
|
+
alignItems: "center",
|
|
918
|
+
transform: "translateY(2px)",
|
|
919
|
+
marginRight: -8
|
|
920
|
+
};
|
|
921
|
+
const buttonStyle = {
|
|
922
|
+
display: "inline-flex",
|
|
923
|
+
alignItems: "center",
|
|
924
|
+
justifyContent: "center",
|
|
925
|
+
gap: 2,
|
|
926
|
+
height: 30,
|
|
927
|
+
padding: "0 6px",
|
|
928
|
+
border: 0,
|
|
929
|
+
borderRadius: 8,
|
|
930
|
+
background: "transparent",
|
|
931
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
932
|
+
font: "inherit",
|
|
933
|
+
whiteSpace: "nowrap",
|
|
934
|
+
cursor: "pointer"
|
|
935
|
+
};
|
|
936
|
+
/**
|
|
937
|
+
* The inline label. Smaller and dimmer than the surrounding chrome on purpose:
|
|
938
|
+
* it names the feature, so it should read as an annotation attached to the icon
|
|
939
|
+
* rather than compete with the adjacent model selector.
|
|
940
|
+
*/
|
|
941
|
+
const labelStyle = {
|
|
942
|
+
fontSize: 11,
|
|
943
|
+
lineHeight: "16px",
|
|
944
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
945
|
+
};
|
|
946
|
+
/** Tooltip bubble: the Fast Mode shape (nowrap, one line, above the control). */
|
|
947
|
+
const tooltipStyle = {
|
|
948
|
+
position: "absolute",
|
|
949
|
+
left: "50%",
|
|
950
|
+
bottom: "calc(100% + 8px)",
|
|
951
|
+
zIndex: 1e3,
|
|
952
|
+
transform: "translateX(-50%)",
|
|
953
|
+
padding: "4px 8px",
|
|
954
|
+
borderRadius: 6,
|
|
955
|
+
background: "var(--dsw-specific-tip, #1f2329)",
|
|
956
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
957
|
+
color: "var(--dsw-alias-label-primary, #fff)",
|
|
958
|
+
fontSize: 12,
|
|
959
|
+
lineHeight: "18px",
|
|
960
|
+
whiteSpace: "nowrap",
|
|
961
|
+
pointerEvents: "none"
|
|
962
|
+
};
|
|
963
|
+
/** Confirmation bubble: same anchor, but interactive and allowed to wrap. */
|
|
964
|
+
const confirmStyle = {
|
|
965
|
+
position: "absolute",
|
|
966
|
+
right: 0,
|
|
967
|
+
bottom: "calc(100% + 8px)",
|
|
968
|
+
zIndex: 1001,
|
|
969
|
+
display: "flex",
|
|
970
|
+
flexDirection: "column",
|
|
971
|
+
gap: 8,
|
|
972
|
+
width: 260,
|
|
973
|
+
padding: "10px 12px",
|
|
974
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
975
|
+
borderRadius: 8,
|
|
976
|
+
background: "var(--dsw-alias-bg-layer-1, #fff)",
|
|
977
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
978
|
+
color: "var(--dsw-alias-label-primary)",
|
|
979
|
+
fontSize: 12,
|
|
980
|
+
lineHeight: "18px"
|
|
981
|
+
};
|
|
982
|
+
const confirmRowStyle = {
|
|
983
|
+
display: "flex",
|
|
984
|
+
justifyContent: "flex-end",
|
|
985
|
+
gap: 8
|
|
986
|
+
};
|
|
987
|
+
const confirmButtonStyle = {
|
|
988
|
+
padding: "3px 10px",
|
|
989
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
990
|
+
borderRadius: 6,
|
|
991
|
+
background: "transparent",
|
|
992
|
+
color: "inherit",
|
|
993
|
+
font: "inherit",
|
|
994
|
+
fontSize: 12,
|
|
995
|
+
cursor: "pointer"
|
|
996
|
+
};
|
|
997
|
+
/**
|
|
998
|
+
* Primary action inside the confirmation bubble.
|
|
999
|
+
*
|
|
1000
|
+
* The fill and its text colour must come as a pair: `brand-primary` resolves to
|
|
1001
|
+
* a light accent in this theme, so hardcoding `color: #fff` on top of it renders
|
|
1002
|
+
* white-on-white. `button-primary-fill` + `label-primary-foreground` is the
|
|
1003
|
+
* theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
|
|
1004
|
+
* the same job.
|
|
1005
|
+
*/
|
|
1006
|
+
const primaryButtonStyle = {
|
|
1007
|
+
...confirmButtonStyle,
|
|
1008
|
+
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
1009
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
1010
|
+
color: "var(--dsw-alias-label-primary-foreground)"
|
|
1011
|
+
};
|
|
1012
|
+
/**
|
|
1013
|
+
* Result note: a single line + a dismiss button, anchored to the control's
|
|
1014
|
+
* right side. Smaller than the confirmation bubble because it carries an
|
|
1015
|
+
* *outcome*, not a *decision* — the work is done, the user only has to read
|
|
1016
|
+
* and dismiss.
|
|
1017
|
+
*/
|
|
1018
|
+
const noteStyle = {
|
|
1019
|
+
position: "absolute",
|
|
1020
|
+
right: 0,
|
|
1021
|
+
bottom: "calc(100% + 8px)",
|
|
1022
|
+
zIndex: 1001,
|
|
1023
|
+
display: "flex",
|
|
1024
|
+
alignItems: "center",
|
|
1025
|
+
gap: 12,
|
|
1026
|
+
padding: "6px 10px",
|
|
1027
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1028
|
+
borderRadius: 8,
|
|
1029
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
1030
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
1031
|
+
color: "var(--dsw-alias-label-primary)",
|
|
1032
|
+
fontSize: 12,
|
|
1033
|
+
lineHeight: "18px",
|
|
1034
|
+
whiteSpace: "nowrap"
|
|
1035
|
+
};
|
|
1036
|
+
/**
|
|
1037
|
+
* The note's dismiss action. Outlined rather than bare text: inside an already
|
|
1038
|
+
* bordered bubble, an unbordered word does not read as something you can click.
|
|
1039
|
+
* Matches the outlined pill convention the plugin's other secondary actions use.
|
|
1040
|
+
*/
|
|
1041
|
+
const noteDismissStyle = {
|
|
1042
|
+
padding: "2px 8px",
|
|
1043
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1044
|
+
borderRadius: 6,
|
|
1045
|
+
background: "transparent",
|
|
1046
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
1047
|
+
font: "inherit",
|
|
1048
|
+
fontSize: 12,
|
|
1049
|
+
lineHeight: "18px",
|
|
1050
|
+
cursor: "pointer"
|
|
1051
|
+
};
|
|
1052
|
+
/**
|
|
1053
|
+
* The feature's static inline label. Deliberately not a state readout — see the
|
|
1054
|
+
* module comment.
|
|
1055
|
+
*/
|
|
1056
|
+
function useLabel(t) {
|
|
1057
|
+
return t("probeLabel");
|
|
1058
|
+
}
|
|
1059
|
+
/** Pick the model's recorded observation out of the probe section. */
|
|
1060
|
+
function resultFor(status, model) {
|
|
1061
|
+
if (status.status !== "signed-in") return void 0;
|
|
1062
|
+
return status.probe?.results.find((result) => result.id === model);
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* The one-line tooltip: current state first, then what a click does — the same
|
|
1066
|
+
* two-part shape Fast Mode uses.
|
|
1067
|
+
*/
|
|
1068
|
+
function tooltipText(t, model, state) {
|
|
1069
|
+
if (state.busy) return t("probeRunning", { model });
|
|
1070
|
+
if (state.failed) return t("probeTooltipRetry");
|
|
1071
|
+
const result = state.result;
|
|
1072
|
+
if (result === void 0) return t("probeTooltipIdle", { model });
|
|
1073
|
+
if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
|
|
1074
|
+
if (result.validation === "non-validating") return t("probeTooltipNotValidating");
|
|
1075
|
+
return t("probeTooltipRetry");
|
|
1076
|
+
}
|
|
1077
|
+
/** Model-independent shell: resolves the selection, then delegates per model. */
|
|
1078
|
+
function WorkBuddyProbeControl({ directory, t }) {
|
|
1079
|
+
const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
|
|
1080
|
+
const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
1081
|
+
const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
|
|
1082
|
+
const card = selection === void 0 ? void 0 : cardVariantFor(selection.provider);
|
|
1083
|
+
const key = card === void 0 || selection === void 0 ? void 0 : `${card.id}:${selection.model}`;
|
|
1084
|
+
return card === void 0 || selection === void 0 || key === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
|
|
1085
|
+
model: selection.model,
|
|
1086
|
+
card,
|
|
1087
|
+
label: useLabel(t),
|
|
1088
|
+
t
|
|
1089
|
+
}, key);
|
|
1090
|
+
}
|
|
1091
|
+
function ModelProbe({ model, card, label, t }) {
|
|
1092
|
+
const [status, setStatus] = (0, react.useState)();
|
|
1093
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1094
|
+
const [confirming, setConfirming] = (0, react.useState)(false);
|
|
1095
|
+
const [tooltipVisible, setTooltipVisible] = (0, react.useState)(false);
|
|
1096
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
1097
|
+
const [note, setNote] = (0, react.useState)();
|
|
1098
|
+
const inFlight = (0, react.useRef)(false);
|
|
1099
|
+
const mounted = (0, react.useRef)(false);
|
|
1100
|
+
const tooltipId = (0, react.useId)();
|
|
1101
|
+
const refresh = (0, react.useCallback)(async (signal) => {
|
|
1102
|
+
const response = await fetch(card.statusPath, {
|
|
1103
|
+
credentials: "same-origin",
|
|
1104
|
+
headers: { accept: "application/json" },
|
|
1105
|
+
...signal === void 0 ? {} : { signal }
|
|
1106
|
+
});
|
|
1107
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1108
|
+
const value = await response.json();
|
|
1109
|
+
if (mounted.current && !signal?.aborted) setStatus(value);
|
|
1110
|
+
}, [card.statusPath]);
|
|
1111
|
+
(0, react.useEffect)(() => {
|
|
1112
|
+
mounted.current = true;
|
|
1113
|
+
const controller = new AbortController();
|
|
1114
|
+
const load = () => {
|
|
1115
|
+
refresh(controller.signal).catch(() => {});
|
|
1116
|
+
};
|
|
1117
|
+
load();
|
|
1118
|
+
const timer = window.setInterval(load, RECONCILE_MS);
|
|
1119
|
+
window.addEventListener("focus", load);
|
|
1120
|
+
return () => {
|
|
1121
|
+
mounted.current = false;
|
|
1122
|
+
controller.abort();
|
|
1123
|
+
window.clearInterval(timer);
|
|
1124
|
+
window.removeEventListener("focus", load);
|
|
1125
|
+
};
|
|
1126
|
+
}, [refresh]);
|
|
1127
|
+
const probe = status?.status === "signed-in" ? status.probe : void 0;
|
|
1128
|
+
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
1129
|
+
const result = status === void 0 ? void 0 : resultFor(status, model);
|
|
1130
|
+
const visible = probe?.candidates.includes(model) === true || result !== void 0;
|
|
1131
|
+
(0, react.useEffect)(() => {
|
|
1132
|
+
setConfirming(false);
|
|
1133
|
+
setNote(void 0);
|
|
1134
|
+
}, [model]);
|
|
1135
|
+
const detect = async () => {
|
|
1136
|
+
if (key === void 0 || inFlight.current || probe?.running === true) return;
|
|
1137
|
+
inFlight.current = true;
|
|
1138
|
+
setNote(void 0);
|
|
1139
|
+
setConfirming(false);
|
|
1140
|
+
setBusy(true);
|
|
1141
|
+
setFailed(false);
|
|
1142
|
+
try {
|
|
1143
|
+
const response = await fetch(card.probePath, {
|
|
1144
|
+
method: "POST",
|
|
1145
|
+
credentials: "same-origin",
|
|
1146
|
+
headers: {
|
|
1147
|
+
"Content-Type": "application/json",
|
|
1148
|
+
"X-WorkBuddy-Probe-Key": key
|
|
1149
|
+
},
|
|
1150
|
+
body: JSON.stringify({
|
|
1151
|
+
action: "probe",
|
|
1152
|
+
model
|
|
1153
|
+
})
|
|
1154
|
+
});
|
|
1155
|
+
const body = await response.json();
|
|
1156
|
+
if (!response.ok || body.state !== "ok" || body.validation !== "validating" && body.validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
|
|
1157
|
+
if (mounted.current) {
|
|
1158
|
+
const completed = {
|
|
1159
|
+
id: model,
|
|
1160
|
+
name: model,
|
|
1161
|
+
validation: body.validation,
|
|
1162
|
+
efforts: body.efforts,
|
|
1163
|
+
probedAt: Date.now()
|
|
1164
|
+
};
|
|
1165
|
+
setNote(completed);
|
|
1166
|
+
}
|
|
1167
|
+
refresh().catch(() => {});
|
|
1168
|
+
} catch {
|
|
1169
|
+
if (mounted.current) setFailed(true);
|
|
1170
|
+
} finally {
|
|
1171
|
+
inFlight.current = false;
|
|
1172
|
+
if (mounted.current) setBusy(false);
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
if (!visible) return null;
|
|
1176
|
+
const text = tooltipText(t, model, {
|
|
1177
|
+
busy,
|
|
1178
|
+
result,
|
|
1179
|
+
failed
|
|
1180
|
+
});
|
|
1181
|
+
const disabled = busy || probe?.running === true || key === void 0;
|
|
1182
|
+
const showTooltip = tooltipVisible && !confirming && note === void 0;
|
|
1183
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1184
|
+
style: wrapperStyle,
|
|
1185
|
+
onMouseEnter: () => {
|
|
1186
|
+
setTooltipVisible(true);
|
|
1187
|
+
},
|
|
1188
|
+
onMouseLeave: () => {
|
|
1189
|
+
setTooltipVisible(false);
|
|
1190
|
+
},
|
|
1191
|
+
children: [
|
|
1192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1193
|
+
type: "button",
|
|
1194
|
+
"aria-label": text,
|
|
1195
|
+
"aria-describedby": showTooltip ? tooltipId : void 0,
|
|
1196
|
+
"aria-busy": busy,
|
|
1197
|
+
"aria-expanded": confirming,
|
|
1198
|
+
disabled,
|
|
1199
|
+
onClick: () => {
|
|
1200
|
+
setConfirming(true);
|
|
1201
|
+
},
|
|
1202
|
+
onFocus: () => {
|
|
1203
|
+
setTooltipVisible(true);
|
|
1204
|
+
},
|
|
1205
|
+
onBlur: () => {
|
|
1206
|
+
setTooltipVisible(false);
|
|
1207
|
+
},
|
|
1208
|
+
style: {
|
|
1209
|
+
...buttonStyle,
|
|
1210
|
+
opacity: disabled && !confirming ? .6 : 1,
|
|
1211
|
+
cursor: disabled ? "default" : "pointer"
|
|
1212
|
+
},
|
|
1213
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1214
|
+
width: "16",
|
|
1215
|
+
height: "16",
|
|
1216
|
+
viewBox: "0 0 24 24",
|
|
1217
|
+
fill: "none",
|
|
1218
|
+
stroke: "currentColor",
|
|
1219
|
+
strokeWidth: "1.6",
|
|
1220
|
+
"aria-hidden": "true",
|
|
1221
|
+
focusable: "false",
|
|
1222
|
+
children: [
|
|
1223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1224
|
+
cx: "12",
|
|
1225
|
+
cy: "12",
|
|
1226
|
+
r: "9"
|
|
1227
|
+
}),
|
|
1228
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1229
|
+
cx: "12",
|
|
1230
|
+
cy: "12",
|
|
1231
|
+
r: "4"
|
|
1232
|
+
}),
|
|
1233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 12 20 4" }),
|
|
1234
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1235
|
+
cx: "12",
|
|
1236
|
+
cy: "12",
|
|
1237
|
+
r: "1"
|
|
1238
|
+
})
|
|
1239
|
+
]
|
|
1240
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1241
|
+
style: labelStyle,
|
|
1242
|
+
children: label
|
|
1243
|
+
})]
|
|
1244
|
+
}),
|
|
1245
|
+
showTooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1246
|
+
id: tooltipId,
|
|
1247
|
+
role: "tooltip",
|
|
1248
|
+
style: tooltipStyle,
|
|
1249
|
+
children: text
|
|
1250
|
+
}),
|
|
1251
|
+
confirming && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1252
|
+
style: confirmStyle,
|
|
1253
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeBubbleBody") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1254
|
+
style: confirmRowStyle,
|
|
1255
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1256
|
+
type: "button",
|
|
1257
|
+
style: confirmButtonStyle,
|
|
1258
|
+
onClick: () => {
|
|
1259
|
+
setConfirming(false);
|
|
1260
|
+
},
|
|
1261
|
+
children: t("cancel")
|
|
1262
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1263
|
+
type: "button",
|
|
1264
|
+
style: primaryButtonStyle,
|
|
1265
|
+
onClick: () => {
|
|
1266
|
+
detect();
|
|
1267
|
+
},
|
|
1268
|
+
children: t("probeConfirmAction")
|
|
1269
|
+
})]
|
|
1270
|
+
})]
|
|
1271
|
+
}),
|
|
1272
|
+
note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1273
|
+
role: "status",
|
|
1274
|
+
"aria-live": "polite",
|
|
1275
|
+
style: noteStyle,
|
|
1276
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: noteText(t, note) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1277
|
+
type: "button",
|
|
1278
|
+
style: noteDismissStyle,
|
|
1279
|
+
onClick: () => {
|
|
1280
|
+
setNote(void 0);
|
|
1281
|
+
},
|
|
1282
|
+
children: t("probeNoteDismiss")
|
|
1283
|
+
})]
|
|
1284
|
+
})
|
|
1285
|
+
]
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
/** Compose the one-line outcome string the note bubble shows. */
|
|
1289
|
+
function noteText(t, result) {
|
|
1290
|
+
if (result.validation === "validating" && result.efforts.length > 0) return t("probeNoteVerified", { levels: result.efforts.join(" / ") });
|
|
1291
|
+
if (result.validation === "non-validating") return t("probeNoteNotValidating");
|
|
1292
|
+
return t("probeNoteUnknown");
|
|
1293
|
+
}
|
|
1294
|
+
//#endregion
|
|
415
1295
|
//#region src/client/locales.ts
|
|
416
1296
|
/** Plugin-card copy registered under the settings.workbuddy locale namespace. */
|
|
417
1297
|
const en = {
|
|
418
1298
|
title: "DSH WorkBuddy Connect",
|
|
419
1299
|
intro: "Use the models in the WorkBuddy desktop app directly in DSH — zero configuration, ready out of the box.",
|
|
1300
|
+
titleAI: "DSH WorkBuddy AI Connect",
|
|
1301
|
+
introAI: "Use the models in the WorkBuddy AI international desktop app directly in DSH — zero configuration, ready out of the box.",
|
|
420
1302
|
expand: "Expand",
|
|
421
1303
|
collapse: "Collapse",
|
|
422
1304
|
loading: "Loading account…",
|
|
423
1305
|
signedOut: "Not signed in",
|
|
424
1306
|
signedOutHint: "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
|
|
1307
|
+
signedOutHintAI: "Sign in once in the WorkBuddy AI desktop app; this plugin follows that sign-in automatically.",
|
|
425
1308
|
signedInAs: "Signed in as {nickname}",
|
|
426
1309
|
accessTokenExpires: "Access token expires {time} (refresh is automatic)",
|
|
427
1310
|
creditsHeading: "Remaining credit",
|
|
1311
|
+
tabStatus: "Status",
|
|
1312
|
+
tabContext: "Context window",
|
|
1313
|
+
tabDetails: "Credit details",
|
|
1314
|
+
creditsDetailHeading: "By package",
|
|
428
1315
|
creditsTotal: "Total: {total}",
|
|
429
1316
|
percentRemaining: "{percent}% remaining",
|
|
430
1317
|
exactRemaining: "{remain} / {size} remaining",
|
|
@@ -432,25 +1319,72 @@ window.__ModuleLoader__.load({
|
|
|
432
1319
|
creditsError: "Credit unavailable: {message}",
|
|
433
1320
|
refresh: "Refresh",
|
|
434
1321
|
refreshing: "Refreshing…",
|
|
1322
|
+
refreshModels: "Refresh model list",
|
|
1323
|
+
refreshingModels: "Refreshing models…",
|
|
1324
|
+
catalogLive: "Model list updated {time}",
|
|
1325
|
+
catalogSaved: "Showing the saved model list from {time}",
|
|
1326
|
+
catalogFallback: "Showing the built-in model list (not yet updated from WorkBuddy)",
|
|
1327
|
+
catalogError: "Last update failed: {message}",
|
|
1328
|
+
catalogAppVersion: "App version {version}",
|
|
435
1329
|
requestFailed: "Request failed",
|
|
436
1330
|
accountHeading: "Account",
|
|
437
1331
|
modelsHeading: "Model offers",
|
|
1332
|
+
contextHeading: "Context window",
|
|
1333
|
+
contextUpTo: "up to {size}",
|
|
438
1334
|
freeModel: "Free",
|
|
439
1335
|
badgeLimitedFree: "Limited-time free",
|
|
440
1336
|
badgeNightDiscount: "Night discount",
|
|
441
|
-
|
|
1337
|
+
badgeFreeNow: "Free now",
|
|
1338
|
+
rate: "{rate} credits per message",
|
|
1339
|
+
rateUnknown: "Price unavailable — refresh to update",
|
|
1340
|
+
probeLabel: "Reasoning levels",
|
|
1341
|
+
probeTooltipIdle: "Detect the reasoning levels {model} accepts",
|
|
1342
|
+
probeTooltipVerified: "Accepted levels: {levels} · click to detect again",
|
|
1343
|
+
probeTooltipNotValidating: "This model does not check the effort parameter",
|
|
1344
|
+
probeTooltipRetry: "Detection did not complete · click to retry",
|
|
1345
|
+
probeBubbleBody: "Send test requests to confirm the available reasoning levels. May consume a small amount of credit.",
|
|
1346
|
+
probeConfirmAction: "Confirm",
|
|
1347
|
+
probeNoteVerified: "Detected: {levels}",
|
|
1348
|
+
probeNoteNotValidating: "This model does not check the effort parameter",
|
|
1349
|
+
probeNoteUnknown: "Detection did not complete",
|
|
1350
|
+
probeNoteDismiss: "Got it",
|
|
1351
|
+
probeHeading: "Reasoning effort detection",
|
|
1352
|
+
probeResultNoLevels: "No tested levels were accepted.",
|
|
1353
|
+
probeIntro: "Some models reason but declare no selectable effort levels. Detecting which levels a model accepts sends a few real requests that may consume credit.",
|
|
1354
|
+
probeConsentHint: "Each detection sends test requests to one model to confirm its available reasoning levels, and may consume a small amount of credit.",
|
|
1355
|
+
probeStart: "Detect",
|
|
1356
|
+
probeRedetect: "Detect again",
|
|
1357
|
+
probeRunning: "Detecting {model}…",
|
|
1358
|
+
probeRunningGeneric: "Detecting…",
|
|
1359
|
+
probeClear: "Clear detected results",
|
|
1360
|
+
probeCandidates: "Detectable models: {count}",
|
|
1361
|
+
probeConfirmBody: "Send test requests to {model} to confirm its available reasoning levels. May consume a small amount of credit.",
|
|
1362
|
+
cancel: "Cancel",
|
|
1363
|
+
probeResultVerified: "Verified levels: {levels}",
|
|
1364
|
+
probeResultNotValidating: "This model does not check the effort parameter",
|
|
1365
|
+
probeResultUnknown: "Detection did not complete",
|
|
1366
|
+
probeResultAt: "Detected {time}",
|
|
1367
|
+
probeResultEmpty: "No detectable models right now.",
|
|
1368
|
+
probeFailed: "Detection failed: {message}"
|
|
442
1369
|
};
|
|
443
1370
|
const zh = {
|
|
444
1371
|
title: "DSH WorkBuddy Connect",
|
|
445
1372
|
intro: "在 DSH 中直接使用 WorkBuddy 桌面 App 包含的模型,开箱即用,无需额外配置。",
|
|
1373
|
+
titleAI: "DSH WorkBuddy AI Connect",
|
|
1374
|
+
introAI: "在 DSH 中直接使用 WorkBuddy AI 国际版桌面 App 包含的模型,开箱即用,无需额外配置。",
|
|
446
1375
|
expand: "展开",
|
|
447
1376
|
collapse: "收起",
|
|
448
1377
|
loading: "正在读取账号…",
|
|
449
1378
|
signedOut: "未登录",
|
|
450
1379
|
signedOutHint: "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
1380
|
+
signedOutHintAI: "在 WorkBuddy AI 国际版桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
451
1381
|
signedInAs: "已登录:{nickname}",
|
|
452
1382
|
accessTokenExpires: "访问令牌 {time} 过期(自动续期)",
|
|
453
1383
|
creditsHeading: "剩余积分",
|
|
1384
|
+
tabStatus: "状态",
|
|
1385
|
+
tabContext: "上下文窗口",
|
|
1386
|
+
tabDetails: "积分详情",
|
|
1387
|
+
creditsDetailHeading: "按套餐",
|
|
454
1388
|
creditsTotal: "合计:{total}",
|
|
455
1389
|
percentRemaining: "剩余 {percent}%",
|
|
456
1390
|
exactRemaining: "剩余 {remain} / {size}",
|
|
@@ -458,13 +1392,53 @@ window.__ModuleLoader__.load({
|
|
|
458
1392
|
creditsError: "积分查询失败:{message}",
|
|
459
1393
|
refresh: "刷新",
|
|
460
1394
|
refreshing: "正在刷新…",
|
|
1395
|
+
refreshModels: "刷新模型列表",
|
|
1396
|
+
refreshingModels: "正在刷新模型…",
|
|
1397
|
+
catalogLive: "模型列表更新于 {time}",
|
|
1398
|
+
catalogSaved: "当前显示已保存的模型列表,更新于 {time}",
|
|
1399
|
+
catalogFallback: "当前显示内置模型列表(尚未从 WorkBuddy 更新)",
|
|
1400
|
+
catalogError: "上次更新失败:{message}",
|
|
1401
|
+
catalogAppVersion: "App 版本 {version}",
|
|
461
1402
|
requestFailed: "请求失败",
|
|
462
1403
|
accountHeading: "账号",
|
|
463
1404
|
modelsHeading: "模型优惠",
|
|
1405
|
+
contextHeading: "上下文窗口",
|
|
1406
|
+
contextUpTo: "最高 {size}",
|
|
464
1407
|
freeModel: "免费",
|
|
465
1408
|
badgeLimitedFree: "限时免费",
|
|
466
1409
|
badgeNightDiscount: "夜间折扣",
|
|
467
|
-
|
|
1410
|
+
badgeFreeNow: "限时免费",
|
|
1411
|
+
rate: "{rate} 积分/次",
|
|
1412
|
+
rateUnknown: "价格未知 — 刷新后更新",
|
|
1413
|
+
probeLabel: "推理等级",
|
|
1414
|
+
probeTooltipIdle: "检测 {model} 可用的推理档位",
|
|
1415
|
+
probeTooltipVerified: "已接受:{levels} · 点击可重新检测",
|
|
1416
|
+
probeTooltipNotValidating: "该模型不校验该参数",
|
|
1417
|
+
probeTooltipRetry: "检测未完成 · 点击重试",
|
|
1418
|
+
probeBubbleBody: "发送探测请求以确认可用推理档位。可能消耗少量积分。",
|
|
1419
|
+
probeConfirmAction: "确认检测",
|
|
1420
|
+
probeNoteVerified: "已检测:{levels}",
|
|
1421
|
+
probeNoteNotValidating: "该模型不校验该参数",
|
|
1422
|
+
probeNoteUnknown: "检测未完成",
|
|
1423
|
+
probeNoteDismiss: "知道了",
|
|
1424
|
+
probeHeading: "推理档位检测",
|
|
1425
|
+
probeResultNoLevels: "本次测试的档位均未被接受。",
|
|
1426
|
+
probeIntro: "部分模型具备思考能力,但没有声明可选档位。检测会发送少量真实请求,可能消耗积分。",
|
|
1427
|
+
probeConsentHint: "每次检测会向该模型发送探测请求,以确认可用推理档位,可能消耗少量积分。",
|
|
1428
|
+
probeStart: "开始检测",
|
|
1429
|
+
probeRedetect: "重新检测",
|
|
1430
|
+
probeRunning: "正在检测 {model}…",
|
|
1431
|
+
probeRunningGeneric: "正在检测…",
|
|
1432
|
+
probeClear: "清除已探测结果",
|
|
1433
|
+
probeCandidates: "可检测模型:{count} 个",
|
|
1434
|
+
probeConfirmBody: "向 {model} 发送探测请求,以确认可用推理档位。可能消耗少量积分。",
|
|
1435
|
+
cancel: "取消",
|
|
1436
|
+
probeResultVerified: "已验证接受的档位:{levels}",
|
|
1437
|
+
probeResultNotValidating: "该模型不校验该参数",
|
|
1438
|
+
probeResultUnknown: "检测未完成",
|
|
1439
|
+
probeResultAt: "检测于 {time}",
|
|
1440
|
+
probeResultEmpty: "当前没有可检测的模型。",
|
|
1441
|
+
probeFailed: "检测失败:{message}"
|
|
468
1442
|
};
|
|
469
1443
|
//#endregion
|
|
470
1444
|
//#region src/client/index.tsx
|
|
@@ -509,12 +1483,26 @@ window.__ModuleLoader__.load({
|
|
|
509
1483
|
en
|
|
510
1484
|
}), "dsh-workbuddy-connect: settings copy");
|
|
511
1485
|
const t = ctx.locale.bind(namespace);
|
|
512
|
-
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
1486
|
+
for (const [index, variant] of CARD_VARIANTS.entries()) ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
513
1487
|
name: "settings.plugin.item",
|
|
514
|
-
key:
|
|
515
|
-
priority: 30,
|
|
516
|
-
inject: () => ({
|
|
1488
|
+
key: variant.id,
|
|
1489
|
+
priority: 30 - index,
|
|
1490
|
+
inject: () => ({
|
|
1491
|
+
t,
|
|
1492
|
+
variant
|
|
1493
|
+
})
|
|
517
1494
|
}, WorkBuddyPluginCard));
|
|
1495
|
+
ctx.inject(["modelDirectories"], (scope) => {
|
|
1496
|
+
scope.slots.inject("conversation.input.right", () => scope.slots.register({
|
|
1497
|
+
name: "conversation.input.right",
|
|
1498
|
+
id: "workbuddy-probe",
|
|
1499
|
+
order: 10,
|
|
1500
|
+
inject: (sessionId) => ({
|
|
1501
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
1502
|
+
t
|
|
1503
|
+
})
|
|
1504
|
+
}, WorkBuddyProbeControl));
|
|
1505
|
+
});
|
|
518
1506
|
} catch (error) {
|
|
519
1507
|
console.error("[dsh-workbuddy-connect] client card failed to load (host provider unaffected):", error);
|
|
520
1508
|
}
|