dsh-workbuddy-connect 0.3.2 → 0.4.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 +17 -5
- package/README.md +18 -7
- package/lib/bin.js +1 -1
- package/lib/client.js +866 -31
- package/lib/{host-heartbeat-BSsKUoB8.js → host-heartbeat-0UAWqWac.js} +232 -3
- package/lib/index.d.ts +224 -1
- package/lib/index.js +563 -43
- package/package.json +11 -5
package/lib/client.js
CHANGED
|
@@ -10,6 +10,420 @@ 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
|
+
//#endregion
|
|
24
|
+
//#region src/client/WorkBuddyProbeControl.tsx
|
|
25
|
+
/**
|
|
26
|
+
* Per-model reasoning-effort entry beside the Composer's model selector.
|
|
27
|
+
*
|
|
28
|
+
* Interaction follows the Fast Mode control `dsh-codex-connect` ships in this
|
|
29
|
+
* same seat, which is the established shape for composer chrome here:
|
|
30
|
+
*
|
|
31
|
+
* - a **static inline label** next to the icon names the feature ("Reasoning
|
|
32
|
+
* levels"), set smaller and dimmer than the surrounding chrome so it reads as
|
|
33
|
+
* an annotation on the icon. It never carries state: the verified levels
|
|
34
|
+
* already appear in the model dropdown (the adapter exposes them as
|
|
35
|
+
* selectable efforts), so repeating them here would duplicate the real answer
|
|
36
|
+
* and make the label's width jump as results change.
|
|
37
|
+
* - a **hover/focus tooltip** carries the state and the click's purpose, the way
|
|
38
|
+
* Fast Mode's tooltip explains its current speed.
|
|
39
|
+
* - the **confirmation** is a small bubble anchored to the control, not a
|
|
40
|
+
* `window.confirm`. Probing spends real credit, so a confirmation stays — but
|
|
41
|
+
* it belongs next to the thing it acts on, sized to one line plus two small
|
|
42
|
+
* buttons.
|
|
43
|
+
*
|
|
44
|
+
* @module dsh-workbuddy-connect/client/probe-control
|
|
45
|
+
*/
|
|
46
|
+
/** How often the control re-checks state when the window regains focus. */
|
|
47
|
+
const RECONCILE_MS = 6e4;
|
|
48
|
+
const wrapperStyle = {
|
|
49
|
+
display: "inline-flex",
|
|
50
|
+
position: "relative",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
transform: "translateY(2px)",
|
|
53
|
+
marginRight: -8
|
|
54
|
+
};
|
|
55
|
+
const buttonStyle$1 = {
|
|
56
|
+
display: "inline-flex",
|
|
57
|
+
alignItems: "center",
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
gap: 2,
|
|
60
|
+
height: 30,
|
|
61
|
+
padding: "0 6px",
|
|
62
|
+
border: 0,
|
|
63
|
+
borderRadius: 8,
|
|
64
|
+
background: "transparent",
|
|
65
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
66
|
+
font: "inherit",
|
|
67
|
+
whiteSpace: "nowrap",
|
|
68
|
+
cursor: "pointer"
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The inline label. Smaller and dimmer than the surrounding chrome on purpose:
|
|
72
|
+
* it names the feature, so it should read as an annotation attached to the icon
|
|
73
|
+
* rather than compete with the adjacent model selector.
|
|
74
|
+
*/
|
|
75
|
+
const labelStyle = {
|
|
76
|
+
fontSize: 11,
|
|
77
|
+
lineHeight: "16px",
|
|
78
|
+
color: "var(--dsw-alias-label-tertiary)"
|
|
79
|
+
};
|
|
80
|
+
/** Tooltip bubble: the Fast Mode shape (nowrap, one line, above the control). */
|
|
81
|
+
const tooltipStyle = {
|
|
82
|
+
position: "absolute",
|
|
83
|
+
left: "50%",
|
|
84
|
+
bottom: "calc(100% + 8px)",
|
|
85
|
+
zIndex: 1e3,
|
|
86
|
+
transform: "translateX(-50%)",
|
|
87
|
+
padding: "4px 8px",
|
|
88
|
+
borderRadius: 6,
|
|
89
|
+
background: "var(--dsw-specific-tip, #1f2329)",
|
|
90
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
91
|
+
color: "var(--dsw-alias-label-primary, #fff)",
|
|
92
|
+
fontSize: 12,
|
|
93
|
+
lineHeight: "18px",
|
|
94
|
+
whiteSpace: "nowrap",
|
|
95
|
+
pointerEvents: "none"
|
|
96
|
+
};
|
|
97
|
+
/** Confirmation bubble: same anchor, but interactive and allowed to wrap. */
|
|
98
|
+
const confirmStyle = {
|
|
99
|
+
position: "absolute",
|
|
100
|
+
right: 0,
|
|
101
|
+
bottom: "calc(100% + 8px)",
|
|
102
|
+
zIndex: 1001,
|
|
103
|
+
display: "flex",
|
|
104
|
+
flexDirection: "column",
|
|
105
|
+
gap: 8,
|
|
106
|
+
width: 260,
|
|
107
|
+
padding: "10px 12px",
|
|
108
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
109
|
+
borderRadius: 8,
|
|
110
|
+
background: "var(--dsw-alias-bg-layer-1, #fff)",
|
|
111
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
112
|
+
color: "var(--dsw-alias-label-primary)",
|
|
113
|
+
fontSize: 12,
|
|
114
|
+
lineHeight: "18px"
|
|
115
|
+
};
|
|
116
|
+
const confirmRowStyle$1 = {
|
|
117
|
+
display: "flex",
|
|
118
|
+
justifyContent: "flex-end",
|
|
119
|
+
gap: 8
|
|
120
|
+
};
|
|
121
|
+
const confirmButtonStyle = {
|
|
122
|
+
padding: "3px 10px",
|
|
123
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
124
|
+
borderRadius: 6,
|
|
125
|
+
background: "transparent",
|
|
126
|
+
color: "inherit",
|
|
127
|
+
font: "inherit",
|
|
128
|
+
fontSize: 12,
|
|
129
|
+
cursor: "pointer"
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Primary action inside the confirmation bubble.
|
|
133
|
+
*
|
|
134
|
+
* The fill and its text colour must come as a pair: `brand-primary` resolves to
|
|
135
|
+
* a light accent in this theme, so hardcoding `color: #fff` on top of it renders
|
|
136
|
+
* white-on-white. `button-primary-fill` + `label-primary-foreground` is the
|
|
137
|
+
* theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
|
|
138
|
+
* the same job.
|
|
139
|
+
*/
|
|
140
|
+
const primaryButtonStyle$1 = {
|
|
141
|
+
...confirmButtonStyle,
|
|
142
|
+
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
143
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
144
|
+
color: "var(--dsw-alias-label-primary-foreground)"
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Result note: a single line + a dismiss button, anchored to the control's
|
|
148
|
+
* right side. Smaller than the confirmation bubble because it carries an
|
|
149
|
+
* *outcome*, not a *decision* — the work is done, the user only has to read
|
|
150
|
+
* and dismiss.
|
|
151
|
+
*/
|
|
152
|
+
const noteStyle = {
|
|
153
|
+
position: "absolute",
|
|
154
|
+
right: 0,
|
|
155
|
+
bottom: "calc(100% + 8px)",
|
|
156
|
+
zIndex: 1001,
|
|
157
|
+
display: "flex",
|
|
158
|
+
alignItems: "center",
|
|
159
|
+
gap: 12,
|
|
160
|
+
padding: "6px 10px",
|
|
161
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
162
|
+
borderRadius: 8,
|
|
163
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
164
|
+
boxShadow: "var(--dsw-shadow-lv2)",
|
|
165
|
+
color: "var(--dsw-alias-label-primary)",
|
|
166
|
+
fontSize: 12,
|
|
167
|
+
lineHeight: "18px",
|
|
168
|
+
whiteSpace: "nowrap"
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* The note's dismiss action. Outlined rather than bare text: inside an already
|
|
172
|
+
* bordered bubble, an unbordered word does not read as something you can click.
|
|
173
|
+
* Matches the outlined pill convention the plugin's other secondary actions use.
|
|
174
|
+
*/
|
|
175
|
+
const noteDismissStyle = {
|
|
176
|
+
padding: "2px 8px",
|
|
177
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
178
|
+
borderRadius: 6,
|
|
179
|
+
background: "transparent",
|
|
180
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
181
|
+
font: "inherit",
|
|
182
|
+
fontSize: 12,
|
|
183
|
+
lineHeight: "18px",
|
|
184
|
+
cursor: "pointer"
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* The feature's static inline label. Deliberately not a state readout — see the
|
|
188
|
+
* module comment.
|
|
189
|
+
*/
|
|
190
|
+
function useLabel(t) {
|
|
191
|
+
return t("probeLabel");
|
|
192
|
+
}
|
|
193
|
+
/** Pick the model's recorded observation out of the probe section. */
|
|
194
|
+
function resultFor(status, model) {
|
|
195
|
+
if (status.status !== "signed-in") return void 0;
|
|
196
|
+
return status.probe?.results.find((result) => result.id === model);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The one-line tooltip: current state first, then what a click does — the same
|
|
200
|
+
* two-part shape Fast Mode uses.
|
|
201
|
+
*/
|
|
202
|
+
function tooltipText(t, model, state) {
|
|
203
|
+
if (state.busy) return t("probeRunning", { model });
|
|
204
|
+
if (state.failed) return t("probeTooltipRetry");
|
|
205
|
+
const result = state.result;
|
|
206
|
+
if (result === void 0) return t("probeTooltipIdle", { model });
|
|
207
|
+
if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
|
|
208
|
+
if (result.validation === "non-validating") return t("probeTooltipNotValidating");
|
|
209
|
+
return t("probeTooltipRetry");
|
|
210
|
+
}
|
|
211
|
+
/** Model-independent shell: resolves the selection, then delegates per model. */
|
|
212
|
+
function WorkBuddyProbeControl({ directory, t }) {
|
|
213
|
+
const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
|
|
214
|
+
const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
215
|
+
const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
|
|
216
|
+
const model = selection?.provider === "workbuddy" ? selection.model : void 0;
|
|
217
|
+
const label = useLabel(t);
|
|
218
|
+
return model === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
|
|
219
|
+
model,
|
|
220
|
+
label,
|
|
221
|
+
t
|
|
222
|
+
}, model);
|
|
223
|
+
}
|
|
224
|
+
function ModelProbe({ model, label, t }) {
|
|
225
|
+
const [status, setStatus] = (0, react.useState)();
|
|
226
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
227
|
+
const [confirming, setConfirming] = (0, react.useState)(false);
|
|
228
|
+
const [tooltipVisible, setTooltipVisible] = (0, react.useState)(false);
|
|
229
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
230
|
+
const [note, setNote] = (0, react.useState)();
|
|
231
|
+
const inFlight = (0, react.useRef)(false);
|
|
232
|
+
const mounted = (0, react.useRef)(false);
|
|
233
|
+
const tooltipId = (0, react.useId)();
|
|
234
|
+
const refresh = (0, react.useCallback)(async (signal) => {
|
|
235
|
+
const response = await fetch(WORKBUDDY_STATUS_PATH, {
|
|
236
|
+
credentials: "same-origin",
|
|
237
|
+
headers: { accept: "application/json" },
|
|
238
|
+
...signal === void 0 ? {} : { signal }
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
241
|
+
const value = await response.json();
|
|
242
|
+
if (mounted.current && !signal?.aborted) setStatus(value);
|
|
243
|
+
}, []);
|
|
244
|
+
(0, react.useEffect)(() => {
|
|
245
|
+
mounted.current = true;
|
|
246
|
+
const controller = new AbortController();
|
|
247
|
+
const load = () => {
|
|
248
|
+
refresh(controller.signal).catch(() => {});
|
|
249
|
+
};
|
|
250
|
+
load();
|
|
251
|
+
const timer = window.setInterval(load, RECONCILE_MS);
|
|
252
|
+
window.addEventListener("focus", load);
|
|
253
|
+
return () => {
|
|
254
|
+
mounted.current = false;
|
|
255
|
+
controller.abort();
|
|
256
|
+
window.clearInterval(timer);
|
|
257
|
+
window.removeEventListener("focus", load);
|
|
258
|
+
};
|
|
259
|
+
}, [refresh]);
|
|
260
|
+
const probe = status?.status === "signed-in" ? status.probe : void 0;
|
|
261
|
+
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
262
|
+
const result = status === void 0 ? void 0 : resultFor(status, model);
|
|
263
|
+
const visible = probe?.candidates.includes(model) === true || result !== void 0;
|
|
264
|
+
(0, react.useEffect)(() => {
|
|
265
|
+
setConfirming(false);
|
|
266
|
+
setNote(void 0);
|
|
267
|
+
}, [model]);
|
|
268
|
+
const detect = async () => {
|
|
269
|
+
if (key === void 0 || inFlight.current || probe?.running === true) return;
|
|
270
|
+
inFlight.current = true;
|
|
271
|
+
setNote(void 0);
|
|
272
|
+
setConfirming(false);
|
|
273
|
+
setBusy(true);
|
|
274
|
+
setFailed(false);
|
|
275
|
+
try {
|
|
276
|
+
const response = await fetch(WORKBUDDY_PROBE_PATH, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
credentials: "same-origin",
|
|
279
|
+
headers: {
|
|
280
|
+
"Content-Type": "application/json",
|
|
281
|
+
"X-WorkBuddy-Probe-Key": key
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
action: "probe",
|
|
285
|
+
model
|
|
286
|
+
})
|
|
287
|
+
});
|
|
288
|
+
const body = await response.json();
|
|
289
|
+
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");
|
|
290
|
+
if (mounted.current) {
|
|
291
|
+
const completed = {
|
|
292
|
+
id: model,
|
|
293
|
+
name: model,
|
|
294
|
+
validation: body.validation,
|
|
295
|
+
efforts: body.efforts,
|
|
296
|
+
probedAt: Date.now()
|
|
297
|
+
};
|
|
298
|
+
setNote(completed);
|
|
299
|
+
}
|
|
300
|
+
refresh().catch(() => {});
|
|
301
|
+
} catch {
|
|
302
|
+
if (mounted.current) setFailed(true);
|
|
303
|
+
} finally {
|
|
304
|
+
inFlight.current = false;
|
|
305
|
+
if (mounted.current) setBusy(false);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
if (!visible) return null;
|
|
309
|
+
const text = tooltipText(t, model, {
|
|
310
|
+
busy,
|
|
311
|
+
result,
|
|
312
|
+
failed
|
|
313
|
+
});
|
|
314
|
+
const disabled = busy || probe?.running === true || key === void 0;
|
|
315
|
+
const showTooltip = tooltipVisible && !confirming && note === void 0;
|
|
316
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
317
|
+
style: wrapperStyle,
|
|
318
|
+
onMouseEnter: () => {
|
|
319
|
+
setTooltipVisible(true);
|
|
320
|
+
},
|
|
321
|
+
onMouseLeave: () => {
|
|
322
|
+
setTooltipVisible(false);
|
|
323
|
+
},
|
|
324
|
+
children: [
|
|
325
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
326
|
+
type: "button",
|
|
327
|
+
"aria-label": text,
|
|
328
|
+
"aria-describedby": showTooltip ? tooltipId : void 0,
|
|
329
|
+
"aria-busy": busy,
|
|
330
|
+
"aria-expanded": confirming,
|
|
331
|
+
disabled,
|
|
332
|
+
onClick: () => {
|
|
333
|
+
setConfirming(true);
|
|
334
|
+
},
|
|
335
|
+
onFocus: () => {
|
|
336
|
+
setTooltipVisible(true);
|
|
337
|
+
},
|
|
338
|
+
onBlur: () => {
|
|
339
|
+
setTooltipVisible(false);
|
|
340
|
+
},
|
|
341
|
+
style: {
|
|
342
|
+
...buttonStyle$1,
|
|
343
|
+
opacity: disabled && !confirming ? .6 : 1,
|
|
344
|
+
cursor: disabled ? "default" : "pointer"
|
|
345
|
+
},
|
|
346
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
347
|
+
width: "16",
|
|
348
|
+
height: "16",
|
|
349
|
+
viewBox: "0 0 24 24",
|
|
350
|
+
fill: "none",
|
|
351
|
+
stroke: "currentColor",
|
|
352
|
+
strokeWidth: "1.6",
|
|
353
|
+
"aria-hidden": "true",
|
|
354
|
+
focusable: "false",
|
|
355
|
+
children: [
|
|
356
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
357
|
+
cx: "12",
|
|
358
|
+
cy: "12",
|
|
359
|
+
r: "9"
|
|
360
|
+
}),
|
|
361
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
362
|
+
cx: "12",
|
|
363
|
+
cy: "12",
|
|
364
|
+
r: "4"
|
|
365
|
+
}),
|
|
366
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 12 20 4" }),
|
|
367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
368
|
+
cx: "12",
|
|
369
|
+
cy: "12",
|
|
370
|
+
r: "1"
|
|
371
|
+
})
|
|
372
|
+
]
|
|
373
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
374
|
+
style: labelStyle,
|
|
375
|
+
children: label
|
|
376
|
+
})]
|
|
377
|
+
}),
|
|
378
|
+
showTooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
379
|
+
id: tooltipId,
|
|
380
|
+
role: "tooltip",
|
|
381
|
+
style: tooltipStyle,
|
|
382
|
+
children: text
|
|
383
|
+
}),
|
|
384
|
+
confirming && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
385
|
+
style: confirmStyle,
|
|
386
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeBubbleBody") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
387
|
+
style: confirmRowStyle$1,
|
|
388
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
389
|
+
type: "button",
|
|
390
|
+
style: confirmButtonStyle,
|
|
391
|
+
onClick: () => {
|
|
392
|
+
setConfirming(false);
|
|
393
|
+
},
|
|
394
|
+
children: t("cancel")
|
|
395
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
396
|
+
type: "button",
|
|
397
|
+
style: primaryButtonStyle$1,
|
|
398
|
+
onClick: () => {
|
|
399
|
+
detect();
|
|
400
|
+
},
|
|
401
|
+
children: t("probeConfirmAction")
|
|
402
|
+
})]
|
|
403
|
+
})]
|
|
404
|
+
}),
|
|
405
|
+
note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
406
|
+
role: "status",
|
|
407
|
+
"aria-live": "polite",
|
|
408
|
+
style: noteStyle,
|
|
409
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: noteText(t, note) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
410
|
+
type: "button",
|
|
411
|
+
style: noteDismissStyle,
|
|
412
|
+
onClick: () => {
|
|
413
|
+
setNote(void 0);
|
|
414
|
+
},
|
|
415
|
+
children: t("probeNoteDismiss")
|
|
416
|
+
})]
|
|
417
|
+
})
|
|
418
|
+
]
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/** Compose the one-line outcome string the note bubble shows. */
|
|
422
|
+
function noteText(t, result) {
|
|
423
|
+
if (result.validation === "validating" && result.efforts.length > 0) return t("probeNoteVerified", { levels: result.efforts.join(" / ") });
|
|
424
|
+
if (result.validation === "non-validating") return t("probeNoteNotValidating");
|
|
425
|
+
return t("probeNoteUnknown");
|
|
426
|
+
}
|
|
13
427
|
//#endregion
|
|
14
428
|
//#region src/client/WorkBuddyPluginCard.tsx
|
|
15
429
|
/** WorkBuddy status card contributed to Harness Plugin configuration. */
|
|
@@ -160,6 +574,82 @@ window.__ModuleLoader__.load({
|
|
|
160
574
|
borderRadius: 999,
|
|
161
575
|
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
|
|
162
576
|
};
|
|
577
|
+
/**
|
|
578
|
+
* Inline confirmation box for a paid detection. Replaces the previous
|
|
579
|
+
* `window.confirm`: the decision is one line plus two buttons, and a modal
|
|
580
|
+
* alert for that is heavier than the action it guards.
|
|
581
|
+
*/
|
|
582
|
+
const confirmBoxStyle = {
|
|
583
|
+
display: "flex",
|
|
584
|
+
flexDirection: "column",
|
|
585
|
+
gap: 10,
|
|
586
|
+
padding: "10px 12px",
|
|
587
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
588
|
+
borderRadius: 8,
|
|
589
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
590
|
+
};
|
|
591
|
+
const confirmRowStyle = {
|
|
592
|
+
display: "flex",
|
|
593
|
+
justifyContent: "flex-end",
|
|
594
|
+
gap: 8
|
|
595
|
+
};
|
|
596
|
+
/** One probeable model's row: name on the left, state and action on the right. */
|
|
597
|
+
const probeRowStyle = {
|
|
598
|
+
display: "flex",
|
|
599
|
+
alignItems: "center",
|
|
600
|
+
justifyContent: "space-between",
|
|
601
|
+
gap: 12
|
|
602
|
+
};
|
|
603
|
+
const probeRowEndStyle = {
|
|
604
|
+
display: "inline-flex",
|
|
605
|
+
alignItems: "center",
|
|
606
|
+
gap: 8,
|
|
607
|
+
flex: "0 0 auto"
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* Tab strip for the card body. Kept visually light — a full pill would compete
|
|
611
|
+
* with the section headings, and the card is already the densest surface the
|
|
612
|
+
* plugin owns.
|
|
613
|
+
*/
|
|
614
|
+
const tabBarStyle = {
|
|
615
|
+
display: "flex",
|
|
616
|
+
gap: 4,
|
|
617
|
+
marginTop: 4,
|
|
618
|
+
borderBottom: "1px solid var(--dsw-alias-border-l2)"
|
|
619
|
+
};
|
|
620
|
+
const tabStyle = {
|
|
621
|
+
padding: "6px 12px",
|
|
622
|
+
border: 0,
|
|
623
|
+
borderBottom: "2px solid transparent",
|
|
624
|
+
background: "transparent",
|
|
625
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
626
|
+
font: "inherit",
|
|
627
|
+
fontSize: 13,
|
|
628
|
+
lineHeight: "20px",
|
|
629
|
+
cursor: "pointer"
|
|
630
|
+
};
|
|
631
|
+
const tabActiveStyle = {
|
|
632
|
+
borderBottom: "2px solid var(--dsw-alias-brand-primary)",
|
|
633
|
+
color: "var(--dsw-alias-label-primary)",
|
|
634
|
+
fontWeight: 600
|
|
635
|
+
};
|
|
636
|
+
const tabPanelStyle = {
|
|
637
|
+
display: "flex",
|
|
638
|
+
flexDirection: "column",
|
|
639
|
+
gap: 18,
|
|
640
|
+
paddingTop: 16
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Primary action of the inline confirmation. Fill and text colour come from the
|
|
644
|
+
* theme as a pair: `brand-primary` is a light accent here, so pairing it with a
|
|
645
|
+
* hardcoded white would render white-on-white.
|
|
646
|
+
*/
|
|
647
|
+
const primaryButtonStyle = {
|
|
648
|
+
...buttonStyle,
|
|
649
|
+
border: "1px solid var(--dsw-alias-button-primary-fill)",
|
|
650
|
+
background: "var(--dsw-alias-button-primary-fill)",
|
|
651
|
+
color: "var(--dsw-alias-label-primary-foreground)"
|
|
652
|
+
};
|
|
163
653
|
function progressFillStyle(percent) {
|
|
164
654
|
return {
|
|
165
655
|
width: `${Math.max(0, Math.min(100, percent))}%`,
|
|
@@ -245,12 +735,177 @@ window.__ModuleLoader__.load({
|
|
|
245
735
|
})]
|
|
246
736
|
});
|
|
247
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Context capacity, listed in full.
|
|
740
|
+
*
|
|
741
|
+
* Every model the upstream reports a capacity for, largest first. A one-line
|
|
742
|
+
* summary with the exceptions on hover was tried and rejected: capacity is
|
|
743
|
+
* reference data you scan by model, and hiding most of it behind a hover made
|
|
744
|
+
* the common case (a model you already have in mind) the hard one to look up.
|
|
745
|
+
*
|
|
746
|
+
* Purely a report of the upstream's own number. The plugin offers no tier
|
|
747
|
+
* picker: the catalog declares one capacity per model and publishes no
|
|
748
|
+
* alternatives, so a menu would mean inventing client-side policy. (The
|
|
749
|
+
* desktop app's "300K / 1M" selector appears nowhere in the API.)
|
|
750
|
+
*/
|
|
751
|
+
function ContextTable({ models, t }) {
|
|
752
|
+
const known = (models ?? []).filter((model) => model.contextWindow !== void 0).sort((a, b) => b.contextWindow - a.contextWindow);
|
|
753
|
+
if (known.length === 0) return null;
|
|
754
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
755
|
+
style: quotaListStyle,
|
|
756
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
757
|
+
style: quotaTitleStyle,
|
|
758
|
+
children: t("contextHeading")
|
|
759
|
+
}), known.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
760
|
+
style: quotaLabelStyle,
|
|
761
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTokens(model.contextWindow) })]
|
|
762
|
+
}, model.id))]
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Compact token count for display: the catalog's own round numbers (`200000`,
|
|
767
|
+
* `1000000`) read better as `200K` / `1M`, and no precision is lost because
|
|
768
|
+
* these values are always whole thousands.
|
|
769
|
+
*/
|
|
770
|
+
function formatTokens(tokens) {
|
|
771
|
+
if (tokens >= 1e6 && tokens % 1e6 === 0) return `${tokens / 1e6}M`;
|
|
772
|
+
if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
|
|
773
|
+
return String(tokens);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Reasoning-effort detection section: consent switches, per-model detection,
|
|
777
|
+
* and the recorded observations.
|
|
778
|
+
*
|
|
779
|
+
* Two deliberate UX rules from the plan (§3.1, §3.2):
|
|
780
|
+
* - the confirmation is shown *before* any request, and its copy states the
|
|
781
|
+
* credit caveat;
|
|
782
|
+
* - a `non-validating` result is presented as an observation about the
|
|
783
|
+
* parameter ("this model does not check it"), never as a statement that a
|
|
784
|
+
* level is unsupported.
|
|
785
|
+
*/
|
|
786
|
+
function ProbeSection({ probe, t, onDetect, onClear, busy }) {
|
|
787
|
+
const [pending, setPending] = (0, react.useState)();
|
|
788
|
+
const [runningModel, setRunningModel] = (0, react.useState)();
|
|
789
|
+
(0, react.useEffect)(() => {
|
|
790
|
+
if (pending !== void 0 && !probe.candidates.includes(pending)) setPending(void 0);
|
|
791
|
+
}, [pending, probe.candidates]);
|
|
792
|
+
const runningArmed = (0, react.useRef)(false);
|
|
793
|
+
(0, react.useEffect)(() => {
|
|
794
|
+
if (runningModel === void 0) return;
|
|
795
|
+
if (busy || probe.running) {
|
|
796
|
+
runningArmed.current = true;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (!runningArmed.current) return;
|
|
800
|
+
runningArmed.current = false;
|
|
801
|
+
setRunningModel(void 0);
|
|
802
|
+
}, [
|
|
803
|
+
runningModel,
|
|
804
|
+
busy,
|
|
805
|
+
probe.running
|
|
806
|
+
]);
|
|
807
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
808
|
+
style: quotaListStyle,
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
811
|
+
style: quotaTitleStyle,
|
|
812
|
+
children: t("probeHeading")
|
|
813
|
+
}),
|
|
814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
815
|
+
style: bodyStyle,
|
|
816
|
+
children: t("probeIntro")
|
|
817
|
+
}),
|
|
818
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
819
|
+
style: bodyStyle,
|
|
820
|
+
children: t("probeConsentHint")
|
|
821
|
+
}),
|
|
822
|
+
probe.running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
823
|
+
style: bodyStyle,
|
|
824
|
+
children: t("probeRunningGeneric")
|
|
825
|
+
}) : null,
|
|
826
|
+
probe.candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
827
|
+
style: bodyStyle,
|
|
828
|
+
children: t("probeResultEmpty")
|
|
829
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
830
|
+
style: quotaGroupStyle,
|
|
831
|
+
children: probe.candidates.map((id) => {
|
|
832
|
+
const result = probe.results.find((entry) => entry.id === id);
|
|
833
|
+
const name = result?.name ?? id;
|
|
834
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
835
|
+
style: modelOfferStyle,
|
|
836
|
+
children: [
|
|
837
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
838
|
+
style: probeRowStyle,
|
|
839
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
840
|
+
style: probeRowEndStyle,
|
|
841
|
+
children: [result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
842
|
+
style: modelBadgeChipStyle,
|
|
843
|
+
children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : t(result.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown")
|
|
844
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
845
|
+
type: "button",
|
|
846
|
+
style: buttonStyle,
|
|
847
|
+
disabled: probe.running || busy,
|
|
848
|
+
onClick: () => {
|
|
849
|
+
setPending(id);
|
|
850
|
+
},
|
|
851
|
+
children: runningModel === id ? t("probeRunning", { model: id }) : t(result === void 0 ? "probeStart" : "probeRedetect")
|
|
852
|
+
})]
|
|
853
|
+
})]
|
|
854
|
+
}),
|
|
855
|
+
result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
856
|
+
style: modelRateStyle,
|
|
857
|
+
children: t("probeResultAt", { time: formatTime(result.probedAt) })
|
|
858
|
+
}),
|
|
859
|
+
pending === id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
860
|
+
style: confirmBoxStyle,
|
|
861
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
862
|
+
style: bodyStyle,
|
|
863
|
+
children: t("probeConfirmBody", { model: name })
|
|
864
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
865
|
+
style: confirmRowStyle,
|
|
866
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
867
|
+
type: "button",
|
|
868
|
+
style: buttonStyle,
|
|
869
|
+
onClick: () => {
|
|
870
|
+
setPending(void 0);
|
|
871
|
+
},
|
|
872
|
+
children: t("cancel")
|
|
873
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
874
|
+
type: "button",
|
|
875
|
+
style: primaryButtonStyle,
|
|
876
|
+
disabled: probe.running || busy,
|
|
877
|
+
onClick: () => {
|
|
878
|
+
setRunningModel(id);
|
|
879
|
+
setPending(void 0);
|
|
880
|
+
onDetect(id);
|
|
881
|
+
},
|
|
882
|
+
children: t("probeConfirmAction")
|
|
883
|
+
})]
|
|
884
|
+
})]
|
|
885
|
+
}) : null
|
|
886
|
+
]
|
|
887
|
+
}, id);
|
|
888
|
+
})
|
|
889
|
+
}),
|
|
890
|
+
probe.results.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
891
|
+
type: "button",
|
|
892
|
+
style: buttonStyle,
|
|
893
|
+
disabled: busy,
|
|
894
|
+
onClick: () => {
|
|
895
|
+
onClear();
|
|
896
|
+
},
|
|
897
|
+
children: t("probeClear")
|
|
898
|
+
})
|
|
899
|
+
]
|
|
900
|
+
});
|
|
901
|
+
}
|
|
248
902
|
/** Render WorkBuddy sign-in state and credit as one expandable card. */
|
|
249
903
|
function WorkBuddyPluginCard({ t }) {
|
|
250
904
|
if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
|
|
251
905
|
const [open, setOpen] = (0, react.useState)(false);
|
|
252
906
|
const [status, setStatus] = (0, react.useState)({ status: "signed-out" });
|
|
253
907
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
908
|
+
const [tab, setTab] = (0, react.useState)("status");
|
|
254
909
|
const mounted = (0, react.useRef)(true);
|
|
255
910
|
(0, react.useEffect)(() => {
|
|
256
911
|
mounted.current = true;
|
|
@@ -306,6 +961,56 @@ window.__ModuleLoader__.load({
|
|
|
306
961
|
if (mounted.current) setBusy(false);
|
|
307
962
|
}
|
|
308
963
|
};
|
|
964
|
+
/**
|
|
965
|
+
* Run one control action and refresh the card's state afterwards.
|
|
966
|
+
*
|
|
967
|
+
* The key travels in a header, not the body: it authorizes the write, and
|
|
968
|
+
* the host never accepts a prompt, a sentinel, or a model outside its own
|
|
969
|
+
* catalog from here.
|
|
970
|
+
*/
|
|
971
|
+
const control = (0, react.useCallback)(async (action) => {
|
|
972
|
+
const key = status.status === "signed-in" ? status.probeKey : void 0;
|
|
973
|
+
if (key === void 0) return;
|
|
974
|
+
setBusy(true);
|
|
975
|
+
try {
|
|
976
|
+
const response = await fetch(WORKBUDDY_PROBE_PATH, {
|
|
977
|
+
method: "POST",
|
|
978
|
+
headers: {
|
|
979
|
+
"Content-Type": "application/json",
|
|
980
|
+
"X-WorkBuddy-Probe-Key": key
|
|
981
|
+
},
|
|
982
|
+
credentials: "same-origin",
|
|
983
|
+
body: JSON.stringify(action)
|
|
984
|
+
});
|
|
985
|
+
const value = await response.json().catch(() => void 0);
|
|
986
|
+
if (!response.ok) {
|
|
987
|
+
const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
|
|
988
|
+
throw new Error(message);
|
|
989
|
+
}
|
|
990
|
+
await refresh();
|
|
991
|
+
} catch (error) {
|
|
992
|
+
if (mounted.current) setStatus((previous) => ({
|
|
993
|
+
status: "error",
|
|
994
|
+
message: error instanceof Error ? error.message : t("requestFailed")
|
|
995
|
+
}));
|
|
996
|
+
} finally {
|
|
997
|
+
if (mounted.current) setBusy(false);
|
|
998
|
+
}
|
|
999
|
+
}, [
|
|
1000
|
+
refresh,
|
|
1001
|
+
status,
|
|
1002
|
+
t
|
|
1003
|
+
]);
|
|
1004
|
+
/**
|
|
1005
|
+
* Start a detection. Confirmation happens inline in the section, so this is
|
|
1006
|
+
* only ever called after the user has already agreed.
|
|
1007
|
+
*/
|
|
1008
|
+
const confirmDetect = (0, react.useCallback)((modelId) => {
|
|
1009
|
+
control({
|
|
1010
|
+
action: "probe",
|
|
1011
|
+
model: modelId
|
|
1012
|
+
});
|
|
1013
|
+
}, [control]);
|
|
309
1014
|
const title = t("title");
|
|
310
1015
|
const label = status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).replace(/[::]\s*$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
|
|
311
1016
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
@@ -366,37 +1071,86 @@ window.__ModuleLoader__.load({
|
|
|
366
1071
|
style: bodyStyle,
|
|
367
1072
|
children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
|
|
368
1073
|
}),
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
children: t("creditsError", { message: status.creditsError })
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1075
|
+
role: "tablist",
|
|
1076
|
+
style: tabBarStyle,
|
|
1077
|
+
children: [
|
|
1078
|
+
"status",
|
|
1079
|
+
"context",
|
|
1080
|
+
"details"
|
|
1081
|
+
].map((id) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1082
|
+
type: "button",
|
|
1083
|
+
role: "tab",
|
|
1084
|
+
"aria-selected": tab === id,
|
|
1085
|
+
onClick: () => {
|
|
1086
|
+
setTab(id);
|
|
1087
|
+
},
|
|
1088
|
+
style: {
|
|
1089
|
+
...tabStyle,
|
|
1090
|
+
...tab === id ? tabActiveStyle : {}
|
|
1091
|
+
},
|
|
1092
|
+
children: t(id === "status" ? "tabStatus" : id === "context" ? "tabContext" : "tabDetails")
|
|
1093
|
+
}, id))
|
|
390
1094
|
}),
|
|
391
|
-
|
|
392
|
-
style:
|
|
393
|
-
children: [
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
1095
|
+
tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1096
|
+
style: tabPanelStyle,
|
|
1097
|
+
children: [
|
|
1098
|
+
status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1099
|
+
style: quotaListStyle,
|
|
1100
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1101
|
+
style: rowStyle,
|
|
1102
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1103
|
+
style: quotaTitleStyle,
|
|
1104
|
+
children: t("creditsHeading")
|
|
1105
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1106
|
+
style: bodyStyle,
|
|
1107
|
+
children: t("creditsTotal", { total: formatNumber(status.credits.total) })
|
|
1108
|
+
})]
|
|
1109
|
+
})
|
|
1110
|
+
}),
|
|
1111
|
+
status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1112
|
+
style: errorStyle,
|
|
1113
|
+
children: t("creditsError", { message: status.creditsError })
|
|
1114
|
+
}),
|
|
1115
|
+
status.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeSection, {
|
|
1116
|
+
probe: status.probe,
|
|
1117
|
+
t,
|
|
1118
|
+
busy,
|
|
1119
|
+
onDetect: confirmDetect,
|
|
1120
|
+
onClear: () => {
|
|
1121
|
+
control({ action: "clear" });
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
]
|
|
1125
|
+
}) : tab === "context" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1126
|
+
style: tabPanelStyle,
|
|
1127
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
|
|
1128
|
+
models: status.models,
|
|
398
1129
|
t
|
|
399
|
-
}
|
|
1130
|
+
})
|
|
1131
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1132
|
+
style: tabPanelStyle,
|
|
1133
|
+
children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1134
|
+
style: quotaListStyle,
|
|
1135
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1136
|
+
style: quotaTitleStyle,
|
|
1137
|
+
children: t("creditsDetailHeading")
|
|
1138
|
+
}), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
|
|
1139
|
+
label: account.packageName,
|
|
1140
|
+
remain: account.remain,
|
|
1141
|
+
size: account.size,
|
|
1142
|
+
t
|
|
1143
|
+
}, `${account.packageName}-${String(index)}`))]
|
|
1144
|
+
}), status.models === void 0 || status.models.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1145
|
+
style: quotaListStyle,
|
|
1146
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1147
|
+
style: quotaTitleStyle,
|
|
1148
|
+
children: t("modelsHeading")
|
|
1149
|
+
}), status.models.filter((model) => model.free === true || (model.badges?.length ?? 0) > 0).map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOfferRow, {
|
|
1150
|
+
model,
|
|
1151
|
+
t
|
|
1152
|
+
}, model.id))]
|
|
1153
|
+
})]
|
|
400
1154
|
})
|
|
401
1155
|
] }) : null,
|
|
402
1156
|
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -425,6 +1179,10 @@ window.__ModuleLoader__.load({
|
|
|
425
1179
|
signedInAs: "Signed in as {nickname}",
|
|
426
1180
|
accessTokenExpires: "Access token expires {time} (refresh is automatic)",
|
|
427
1181
|
creditsHeading: "Remaining credit",
|
|
1182
|
+
tabStatus: "Status",
|
|
1183
|
+
tabContext: "Context window",
|
|
1184
|
+
tabDetails: "Credit details",
|
|
1185
|
+
creditsDetailHeading: "By package",
|
|
428
1186
|
creditsTotal: "Total: {total}",
|
|
429
1187
|
percentRemaining: "{percent}% remaining",
|
|
430
1188
|
exactRemaining: "{remain} / {size} remaining",
|
|
@@ -435,10 +1193,41 @@ window.__ModuleLoader__.load({
|
|
|
435
1193
|
requestFailed: "Request failed",
|
|
436
1194
|
accountHeading: "Account",
|
|
437
1195
|
modelsHeading: "Model offers",
|
|
1196
|
+
contextHeading: "Context window",
|
|
438
1197
|
freeModel: "Free",
|
|
439
1198
|
badgeLimitedFree: "Limited-time free",
|
|
440
1199
|
badgeNightDiscount: "Night discount",
|
|
441
|
-
rate: "{rate} credits per message"
|
|
1200
|
+
rate: "{rate} credits per message",
|
|
1201
|
+
probeLabel: "Reasoning levels",
|
|
1202
|
+
probeTooltipIdle: "Detect the reasoning levels {model} accepts",
|
|
1203
|
+
probeTooltipVerified: "Accepted levels: {levels} · click to detect again",
|
|
1204
|
+
probeTooltipNotValidating: "This model does not check the effort parameter",
|
|
1205
|
+
probeTooltipRetry: "Detection did not complete · click to retry",
|
|
1206
|
+
probeBubbleBody: "Send test requests to confirm the available reasoning levels. May consume a small amount of credit.",
|
|
1207
|
+
probeConfirmAction: "Confirm",
|
|
1208
|
+
probeNoteVerified: "Detected: {levels}",
|
|
1209
|
+
probeNoteNotValidating: "This model does not check the effort parameter",
|
|
1210
|
+
probeNoteUnknown: "Detection did not complete",
|
|
1211
|
+
probeNoteDismiss: "Got it",
|
|
1212
|
+
probeHeading: "Reasoning effort detection",
|
|
1213
|
+
probeResultNoLevels: "No tested levels were accepted.",
|
|
1214
|
+
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.",
|
|
1215
|
+
probeConsent: "Authorize detection",
|
|
1216
|
+
probeConsentHint: "Each detection sends test requests to one model to confirm its available reasoning levels, and may consume a small amount of credit.",
|
|
1217
|
+
probeStart: "Detect",
|
|
1218
|
+
probeRedetect: "Detect again",
|
|
1219
|
+
probeRunning: "Detecting {model}…",
|
|
1220
|
+
probeRunningGeneric: "Detecting…",
|
|
1221
|
+
probeClear: "Clear detected results",
|
|
1222
|
+
probeCandidates: "Detectable models: {count}",
|
|
1223
|
+
probeConfirmBody: "Send test requests to {model} to confirm its available reasoning levels. May consume a small amount of credit.",
|
|
1224
|
+
cancel: "Cancel",
|
|
1225
|
+
probeResultVerified: "Verified levels: {levels}",
|
|
1226
|
+
probeResultNotValidating: "This model does not check the effort parameter",
|
|
1227
|
+
probeResultUnknown: "Detection did not complete",
|
|
1228
|
+
probeResultAt: "Detected {time}",
|
|
1229
|
+
probeResultEmpty: "No detectable models right now.",
|
|
1230
|
+
probeFailed: "Detection failed: {message}"
|
|
442
1231
|
};
|
|
443
1232
|
const zh = {
|
|
444
1233
|
title: "DSH WorkBuddy Connect",
|
|
@@ -451,6 +1240,10 @@ window.__ModuleLoader__.load({
|
|
|
451
1240
|
signedInAs: "已登录:{nickname}",
|
|
452
1241
|
accessTokenExpires: "访问令牌 {time} 过期(自动续期)",
|
|
453
1242
|
creditsHeading: "剩余积分",
|
|
1243
|
+
tabStatus: "状态",
|
|
1244
|
+
tabContext: "上下文窗口",
|
|
1245
|
+
tabDetails: "积分详情",
|
|
1246
|
+
creditsDetailHeading: "按套餐",
|
|
454
1247
|
creditsTotal: "合计:{total}",
|
|
455
1248
|
percentRemaining: "剩余 {percent}%",
|
|
456
1249
|
exactRemaining: "剩余 {remain} / {size}",
|
|
@@ -461,10 +1254,41 @@ window.__ModuleLoader__.load({
|
|
|
461
1254
|
requestFailed: "请求失败",
|
|
462
1255
|
accountHeading: "账号",
|
|
463
1256
|
modelsHeading: "模型优惠",
|
|
1257
|
+
contextHeading: "上下文窗口",
|
|
464
1258
|
freeModel: "免费",
|
|
465
1259
|
badgeLimitedFree: "限时免费",
|
|
466
1260
|
badgeNightDiscount: "夜间折扣",
|
|
467
|
-
rate: "{rate} 积分/次"
|
|
1261
|
+
rate: "{rate} 积分/次",
|
|
1262
|
+
probeLabel: "推理等级",
|
|
1263
|
+
probeTooltipIdle: "检测 {model} 可用的推理档位",
|
|
1264
|
+
probeTooltipVerified: "已接受:{levels} · 点击可重新检测",
|
|
1265
|
+
probeTooltipNotValidating: "该模型不校验该参数",
|
|
1266
|
+
probeTooltipRetry: "检测未完成 · 点击重试",
|
|
1267
|
+
probeBubbleBody: "发送探测请求以确认可用推理档位。可能消耗少量积分。",
|
|
1268
|
+
probeConfirmAction: "确认检测",
|
|
1269
|
+
probeNoteVerified: "已检测:{levels}",
|
|
1270
|
+
probeNoteNotValidating: "该模型不校验该参数",
|
|
1271
|
+
probeNoteUnknown: "检测未完成",
|
|
1272
|
+
probeNoteDismiss: "知道了",
|
|
1273
|
+
probeHeading: "推理档位检测",
|
|
1274
|
+
probeResultNoLevels: "本次测试的档位均未被接受。",
|
|
1275
|
+
probeIntro: "部分模型具备思考能力,但没有声明可选档位。检测会发送少量真实请求,可能消耗积分。",
|
|
1276
|
+
probeConsent: "授权检测",
|
|
1277
|
+
probeConsentHint: "每次检测会向该模型发送探测请求,以确认可用推理档位,可能消耗少量积分。",
|
|
1278
|
+
probeStart: "开始检测",
|
|
1279
|
+
probeRedetect: "重新检测",
|
|
1280
|
+
probeRunning: "正在检测 {model}…",
|
|
1281
|
+
probeRunningGeneric: "正在检测…",
|
|
1282
|
+
probeClear: "清除已探测结果",
|
|
1283
|
+
probeCandidates: "可检测模型:{count} 个",
|
|
1284
|
+
probeConfirmBody: "向 {model} 发送探测请求,以确认可用推理档位。可能消耗少量积分。",
|
|
1285
|
+
cancel: "取消",
|
|
1286
|
+
probeResultVerified: "已验证接受的档位:{levels}",
|
|
1287
|
+
probeResultNotValidating: "该模型不校验该参数",
|
|
1288
|
+
probeResultUnknown: "检测未完成",
|
|
1289
|
+
probeResultAt: "检测于 {time}",
|
|
1290
|
+
probeResultEmpty: "当前没有可检测的模型。",
|
|
1291
|
+
probeFailed: "检测失败:{message}"
|
|
468
1292
|
};
|
|
469
1293
|
//#endregion
|
|
470
1294
|
//#region src/client/index.tsx
|
|
@@ -515,6 +1339,17 @@ window.__ModuleLoader__.load({
|
|
|
515
1339
|
priority: 30,
|
|
516
1340
|
inject: () => ({ t })
|
|
517
1341
|
}, WorkBuddyPluginCard));
|
|
1342
|
+
ctx.inject(["modelDirectories"], (scope) => {
|
|
1343
|
+
scope.slots.inject("conversation.input.right", () => scope.slots.register({
|
|
1344
|
+
name: "conversation.input.right",
|
|
1345
|
+
id: "workbuddy-probe",
|
|
1346
|
+
order: 10,
|
|
1347
|
+
inject: (sessionId) => ({
|
|
1348
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
1349
|
+
t
|
|
1350
|
+
})
|
|
1351
|
+
}, WorkBuddyProbeControl));
|
|
1352
|
+
});
|
|
518
1353
|
} catch (error) {
|
|
519
1354
|
console.error("[dsh-workbuddy-connect] client card failed to load (host provider unaffected):", error);
|
|
520
1355
|
}
|