dsh-workbuddy-bridge 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -54
- package/README_en.md +61 -63
- package/lib/bin.js +8 -7
- package/lib/client.js +1210 -411
- package/lib/{variants-DP0Ho0Tc.js → heartbeat-DihKwwFX.js} +152 -97
- package/lib/index.d.ts +187 -4
- package/lib/index.js +582 -88
- package/locale/README.md +10 -0
- package/package.json +40 -28
package/lib/client.js
CHANGED
|
@@ -31,54 +31,74 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
timeStyle: "short"
|
|
32
32
|
}).format(new Date(ms));
|
|
33
33
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Locale-formatted cycle-reset time, falling back to the wire string.
|
|
36
|
-
*
|
|
37
|
-
* The host forwards the upstream's own timestamp; when it is not a date this
|
|
38
|
-
* plugin can parse, the raw text is still more useful than nothing.
|
|
39
|
-
*/
|
|
40
|
-
function formatCycleReset(time) {
|
|
41
|
-
const parsed = Date.parse(time);
|
|
42
|
-
return Number.isNaN(parsed) ? time : formatTime(parsed);
|
|
43
|
-
}
|
|
44
34
|
/** Locale-formatted percentage, at most one fraction digit. */
|
|
45
35
|
function formatPercent(percent) {
|
|
46
36
|
return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent);
|
|
47
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A credit amount, at the precision the ledger actually carries.
|
|
40
|
+
*
|
|
41
|
+
* Credits are recorded to two decimals — the desktop app accumulates stage
|
|
42
|
+
* increments with `Math.round(value * 100) / 100` — so trailing zeros carry no
|
|
43
|
+
* information: `0.50` is `0.5` and `3.00` is `3`. This mirrors that app's own
|
|
44
|
+
* `formatCredit` (integers bare, otherwise two decimals with trailing zeros
|
|
45
|
+
* stripped), so a figure read here matches the one the WorkBuddy client shows
|
|
46
|
+
* for the same request.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately *not* `Intl`-formatted: this is a ledger quantity at a fixed
|
|
49
|
+
* scale, not a locale-sensitive count, and a thousands separator on a balance
|
|
50
|
+
* like `5143` would make it differ from the account's own display.
|
|
51
|
+
*/
|
|
52
|
+
function formatCredit(credit) {
|
|
53
|
+
if (!Number.isFinite(credit)) return "";
|
|
54
|
+
if (Number.isInteger(credit)) return String(credit);
|
|
55
|
+
return credit.toFixed(2).replace(/\.?0+$/u, "");
|
|
56
|
+
}
|
|
48
57
|
//#endregion
|
|
49
58
|
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/workbuddy.module.css.js
|
|
50
|
-
const css$
|
|
51
|
-
const tagId$
|
|
52
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
59
|
+
const css$4 = "._8BbhmW_page,._8BbhmW_cards{flex-direction:column;gap:12px;display:flex}._8BbhmW_toggleRow{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);justify-content:space-between;align-items:flex-start;gap:16px;padding:12px 0;font-size:13px;line-height:1.5;display:flex}._8BbhmW_toggleLabel{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}._8BbhmW_toggleHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}._8BbhmW_body{flex-direction:column;gap:12px;padding:4px 0 12px;display:flex}._8BbhmW_section{flex-direction:column;display:flex}._8BbhmW_section>*+*{margin-top:12px}._8BbhmW_cardActions{align-items:center;gap:8px;display:flex}._8BbhmW_field{flex-direction:column;gap:6px;display:flex}._8BbhmW_head{align-items:center;gap:8px;display:flex}._8BbhmW_main{flex-wrap:wrap;flex:1;align-items:baseline;gap:8px;min-width:0;display:flex}._8BbhmW_action{flex:none;align-items:center;gap:8px;display:inline-flex}._8BbhmW_note{flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0;display:flex}._8BbhmW_part{flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0;font-size:13px;line-height:1.5;display:inline-flex}._8BbhmW_caption{color:var(--dsw-alias-label-tertiary)}._8BbhmW_figure{min-width:0;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;align-items:baseline;gap:6px;font-weight:500;display:inline-flex}._8BbhmW_sectionActions{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;gap:8px;padding-top:10px;display:flex}._8BbhmW_title{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:500;line-height:1.5}._8BbhmW_summaryRow{flex-wrap:wrap;gap:4px;height:auto;min-height:28px}._8BbhmW_summaryTitle{flex:1;min-width:0}._8BbhmW_statusLine{color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;font-size:12px;line-height:1.5;display:inline-flex}._8BbhmW_text{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.5}._8BbhmW_dim{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}._8BbhmW_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:1.5}._8BbhmW_package{flex-direction:column;gap:8px;padding:8px 0;display:flex}._8BbhmW_package+._8BbhmW_package{border-top:.5px solid var(--dsw-alias-border-l2)}._8BbhmW_packageLabel{justify-content:space-between;align-items:baseline;gap:8px;font-size:13px;line-height:1.5;display:flex}._8BbhmW_track{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;height:8px;overflow:hidden}._8BbhmW_fill{border-radius:inherit;background:var(--dsw-alias-brand-primary);height:100%}._8BbhmW_panel{flex-direction:column;gap:12px;height:380px;display:flex}._8BbhmW_panel>*{flex:none}._8BbhmW_list{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;gap:0;min-width:0;margin:0;padding:6px 14px;display:grid;overflow:auto}._8BbhmW_panel>._8BbhmW_list{flex:auto;min-height:0}._8BbhmW_listHeader{border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin:0;padding:6px 0 8px;font-size:12px;line-height:1.5}._8BbhmW_modelRow{justify-content:space-between;align-items:center;gap:8px;padding:8px 0;display:flex}._8BbhmW_modelRow+._8BbhmW_modelRow{border-top:.5px solid var(--dsw-alias-border-l2)}._8BbhmW_modelMain{align-items:center;gap:8px;min-width:0;display:flex}._8BbhmW_modelCheck{font-size:13px;line-height:1.5}._8BbhmW_modelStack{flex-direction:column;gap:2px;min-width:0;display:flex}._8BbhmW_name{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}._8BbhmW_modelEnd{text-align:right;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-direction:column;gap:2px;font-size:12px;line-height:1.5;display:flex}._8BbhmW_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}._8BbhmW_badges{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._8BbhmW_probeRow{justify-content:space-between;align-items:center;gap:8px;padding:8px 0;display:flex}._8BbhmW_modelStack+._8BbhmW_modelStack{border-top:.5px solid var(--dsw-alias-border-l2)}._8BbhmW_probeEnd{flex:none;align-items:center;gap:8px;display:inline-flex}._8BbhmW_assist{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-module-platform);flex-direction:column;gap:8px;padding:12px;display:flex}._8BbhmW_prompt{min-width:0;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;flex:220px;margin:0;font-size:12px;line-height:1.6}._8BbhmW_promptRow{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-3);flex-wrap:wrap;align-items:flex-start;gap:8px;padding:8px 9px 8px 11px;display:flex}";
|
|
60
|
+
const tagId$4 = "dsh-workbuddy-bridge/workbuddy.module.css";
|
|
61
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
|
|
53
62
|
const tag = document.createElement("style");
|
|
54
63
|
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
55
|
-
tag.dataset.pluginCss = tagId$
|
|
56
|
-
tag.textContent = css$
|
|
64
|
+
tag.dataset.pluginCss = tagId$4;
|
|
65
|
+
tag.textContent = css$4;
|
|
57
66
|
document.head.appendChild(tag);
|
|
58
67
|
}
|
|
59
68
|
var workbuddy_module_css_default = {
|
|
69
|
+
"action": "_8BbhmW_action",
|
|
60
70
|
"assist": "_8BbhmW_assist",
|
|
61
71
|
"badges": "_8BbhmW_badges",
|
|
62
72
|
"body": "_8BbhmW_body",
|
|
73
|
+
"caption": "_8BbhmW_caption",
|
|
74
|
+
"cardActions": "_8BbhmW_cardActions",
|
|
63
75
|
"cards": "_8BbhmW_cards",
|
|
64
76
|
"dim": "_8BbhmW_dim",
|
|
65
77
|
"error": "_8BbhmW_error",
|
|
78
|
+
"field": "_8BbhmW_field",
|
|
79
|
+
"figure": "_8BbhmW_figure",
|
|
66
80
|
"fill": "_8BbhmW_fill",
|
|
81
|
+
"head": "_8BbhmW_head",
|
|
67
82
|
"list": "_8BbhmW_list",
|
|
83
|
+
"listHeader": "_8BbhmW_listHeader",
|
|
84
|
+
"main": "_8BbhmW_main",
|
|
68
85
|
"meta": "_8BbhmW_meta",
|
|
86
|
+
"modelCheck": "_8BbhmW_modelCheck",
|
|
69
87
|
"modelEnd": "_8BbhmW_modelEnd",
|
|
70
88
|
"modelMain": "_8BbhmW_modelMain",
|
|
71
89
|
"modelRow": "_8BbhmW_modelRow",
|
|
72
90
|
"modelStack": "_8BbhmW_modelStack",
|
|
73
91
|
"name": "_8BbhmW_name",
|
|
92
|
+
"note": "_8BbhmW_note",
|
|
74
93
|
"package": "_8BbhmW_package",
|
|
75
94
|
"packageLabel": "_8BbhmW_packageLabel",
|
|
76
95
|
"page": "_8BbhmW_page",
|
|
96
|
+
"panel": "_8BbhmW_panel",
|
|
97
|
+
"part": "_8BbhmW_part",
|
|
77
98
|
"probeEnd": "_8BbhmW_probeEnd",
|
|
78
99
|
"probeRow": "_8BbhmW_probeRow",
|
|
79
100
|
"prompt": "_8BbhmW_prompt",
|
|
80
101
|
"promptRow": "_8BbhmW_promptRow",
|
|
81
|
-
"row": "_8BbhmW_row",
|
|
82
102
|
"section": "_8BbhmW_section",
|
|
83
103
|
"sectionActions": "_8BbhmW_sectionActions",
|
|
84
104
|
"statusLine": "_8BbhmW_statusLine",
|
|
@@ -92,6 +112,58 @@ window.__ModuleLoader__.load({
|
|
|
92
112
|
"track": "_8BbhmW_track"
|
|
93
113
|
};
|
|
94
114
|
//#endregion
|
|
115
|
+
//#region src/client/field.tsx
|
|
116
|
+
/**
|
|
117
|
+
* Render one fact: a main line with an optional control, and an optional note.
|
|
118
|
+
*
|
|
119
|
+
* @param props - see {@link FieldProps}.
|
|
120
|
+
*/
|
|
121
|
+
function Field({ main, note, action }) {
|
|
122
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
123
|
+
className: workbuddy_module_css_default.field,
|
|
124
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
125
|
+
className: workbuddy_module_css_default.head,
|
|
126
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
127
|
+
className: workbuddy_module_css_default.main,
|
|
128
|
+
children: main
|
|
129
|
+
}), action === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
130
|
+
className: workbuddy_module_css_default.action,
|
|
131
|
+
children: action
|
|
132
|
+
})]
|
|
133
|
+
}), note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
134
|
+
className: workbuddy_module_css_default.note,
|
|
135
|
+
children: note
|
|
136
|
+
})]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Render one line of tiered text: `caption value after`.
|
|
141
|
+
*
|
|
142
|
+
* Omit `children` for a line that is pure copy — a caption alone renders as the
|
|
143
|
+
* grey sentence it is, which is how the note under the detection list works.
|
|
144
|
+
*
|
|
145
|
+
* @param props - see {@link PartProps}.
|
|
146
|
+
*/
|
|
147
|
+
function Part({ caption, children, after }) {
|
|
148
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
149
|
+
className: workbuddy_module_css_default.part,
|
|
150
|
+
children: [
|
|
151
|
+
caption === void 0 || caption === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
152
|
+
className: workbuddy_module_css_default.caption,
|
|
153
|
+
children: caption
|
|
154
|
+
}),
|
|
155
|
+
children === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
156
|
+
className: workbuddy_module_css_default.figure,
|
|
157
|
+
children
|
|
158
|
+
}),
|
|
159
|
+
after === void 0 || after === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
160
|
+
className: workbuddy_module_css_default.caption,
|
|
161
|
+
children: after
|
|
162
|
+
})
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
95
167
|
//#region src/client/panels.tsx
|
|
96
168
|
/**
|
|
97
169
|
* The card's three panels and the Agent assist block.
|
|
@@ -149,6 +221,23 @@ window.__ModuleLoader__.load({
|
|
|
149
221
|
if (badge === "Free now") return t("badgeFreeNow");
|
|
150
222
|
return badge;
|
|
151
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Where the model list on screen came from — the source, as a value.
|
|
226
|
+
*
|
|
227
|
+
* Lives here rather than in the card header: the answer describes the state of
|
|
228
|
+
* the catalog, and the catalog is this panel's subject. It sat in the account
|
|
229
|
+
* block only because that block had room, which made a fact about the model list
|
|
230
|
+
* read as a fact about the account.
|
|
231
|
+
*
|
|
232
|
+
* Only the source. The moment it was read is a *dynamic* value and is rendered
|
|
233
|
+
* as its own element beside this one — an interpolated sentence would carry a
|
|
234
|
+
* localized date whose length changes with the locale, and the label above it
|
|
235
|
+
* would have to move to keep up.
|
|
236
|
+
*/
|
|
237
|
+
function catalogSourceText(catalog, t) {
|
|
238
|
+
if (catalog.source === "live") return t("catalogSourceLive");
|
|
239
|
+
return catalog.source === "saved" ? t("catalogSourceSaved") : t("catalogSourceFallback");
|
|
240
|
+
}
|
|
152
241
|
/** One model's promotional badges, plus `Free` when the upstream says so. */
|
|
153
242
|
function ModelBadges({ model, t }) {
|
|
154
243
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -177,7 +266,13 @@ window.__ModuleLoader__.load({
|
|
|
177
266
|
className: workbuddy_module_css_default.package,
|
|
178
267
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
179
268
|
className: workbuddy_module_css_default.packageLabel,
|
|
180
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
269
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
270
|
+
className: workbuddy_module_css_default.caption,
|
|
271
|
+
children: label
|
|
272
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
273
|
+
className: workbuddy_module_css_default.figure,
|
|
274
|
+
children: quota
|
|
275
|
+
})]
|
|
181
276
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
182
277
|
className: workbuddy_module_css_default.track,
|
|
183
278
|
role: "progressbar",
|
|
@@ -185,45 +280,55 @@ window.__ModuleLoader__.load({
|
|
|
185
280
|
"aria-valuetext": quota
|
|
186
281
|
})]
|
|
187
282
|
});
|
|
188
|
-
const
|
|
189
|
-
const
|
|
283
|
+
const percent = size > 0 ? remain / size * 100 : void 0;
|
|
284
|
+
const reading = percent === void 0 ? t("creditPackageUnknownSize", { remain: formatNumber(remain) }) : t("packageRemaining", {
|
|
190
285
|
remain: formatNumber(remain),
|
|
191
|
-
size: formatNumber(size)
|
|
192
|
-
|
|
193
|
-
|
|
286
|
+
size: formatNumber(size),
|
|
287
|
+
percent: formatPercent(percent)
|
|
288
|
+
});
|
|
194
289
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
195
290
|
className: workbuddy_module_css_default.package,
|
|
196
|
-
children: [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
202
|
-
className: workbuddy_module_css_default.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
children: detail
|
|
291
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
292
|
+
className: workbuddy_module_css_default.packageLabel,
|
|
293
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
294
|
+
className: workbuddy_module_css_default.caption,
|
|
295
|
+
children: label
|
|
296
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
297
|
+
className: workbuddy_module_css_default.figure,
|
|
298
|
+
children: reading
|
|
299
|
+
})]
|
|
300
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
301
|
+
className: workbuddy_module_css_default.track,
|
|
302
|
+
role: "progressbar",
|
|
303
|
+
"aria-label": label,
|
|
304
|
+
...percent === void 0 ? { "aria-valuetext": reading } : {
|
|
305
|
+
"aria-valuemin": 0,
|
|
306
|
+
"aria-valuemax": 100,
|
|
307
|
+
"aria-valuenow": percent
|
|
308
|
+
},
|
|
309
|
+
children: percent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
310
|
+
className: workbuddy_module_css_default.fill,
|
|
311
|
+
style: { width: `${Math.max(0, Math.min(100, percent))}%` }
|
|
218
312
|
})
|
|
219
|
-
]
|
|
313
|
+
})]
|
|
220
314
|
});
|
|
221
315
|
}
|
|
222
|
-
/**
|
|
316
|
+
/**
|
|
317
|
+
* The per-package credit breakdown, under the card's own "Credits" tab.
|
|
318
|
+
*
|
|
319
|
+
* Rendered into the **same scroll container the other two panels use**. It
|
|
320
|
+
* originally rendered straight into the panel with no container at all, so while
|
|
321
|
+
* the models and detection tabs were capped at 280px this one grew to whatever
|
|
322
|
+
* the account had — 11 packages on the maintainer's account, roughly three times
|
|
323
|
+
* the height of the other two tabs (2026-10-02). Switching tabs resized the card
|
|
324
|
+
* and only this tab had no scrollbar; now all three are one geometry.
|
|
325
|
+
*/
|
|
223
326
|
function CreditsPanel({ credits, t }) {
|
|
327
|
+
const shown = credits.accounts.filter((account) => account.packageName === "enterprise" || account.remain > 0 || account.unlimited === true);
|
|
328
|
+
if (shown.length === 0) return null;
|
|
224
329
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
225
|
-
className: workbuddy_module_css_default.
|
|
226
|
-
children:
|
|
330
|
+
className: workbuddy_module_css_default.list,
|
|
331
|
+
children: shown.map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
|
|
227
332
|
label: account.packageName === "enterprise" ? t("packageEnterprise") : account.packageName,
|
|
228
333
|
remain: account.remain,
|
|
229
334
|
size: account.size,
|
|
@@ -250,22 +355,34 @@ window.__ModuleLoader__.load({
|
|
|
250
355
|
* budget actually requested, while the larger value is a ceiling the upstream
|
|
251
356
|
* would accept.
|
|
252
357
|
*/
|
|
253
|
-
function ModelsPanel({ models, visibility, toggling, disabled, onToggle, t }) {
|
|
358
|
+
function ModelsPanel({ models, visibility, toggling, disabled, catalog, onToggle, t }) {
|
|
254
359
|
const rows = [...models ?? []].sort((a, b) => {
|
|
255
360
|
if (a.contextWindow === void 0) return b.contextWindow === void 0 ? 0 : 1;
|
|
256
361
|
if (b.contextWindow === void 0) return -1;
|
|
257
362
|
return b.contextWindow - a.contextWindow;
|
|
258
363
|
});
|
|
259
|
-
if (rows.length === 0) return null;
|
|
260
364
|
const hidden = new Set(visibility?.hidden ?? []);
|
|
261
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
365
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
366
|
+
catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
367
|
+
main: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, {
|
|
368
|
+
caption: t("catalogSourceFrom"),
|
|
369
|
+
children: catalogSourceText(catalog, t)
|
|
370
|
+
}),
|
|
371
|
+
note: catalog.fetchedAt === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, {
|
|
372
|
+
caption: t("catalogUpdatedAt"),
|
|
373
|
+
children: formatTime(catalog.fetchedAt)
|
|
374
|
+
})
|
|
375
|
+
}),
|
|
376
|
+
catalog?.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
377
|
+
className: workbuddy_module_css_default.error,
|
|
378
|
+
children: t("catalogError", { message: catalog.error })
|
|
379
|
+
}),
|
|
380
|
+
rows.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
267
381
|
className: workbuddy_module_css_default.list,
|
|
268
|
-
children:
|
|
382
|
+
children: [visibility === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
383
|
+
className: workbuddy_module_css_default.listHeader,
|
|
384
|
+
children: t("visibilityIntro")
|
|
385
|
+
}), rows.map((model) => {
|
|
269
386
|
const capacity = model.contextWindow;
|
|
270
387
|
const alternative = capacity !== void 0 && model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
|
|
271
388
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -279,6 +396,7 @@ window.__ModuleLoader__.load({
|
|
|
279
396
|
checked: !hidden.has(model.id),
|
|
280
397
|
disabled: disabled || toggling.has(model.id),
|
|
281
398
|
label: model.name,
|
|
399
|
+
className: workbuddy_module_css_default.modelCheck,
|
|
282
400
|
onChange: (visible) => {
|
|
283
401
|
onToggle(model.id, visible, visibility.account);
|
|
284
402
|
}
|
|
@@ -315,25 +433,33 @@ window.__ModuleLoader__.load({
|
|
|
315
433
|
]
|
|
316
434
|
})]
|
|
317
435
|
}, model.id);
|
|
318
|
-
})
|
|
319
|
-
})
|
|
320
|
-
});
|
|
436
|
+
})]
|
|
437
|
+
})
|
|
438
|
+
] });
|
|
321
439
|
}
|
|
322
440
|
/**
|
|
323
|
-
* Reasoning
|
|
441
|
+
* Reasoning levels: one row per reasoning model, and what its switch does.
|
|
324
442
|
*
|
|
325
|
-
*
|
|
326
|
-
* - **
|
|
443
|
+
* Three deliberate UX rules:
|
|
444
|
+
* - **The list covers every reasoning model, not just the detectable ones.**
|
|
445
|
+
* Showing only the detectable rows was what made the panel unable to answer
|
|
446
|
+
* "why does this model have no levels?" — the models whose answer was already
|
|
447
|
+
* known were exactly the ones missing from the list;
|
|
448
|
+
* - **one press detects.** The row states the current answer and the button
|
|
327
449
|
* beside it runs the check; the cost is stated once, above the list, instead
|
|
328
450
|
* of being asked again per row. A confirmation whose only other option is
|
|
329
451
|
* "cancel" costs a click and answers nothing;
|
|
330
|
-
* - a
|
|
331
|
-
*
|
|
332
|
-
*
|
|
452
|
+
* - **a settled row says so.** A declared set cannot be improved by detecting,
|
|
453
|
+
* so its button names that instead of offering a run that would spend credit
|
|
454
|
+
* to learn nothing.
|
|
455
|
+
*
|
|
456
|
+
* A `non-validating` result is presented as an observation about the parameter
|
|
457
|
+
* ("this model does not check it"), never as a statement that a level is
|
|
458
|
+
* unsupported.
|
|
333
459
|
*
|
|
334
460
|
* The tab bar owns the heading, so this panel renders no heading of its own.
|
|
335
461
|
*/
|
|
336
|
-
function ProbePanel({ probe, models, busy, onDetect, onClear, t }) {
|
|
462
|
+
function ProbePanel$1({ probe, models, busy, onDetect, onClear, t }) {
|
|
337
463
|
const [runningModel, setRunningModel] = (0, react.useState)();
|
|
338
464
|
const armed = (0, react.useRef)(false);
|
|
339
465
|
(0, react.useEffect)(() => {
|
|
@@ -350,62 +476,62 @@ window.__ModuleLoader__.load({
|
|
|
350
476
|
busy,
|
|
351
477
|
probe.running
|
|
352
478
|
]);
|
|
353
|
-
if (probe.
|
|
479
|
+
if (probe.models.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
354
480
|
className: workbuddy_module_css_default.text,
|
|
355
481
|
children: t("probeResultEmpty")
|
|
356
482
|
});
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
483
|
+
const detected = probe.models.filter((entry) => entry.probedAt !== void 0);
|
|
484
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
485
|
+
main: detected.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, { caption: t("probeDetectedNone") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, {
|
|
486
|
+
caption: t("probeDetectedCaption"),
|
|
487
|
+
after: t("probeDetectedUnit"),
|
|
488
|
+
children: detected.length
|
|
489
|
+
}),
|
|
490
|
+
note: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, { caption: t("probeCostNote") }),
|
|
491
|
+
action: detected.length === 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
492
|
+
size: "sm",
|
|
493
|
+
disabled: busy,
|
|
494
|
+
onClick: onClear,
|
|
495
|
+
children: t("probeClear")
|
|
496
|
+
})
|
|
497
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
498
|
+
className: workbuddy_module_css_default.list,
|
|
499
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
500
|
+
className: workbuddy_module_css_default.listHeader,
|
|
501
|
+
children: t("probeListIntro")
|
|
502
|
+
}), probe.models.map((entry) => {
|
|
503
|
+
const id = entry.id;
|
|
504
|
+
const name = models?.find((model) => model.id === id)?.name ?? entry.name ?? id;
|
|
505
|
+
const hasLevels = entry.efforts.length > 0;
|
|
506
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
507
|
+
className: workbuddy_module_css_default.modelStack,
|
|
508
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
509
|
+
className: workbuddy_module_css_default.probeRow,
|
|
510
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
511
|
+
className: workbuddy_module_css_default.name,
|
|
512
|
+
children: name
|
|
513
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
514
|
+
className: workbuddy_module_css_default.probeEnd,
|
|
515
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
516
|
+
tone: hasLevels ? "success" : "neutral",
|
|
517
|
+
children: hasLevels ? entry.efforts.join(" / ") : t(entry.detectable ? entry.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown" : "probeResultDeclaredNone")
|
|
518
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
519
|
+
size: "sm",
|
|
520
|
+
disabled: busy || probe.running || !entry.detectable,
|
|
521
|
+
onClick: () => {
|
|
522
|
+
setRunningModel(id);
|
|
523
|
+
onDetect(id);
|
|
524
|
+
},
|
|
525
|
+
children: runningModel === id && entry.detectable ? t("probeRunning", { model: name }) : !entry.detectable ? t("probeActionDeclared") : t(entry.probedAt === void 0 ? "probeStart" : "probeRedetect")
|
|
394
526
|
})]
|
|
395
|
-
}
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
onClick: onClear,
|
|
404
|
-
children: t("probeClear")
|
|
405
|
-
})
|
|
406
|
-
})
|
|
407
|
-
]
|
|
408
|
-
});
|
|
527
|
+
})]
|
|
528
|
+
}), entry.probedAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
529
|
+
className: workbuddy_module_css_default.meta,
|
|
530
|
+
children: t("probeResultAt", { time: formatTime(entry.probedAt) })
|
|
531
|
+
})]
|
|
532
|
+
}, id);
|
|
533
|
+
})]
|
|
534
|
+
})] });
|
|
409
535
|
}
|
|
410
536
|
/**
|
|
411
537
|
* The Agent assist block for a path failure: what is wrong, one copyable
|
|
@@ -470,8 +596,156 @@ window.__ModuleLoader__.load({
|
|
|
470
596
|
});
|
|
471
597
|
}
|
|
472
598
|
//#endregion
|
|
599
|
+
//#region src/client/PanelBoundary.tsx
|
|
600
|
+
/**
|
|
601
|
+
* A boundary that keeps one panel's crash from taking the whole card.
|
|
602
|
+
*
|
|
603
|
+
* The host renders each slot under its own error boundary, and that boundary
|
|
604
|
+
* **latches**: once a child throws, the region stays gone for the life of the
|
|
605
|
+
* mount. The user's only way back is to disable and re-enable the plugin. So a
|
|
606
|
+
* throw anywhere inside this card does not cost one panel — it costs the entire
|
|
607
|
+
* configuration area (2026-10-02: `probe.models` was not an array, and switching
|
|
608
|
+
* to the detection tab blanked everything below the plugin's own switch).
|
|
609
|
+
*
|
|
610
|
+
* React's rule is that an error boundary must be a **class** — `getDerivedStateFromError`
|
|
611
|
+
* and `componentDidCatch` have no hook equivalent. That is the only reason this
|
|
612
|
+
* file is not a function component.
|
|
613
|
+
*
|
|
614
|
+
* What it is *not*: a substitute for validating inputs. The boundary is the last
|
|
615
|
+
* line of defence for the failure nobody predicted; the shape guard in
|
|
616
|
+
* `status-document.ts` is what keeps malformed documents from getting this far.
|
|
617
|
+
* Both exist because they fail differently — the guard degrades to "nothing to
|
|
618
|
+
* show", the boundary degrades to "this block says it broke".
|
|
619
|
+
*/
|
|
620
|
+
/**
|
|
621
|
+
* Catch a render error in one panel and render a local fallback instead.
|
|
622
|
+
*
|
|
623
|
+
* The fallback is deliberately plain: a block that broke cannot be trusted to
|
|
624
|
+
* render its own recovery UI, and the one thing the user needs is a way to try
|
|
625
|
+
* again — which is what `reset` gives, by clearing the caught error so the
|
|
626
|
+
* children mount afresh.
|
|
627
|
+
*/
|
|
628
|
+
var PanelBoundary = class extends react.Component {
|
|
629
|
+
state = { message: void 0 };
|
|
630
|
+
static getDerivedStateFromError(error) {
|
|
631
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
632
|
+
}
|
|
633
|
+
componentDidCatch(error, info) {
|
|
634
|
+
console.error(`dsh-workbuddy-bridge: ${this.props.label} failed to render`, error, info);
|
|
635
|
+
}
|
|
636
|
+
render() {
|
|
637
|
+
const { message } = this.state;
|
|
638
|
+
if (message === void 0) return this.props.children;
|
|
639
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
640
|
+
className: workbuddy_module_css_default.section,
|
|
641
|
+
role: "alert",
|
|
642
|
+
children: [
|
|
643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
644
|
+
className: workbuddy_module_css_default.error,
|
|
645
|
+
children: this.props.t("panelCrashed", { panel: this.props.label })
|
|
646
|
+
}),
|
|
647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
648
|
+
className: workbuddy_module_css_default.dim,
|
|
649
|
+
children: message
|
|
650
|
+
}),
|
|
651
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
652
|
+
className: workbuddy_module_css_default.sectionActions,
|
|
653
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
654
|
+
size: "sm",
|
|
655
|
+
onClick: () => {
|
|
656
|
+
this.setState({ message: void 0 });
|
|
657
|
+
},
|
|
658
|
+
children: this.props.t("panelRetry")
|
|
659
|
+
})
|
|
660
|
+
})
|
|
661
|
+
]
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/coin-glyph.module.css.js
|
|
667
|
+
const css$3 = ".W7jfGW_glyph{flex:none;align-self:center}";
|
|
668
|
+
const tagId$3 = "dsh-workbuddy-bridge/coin-glyph.module.css";
|
|
669
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
670
|
+
const tag = document.createElement("style");
|
|
671
|
+
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
672
|
+
tag.dataset.pluginCss = tagId$3;
|
|
673
|
+
tag.textContent = css$3;
|
|
674
|
+
document.head.appendChild(tag);
|
|
675
|
+
}
|
|
676
|
+
var coin_glyph_module_css_default = { "glyph": "W7jfGW_glyph" };
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/client/coin-glyph.tsx
|
|
679
|
+
/** Render the coin-stack glyph. Decorative: the caller supplies the accessible name. */
|
|
680
|
+
function CoinGlyph() {
|
|
681
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
682
|
+
width: "16",
|
|
683
|
+
height: "16",
|
|
684
|
+
viewBox: "0 0 16 16",
|
|
685
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
686
|
+
fill: "none",
|
|
687
|
+
stroke: "currentColor",
|
|
688
|
+
strokeWidth: "1",
|
|
689
|
+
"aria-hidden": "true",
|
|
690
|
+
className: coin_glyph_module_css_default.glyph,
|
|
691
|
+
children: [
|
|
692
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ellipse", {
|
|
693
|
+
cx: "8",
|
|
694
|
+
cy: "4.7",
|
|
695
|
+
rx: "4.7",
|
|
696
|
+
ry: "1.25"
|
|
697
|
+
}),
|
|
698
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.3 4.7V10.7A4.7 1.25 0 0 0 12.7 10.7V4.7" }),
|
|
699
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3.3 7.7A4.7 1.25 0 0 0 12.7 7.7" })
|
|
700
|
+
]
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
//#endregion
|
|
473
704
|
//#region src/client/status-document.ts
|
|
474
705
|
/**
|
|
706
|
+
* Whether one probe row is safe to read.
|
|
707
|
+
*
|
|
708
|
+
* `efforts` is the field the readers actually dereference (`.length`, `.join`),
|
|
709
|
+
* so it is the one that must be an array — a row whose `efforts` is absent or a
|
|
710
|
+
* string would throw inside the panel rather than degrade.
|
|
711
|
+
*/
|
|
712
|
+
function isEffortRow(value) {
|
|
713
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
714
|
+
const row = value;
|
|
715
|
+
return typeof row["id"] === "string" && Array.isArray(row["efforts"]);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Whether the `probe` section is safe to read.
|
|
719
|
+
*
|
|
720
|
+
* Validated to the depth the readers go, and no further. `consent` and `running`
|
|
721
|
+
* are only ever compared, so a missing value degrades correctly on its own;
|
|
722
|
+
* `models` is iterated and each row's `efforts` is measured, so those two must
|
|
723
|
+
* actually be arrays.
|
|
724
|
+
*
|
|
725
|
+
* The depth matters in both directions. Too shallow and a malformed document
|
|
726
|
+
* throws inside the panel — which the host's slot boundary turns into a
|
|
727
|
+
* *latched* failure, so the whole configuration area disappears until the plugin
|
|
728
|
+
* is toggled (2026-10-02, reached through a client-bundle hot reload that
|
|
729
|
+
* changed the document shape under a mounted component). Too deep and the guard
|
|
730
|
+
* starts rejecting documents the host legitimately omits fields from.
|
|
731
|
+
*/
|
|
732
|
+
function isProbeSection(value) {
|
|
733
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
734
|
+
const models = value["models"];
|
|
735
|
+
return Array.isArray(models) && models.every(isEffortRow);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Whether the `visibility` section is safe to read.
|
|
739
|
+
*
|
|
740
|
+
* `hidden` is spread into a `Set` by the models panel, which throws on a
|
|
741
|
+
* non-iterable; the account key is only compared, so it is left alone.
|
|
742
|
+
*/
|
|
743
|
+
function isVisibilitySection(value) {
|
|
744
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
745
|
+
const hidden = value["hidden"];
|
|
746
|
+
return hidden === void 0 || Array.isArray(hidden) && hidden.every((id) => typeof id === "string");
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
475
749
|
* Whether a parsed status response really is a status document.
|
|
476
750
|
*
|
|
477
751
|
* A 200 is not a promise about the body: it may be empty, literal `null`, a
|
|
@@ -479,9 +753,12 @@ window.__ModuleLoader__.load({
|
|
|
479
753
|
* read the same route, so both must agree on what is valid — storing an
|
|
480
754
|
* unreadable value puts something in state that the next render dereferences.
|
|
481
755
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
756
|
+
* Beyond the discriminator this validates the two nested structures a reader
|
|
757
|
+
* iterates **without** a guard: `probe.models` and `visibility.hidden`. Those
|
|
758
|
+
* are the ones that turn a malformed document into a thrown render rather than
|
|
759
|
+
* a degraded one. Everything else stays optional on purpose — validating
|
|
760
|
+
* optional fields would reject documents the host legitimately omits fields
|
|
761
|
+
* from, and a reader that only compares a value degrades correctly without help.
|
|
485
762
|
*
|
|
486
763
|
* `reasonCode` is therefore *not* rejected here — a card renders `reason`
|
|
487
764
|
* either way — but every reader must narrow it with
|
|
@@ -492,8 +769,35 @@ window.__ModuleLoader__.load({
|
|
|
492
769
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
493
770
|
const wrapped = value;
|
|
494
771
|
const status = wrapped["status"];
|
|
495
|
-
if (status === "
|
|
496
|
-
|
|
772
|
+
if (status === "error") return typeof wrapped["message"] === "string";
|
|
773
|
+
if (status !== "signed-out" && status !== "signed-in") return false;
|
|
774
|
+
const probe = wrapped["probe"];
|
|
775
|
+
if (probe !== void 0 && !isProbeSection(probe)) return false;
|
|
776
|
+
const visibility = wrapped["visibility"];
|
|
777
|
+
return visibility === void 0 || isVisibilitySection(visibility);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Whether a parsed credit response really is a credit document.
|
|
781
|
+
*
|
|
782
|
+
* Same reasoning as {@link isWorkBuddyWebStatus}: a 200 promises nothing about
|
|
783
|
+
* the body. Stricter about the payload than the status check because this one
|
|
784
|
+
* *is* the data — every entry is read and formatted — so a map holding a string
|
|
785
|
+
* or a `NaN` where a number belongs would reach the label. Entries that are not
|
|
786
|
+
* finite non-negative numbers are dropped rather than rejecting the whole
|
|
787
|
+
* document: one malformed row must not blank every other message's label.
|
|
788
|
+
*/
|
|
789
|
+
function isWorkBuddySessionCredits(value) {
|
|
790
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
791
|
+
const wrapped = value;
|
|
792
|
+
if (typeof wrapped["sessionId"] !== "string") return false;
|
|
793
|
+
const credits = wrapped["credits"];
|
|
794
|
+
if (typeof credits !== "object" || credits === null || Array.isArray(credits)) return false;
|
|
795
|
+
const record = credits;
|
|
796
|
+
for (const key of Object.keys(record)) {
|
|
797
|
+
const credit = record[key];
|
|
798
|
+
if (typeof credit !== "number" || !Number.isFinite(credit) || credit < 0) delete record[key];
|
|
799
|
+
}
|
|
800
|
+
return true;
|
|
497
801
|
}
|
|
498
802
|
//#endregion
|
|
499
803
|
//#region src/client/use-status.ts
|
|
@@ -520,7 +824,7 @@ window.__ModuleLoader__.load({
|
|
|
520
824
|
* document rather than in place of it.
|
|
521
825
|
*/
|
|
522
826
|
/** How often the status document is re-read while a variant is signed in. */
|
|
523
|
-
const POLL_INTERVAL_MS = 6e4;
|
|
827
|
+
const POLL_INTERVAL_MS$2 = 6e4;
|
|
524
828
|
/**
|
|
525
829
|
* Own one variant's status document.
|
|
526
830
|
* @param variant - which product's routes and copy to use.
|
|
@@ -661,28 +965,39 @@ window.__ModuleLoader__.load({
|
|
|
661
965
|
const timer = window.setInterval(() => {
|
|
662
966
|
if (signedOut.current) return;
|
|
663
967
|
read(controller.signal);
|
|
664
|
-
}, POLL_INTERVAL_MS);
|
|
968
|
+
}, POLL_INTERVAL_MS$2);
|
|
665
969
|
return () => {
|
|
666
970
|
window.clearInterval(timer);
|
|
667
971
|
controller.abort();
|
|
668
972
|
};
|
|
669
973
|
}, [read]);
|
|
974
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
975
|
+
setBusy(true);
|
|
976
|
+
const controller = track();
|
|
977
|
+
try {
|
|
978
|
+
await read(controller.signal);
|
|
979
|
+
} finally {
|
|
980
|
+
inFlight.current.delete(controller);
|
|
981
|
+
if (mounted.current) setBusy(false);
|
|
982
|
+
}
|
|
983
|
+
}, [read, track]);
|
|
670
984
|
return {
|
|
671
985
|
status,
|
|
672
986
|
readFailure,
|
|
673
987
|
busy,
|
|
674
988
|
toggling,
|
|
675
|
-
refresh
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
} finally {
|
|
681
|
-
inFlight.current.delete(controller);
|
|
682
|
-
if (mounted.current) setBusy(false);
|
|
989
|
+
refresh,
|
|
990
|
+
refreshAll: (0, react.useCallback)(async () => {
|
|
991
|
+
if ((status?.status === "signed-in" ? status.probeKey : void 0) === void 0) {
|
|
992
|
+
await refresh();
|
|
993
|
+
return;
|
|
683
994
|
}
|
|
684
|
-
|
|
685
|
-
|
|
995
|
+
await control({ action: "refresh" });
|
|
996
|
+
}, [
|
|
997
|
+
control,
|
|
998
|
+
refresh,
|
|
999
|
+
status
|
|
1000
|
+
]),
|
|
686
1001
|
detect: (0, react.useCallback)((model) => control({
|
|
687
1002
|
action: "probe",
|
|
688
1003
|
model
|
|
@@ -719,6 +1034,18 @@ window.__ModuleLoader__.load({
|
|
|
719
1034
|
* reasoning-effort detection.
|
|
720
1035
|
*/
|
|
721
1036
|
/**
|
|
1037
|
+
* Locale key per tab, so the crash fallback can name the block it replaced.
|
|
1038
|
+
*
|
|
1039
|
+
* The tabs' own `label` field is a `ReactNode` (the host's `SegmentedTab` allows
|
|
1040
|
+
* a node), so it cannot be fed back to `t()`. This map is the one place that
|
|
1041
|
+
* knows both the tab identity and the key behind its label.
|
|
1042
|
+
*/
|
|
1043
|
+
const TAB_LABEL_KEY = {
|
|
1044
|
+
credits: "tabCredits",
|
|
1045
|
+
models: "tabModels",
|
|
1046
|
+
probe: "tabProbe"
|
|
1047
|
+
};
|
|
1048
|
+
/**
|
|
722
1049
|
* The state dot's semantic.
|
|
723
1050
|
*
|
|
724
1051
|
* Takes `'loading'` as well as the document's own states: before the first
|
|
@@ -731,23 +1058,42 @@ window.__ModuleLoader__.load({
|
|
|
731
1058
|
if (status.status === "signed-in") return "done";
|
|
732
1059
|
return status.status === "error" ? "error" : "idle";
|
|
733
1060
|
}
|
|
734
|
-
/**
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* What the account state says, in one place.
|
|
1063
|
+
*
|
|
1064
|
+
* **One home, because two homes already disagreed.** The collapsed header renders
|
|
1065
|
+
* this as one string and the expanded block renders it as a caption plus a value;
|
|
1066
|
+
* when each derived the line itself, the body branched on *having an identity*
|
|
1067
|
+
* instead of on the sign-in state — and since a signed-out document has no
|
|
1068
|
+
* identity either, a signed-out card read 未登录 in the header and 已登录 in the
|
|
1069
|
+
* body, with the state dot correctly unlit beside them (reported 2026-10-03).
|
|
1070
|
+
*
|
|
1071
|
+
* The identity is the nickname when the desktop app reports one, otherwise the
|
|
1072
|
+
* account id: the credential does not always carry a nickname, while the id is
|
|
1073
|
+
* what the per-account state (hidden models, probe records) is keyed by, so it is
|
|
1074
|
+
* the one name that is always there.
|
|
1075
|
+
*/
|
|
1076
|
+
function accountLine(status, t) {
|
|
1077
|
+
if (status === void 0) return { caption: t("loading") };
|
|
1078
|
+
if (status.status === "signed-in") {
|
|
1079
|
+
const identity = status.nickname ?? status.uid;
|
|
1080
|
+
return identity === void 0 ? { caption: t("signedIn") } : {
|
|
1081
|
+
caption: t("signedInLabel"),
|
|
1082
|
+
value: identity
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
return { caption: status.status === "error" ? t("requestFailed") : t("signedOut") };
|
|
739
1086
|
}
|
|
740
|
-
/**
|
|
741
|
-
function
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
return catalog.appVersion === void 0 ? line : `${line} · ${t("catalogAppVersion", { version: catalog.appVersion })}`;
|
|
1087
|
+
/** The same line flattened for the collapsed header, where it is one string. */
|
|
1088
|
+
function accountLabel(status, t) {
|
|
1089
|
+
const { caption, value } = accountLine(status, t);
|
|
1090
|
+
return value === void 0 ? caption : `${caption} ${value}`;
|
|
745
1091
|
}
|
|
746
1092
|
/** Render one variant's live status card. */
|
|
747
1093
|
function WorkBuddyCard({ variant, t }) {
|
|
748
1094
|
const [open, setOpen] = (0, react.useState)(false);
|
|
749
1095
|
const [tab, setTab] = (0, react.useState)("credits");
|
|
750
|
-
const { status, readFailure, busy, toggling, refresh,
|
|
1096
|
+
const { status, readFailure, busy, toggling, refresh, refreshAll, detect, clearDetections, setVisibility } = useWorkBuddyStatus(variant, t);
|
|
751
1097
|
const baseId = (0, react.useId)();
|
|
752
1098
|
const creditsTab = {
|
|
753
1099
|
value: "credits",
|
|
@@ -780,6 +1126,7 @@ window.__ModuleLoader__.load({
|
|
|
780
1126
|
*/
|
|
781
1127
|
const assistCode = status?.status === "signed-out" ? assistCodeFor(status.reasonCode) : void 0;
|
|
782
1128
|
const signedIn = status?.status === "signed-in" ? status : void 0;
|
|
1129
|
+
const account = accountLine(status, t);
|
|
783
1130
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
|
|
784
1131
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState(status) }),
|
|
785
1132
|
title: t(variant.titleKey),
|
|
@@ -800,78 +1147,48 @@ window.__ModuleLoader__.load({
|
|
|
800
1147
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
801
1148
|
className: workbuddy_module_css_default.body,
|
|
802
1149
|
children: [
|
|
1150
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1151
|
+
className: workbuddy_module_css_default.cardActions,
|
|
1152
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1153
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutlineRegular, {}),
|
|
1154
|
+
disabled: busy,
|
|
1155
|
+
onClick: () => {
|
|
1156
|
+
refreshAll();
|
|
1157
|
+
},
|
|
1158
|
+
children: busy ? t("refreshingAll") : t("refreshAll")
|
|
1159
|
+
})
|
|
1160
|
+
}),
|
|
803
1161
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
804
1162
|
className: workbuddy_module_css_default.section,
|
|
805
|
-
children: [
|
|
806
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
807
|
-
|
|
808
|
-
children:
|
|
809
|
-
}),
|
|
810
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
811
|
-
className: workbuddy_module_css_default.row,
|
|
812
|
-
children: assistCode === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
813
|
-
size: "sm",
|
|
814
|
-
disabled: busy,
|
|
815
|
-
onClick: () => {
|
|
816
|
-
refresh();
|
|
817
|
-
},
|
|
818
|
-
children: busy ? t("refreshing") : t("refresh")
|
|
819
|
-
}) : null
|
|
820
|
-
}),
|
|
821
|
-
readFailure === void 0 || status === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
822
|
-
className: workbuddy_module_css_default.error,
|
|
823
|
-
children: t("statusRefreshFailed", { message: readFailure })
|
|
1163
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1164
|
+
main: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, {
|
|
1165
|
+
caption: account.caption,
|
|
1166
|
+
children: account.value === void 0 ? void 0 : account.value
|
|
824
1167
|
}),
|
|
825
|
-
signedIn?.expiresAt === void 0 ?
|
|
826
|
-
|
|
827
|
-
children:
|
|
1168
|
+
note: signedIn?.expiresAt === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Part, {
|
|
1169
|
+
caption: t("sessionExpiryLabel"),
|
|
1170
|
+
children: formatTime(signedIn.expiresAt)
|
|
828
1171
|
})
|
|
829
|
-
|
|
830
|
-
}),
|
|
831
|
-
signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
832
|
-
signedIn.catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
833
|
-
className: workbuddy_module_css_default.row,
|
|
834
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
835
|
-
className: workbuddy_module_css_default.text,
|
|
836
|
-
children: catalogProvenance(signedIn.catalog, t)
|
|
837
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
838
|
-
size: "sm",
|
|
839
|
-
disabled: busy,
|
|
840
|
-
onClick: () => {
|
|
841
|
-
refreshModels();
|
|
842
|
-
},
|
|
843
|
-
children: busy ? t("refreshingModels") : t("refreshModels")
|
|
844
|
-
})]
|
|
845
|
-
}),
|
|
846
|
-
signedIn.catalog?.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1172
|
+
}), readFailure === void 0 || status === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
847
1173
|
className: workbuddy_module_css_default.error,
|
|
848
|
-
children: t("
|
|
849
|
-
})
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1174
|
+
children: t("statusRefreshFailed", { message: readFailure })
|
|
1175
|
+
})]
|
|
1176
|
+
}),
|
|
1177
|
+
signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
|
|
1178
|
+
items: tabs,
|
|
1179
|
+
value: tab,
|
|
1180
|
+
onChange: setTab,
|
|
1181
|
+
label: t("tabLabel")
|
|
1182
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelBoundary, {
|
|
1183
|
+
label: t(TAB_LABEL_KEY[tab]),
|
|
1184
|
+
t,
|
|
1185
|
+
children: tab === "credits" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1186
|
+
className: workbuddy_module_css_default.panel,
|
|
858
1187
|
id: creditsTab.panelId,
|
|
859
1188
|
role: "tabpanel",
|
|
860
1189
|
"aria-labelledby": creditsTab.id,
|
|
861
1190
|
children: [
|
|
862
|
-
signedIn.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
863
|
-
className: workbuddy_module_css_default.section,
|
|
864
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
865
|
-
className: workbuddy_module_css_default.row,
|
|
866
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
867
|
-
className: workbuddy_module_css_default.title,
|
|
868
|
-
children: signedIn.credits.unlimited === true ? t("creditsTotalUnlimited") : t("creditsTotal", { total: formatNumber(signedIn.credits.total) })
|
|
869
|
-
})
|
|
870
|
-
}), signedIn.credits.cycleResetTime === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
871
|
-
className: workbuddy_module_css_default.dim,
|
|
872
|
-
children: t("cycleResetAt", { time: formatCycleReset(signedIn.credits.cycleResetTime) })
|
|
873
|
-
})]
|
|
874
|
-
}),
|
|
1191
|
+
signedIn.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, { main: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Part, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CoinGlyph, {}), signedIn.credits.unlimited === true ? t("creditsTotalUnlimitedValue") : formatNumber(signedIn.credits.total)] }) }),
|
|
875
1192
|
signedIn.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditsPanel, {
|
|
876
1193
|
credits: signedIn.credits,
|
|
877
1194
|
t
|
|
@@ -882,7 +1199,7 @@ window.__ModuleLoader__.load({
|
|
|
882
1199
|
})
|
|
883
1200
|
]
|
|
884
1201
|
}) : tab === "models" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
885
|
-
className: workbuddy_module_css_default.
|
|
1202
|
+
className: workbuddy_module_css_default.panel,
|
|
886
1203
|
id: modelsTab.panelId,
|
|
887
1204
|
role: "tabpanel",
|
|
888
1205
|
"aria-labelledby": modelsTab.id,
|
|
@@ -891,17 +1208,18 @@ window.__ModuleLoader__.load({
|
|
|
891
1208
|
visibility: signedIn.visibility,
|
|
892
1209
|
toggling,
|
|
893
1210
|
disabled: busy,
|
|
1211
|
+
catalog: signedIn.catalog,
|
|
894
1212
|
t,
|
|
895
1213
|
onToggle: (model, visible, account) => {
|
|
896
1214
|
setVisibility(model, visible, account);
|
|
897
1215
|
}
|
|
898
1216
|
})
|
|
899
1217
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
900
|
-
className: workbuddy_module_css_default.
|
|
1218
|
+
className: workbuddy_module_css_default.panel,
|
|
901
1219
|
id: probeTab.panelId,
|
|
902
1220
|
role: "tabpanel",
|
|
903
1221
|
"aria-labelledby": probeTab.id,
|
|
904
|
-
children: signedIn.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbePanel, {
|
|
1222
|
+
children: signedIn.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbePanel$1, {
|
|
905
1223
|
probe: signedIn.probe,
|
|
906
1224
|
models: signedIn.models,
|
|
907
1225
|
busy,
|
|
@@ -914,7 +1232,7 @@ window.__ModuleLoader__.load({
|
|
|
914
1232
|
}
|
|
915
1233
|
})
|
|
916
1234
|
})
|
|
917
|
-
] }),
|
|
1235
|
+
}, tab)] }),
|
|
918
1236
|
status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
919
1237
|
className: status.reason === void 0 ? workbuddy_module_css_default.text : workbuddy_module_css_default.error,
|
|
920
1238
|
children: status.reason ?? t(variant.signedOutKey)
|
|
@@ -935,21 +1253,68 @@ window.__ModuleLoader__.load({
|
|
|
935
1253
|
})
|
|
936
1254
|
});
|
|
937
1255
|
}
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/shared/paths.ts
|
|
1258
|
+
/** Node-free constants and types shared by the Host and browser halves. */
|
|
1259
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
1260
|
+
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/status";
|
|
1261
|
+
/**
|
|
1262
|
+
* Plugin-owned probe control endpoint.
|
|
1263
|
+
*
|
|
1264
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
1265
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
1266
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
1267
|
+
* also requires the in-memory key the browser half receives with the status
|
|
1268
|
+
* document.
|
|
1269
|
+
*/
|
|
1270
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/probe";
|
|
1271
|
+
/**
|
|
1272
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
1273
|
+
*
|
|
1274
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
1275
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
1276
|
+
* independently, and a shared expression is one build-config drift away from
|
|
1277
|
+
* the desk asking a route the host never mounted.
|
|
1278
|
+
*/
|
|
1279
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-bridge/ai/status";
|
|
1280
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-bridge/ai/probe";
|
|
1281
|
+
/**
|
|
1282
|
+
* Plugin-owned per-message credit route.
|
|
1283
|
+
*
|
|
1284
|
+
* A read-only GET taking `?sessionId=…`, answering `messageId → credit` for one
|
|
1285
|
+
* Session. It is its own route rather than a field of the status document
|
|
1286
|
+
* because the two have different lifetimes: the status document is per
|
|
1287
|
+
* *account* and is re-read when the account changes, while credit accounting
|
|
1288
|
+
* is per *Session* and grows as messages are sent.
|
|
1289
|
+
*
|
|
1290
|
+
* **One route for both variants, on purpose.** The accounting is attributed to
|
|
1291
|
+
* a DSH `messageId` before it is stored, and a message id belongs to one
|
|
1292
|
+
* Session regardless of which provider served it — a conversation can switch
|
|
1293
|
+
* between the two WorkBuddy products (or away from both) mid-thread. A route
|
|
1294
|
+
* per variant would force the message row to know which provider answered a
|
|
1295
|
+
* message *it is merely labelling*, and would strand every message whose turn
|
|
1296
|
+
* ran under the other variant.
|
|
1297
|
+
*
|
|
1298
|
+
* No key is required: the route only reads what this plugin already observed on
|
|
1299
|
+
* its own loopback stream, carries no credential material, and answers loopback
|
|
1300
|
+
* requests through the same Host/Origin guard as the status route.
|
|
1301
|
+
*/
|
|
1302
|
+
const WORKBUDDY_CREDIT_PATH = "/plugins/dsh-workbuddy-bridge/credit";
|
|
938
1303
|
/** Both variants, in display order. */
|
|
939
1304
|
const CARD_VARIANTS = [{
|
|
940
1305
|
id: "workbuddy",
|
|
941
1306
|
titleKey: "title",
|
|
942
1307
|
signedOutKey: "signedOutHint",
|
|
943
|
-
statusPath:
|
|
944
|
-
probePath:
|
|
1308
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
1309
|
+
probePath: WORKBUDDY_PROBE_PATH,
|
|
945
1310
|
appName: "WorkBuddy",
|
|
946
1311
|
unavailableKey: "assistUnavailableCN"
|
|
947
1312
|
}, {
|
|
948
1313
|
id: "workbuddy-ai",
|
|
949
1314
|
titleKey: "titleAI",
|
|
950
1315
|
signedOutKey: "signedOutHintAI",
|
|
951
|
-
statusPath:
|
|
952
|
-
probePath:
|
|
1316
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
1317
|
+
probePath: WORKBUDDY_AI_PROBE_PATH,
|
|
953
1318
|
appName: "WorkBuddy AI",
|
|
954
1319
|
unavailableKey: "assistUnavailableAI"
|
|
955
1320
|
}];
|
|
@@ -969,35 +1334,33 @@ window.__ModuleLoader__.load({
|
|
|
969
1334
|
signedOutHint: "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
|
|
970
1335
|
signedOutHintAI: "Sign in once in the WorkBuddy AI desktop app; this plugin follows that sign-in automatically.",
|
|
971
1336
|
signedIn: "Signed in",
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
accountHeading: "Account",
|
|
1337
|
+
signedInLabel: "Signed in as",
|
|
1338
|
+
sessionExpiryLabel: "Expires",
|
|
975
1339
|
requestFailed: "Request failed",
|
|
976
1340
|
statusRefreshFailed: "Refresh failed: {message} — showing the last known state",
|
|
977
1341
|
statusResponseInvalid: "WorkBuddy returned an unreadable status reply",
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1342
|
+
catalogSourceFrom: "From",
|
|
1343
|
+
catalogSourceLive: "the app",
|
|
1344
|
+
catalogSourceSaved: "a saved copy",
|
|
1345
|
+
catalogSourceFallback: "the built-in list",
|
|
1346
|
+
catalogUpdatedAt: "Updated",
|
|
981
1347
|
catalogError: "Last update failed: {message}",
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
refreshing: "Refreshing…",
|
|
985
|
-
refreshModels: "Refresh model list",
|
|
986
|
-
refreshingModels: "Refreshing models…",
|
|
1348
|
+
refreshAll: "Refresh all",
|
|
1349
|
+
refreshingAll: "Refreshing…",
|
|
987
1350
|
tabLabel: "WorkBuddy detail",
|
|
988
1351
|
tabCredits: "Credits",
|
|
989
1352
|
tabModels: "Models",
|
|
990
1353
|
tabProbe: "Detection",
|
|
991
|
-
|
|
992
|
-
creditsTotalUnlimited: "Total: Unlimited",
|
|
1354
|
+
creditsTotalUnlimitedValue: "Unlimited",
|
|
993
1355
|
unlimitedQuota: "Unlimited",
|
|
994
1356
|
packageEnterprise: "Enterprise quota",
|
|
995
|
-
|
|
996
|
-
percentRemaining: "{percent}% remaining",
|
|
997
|
-
percentUnknown: "Remaining share unknown",
|
|
998
|
-
exactRemaining: "{remain} / {size} remaining",
|
|
1357
|
+
packageRemaining: "{remain} / {size} left ({percent}%)",
|
|
999
1358
|
creditPackageUnknownSize: "{remain} remaining",
|
|
1000
1359
|
creditsError: "Credit unavailable: {message}",
|
|
1360
|
+
creditConsumed: "Used {credit}",
|
|
1361
|
+
creditConsumedTitle: "Credit this message consumed, as reported by WorkBuddy",
|
|
1362
|
+
creditRemainingUnlimited: "Unlimited",
|
|
1363
|
+
creditBalanceTitle: "Remaining credit on your {product} account",
|
|
1001
1364
|
freeModel: "Free",
|
|
1002
1365
|
badgeLimitedFree: "Limited-time free",
|
|
1003
1366
|
badgeNightDiscount: "Night discount",
|
|
@@ -1009,27 +1372,40 @@ window.__ModuleLoader__.load({
|
|
|
1009
1372
|
contextUnknown: "no declared context window",
|
|
1010
1373
|
visibilityIntro: "Uncheck a model to hide it from the picker. The choice is saved per signed-in account; chats already using a hidden model keep working.",
|
|
1011
1374
|
visibilityStaleAccount: "The signed-in account changed — this change was not saved.",
|
|
1012
|
-
|
|
1375
|
+
probeDetectedCaption: "Detected",
|
|
1376
|
+
probeDetectedUnit: "",
|
|
1377
|
+
probeDetectedNone: "Nothing detected yet",
|
|
1378
|
+
/** Why the list contains models that need no detection — the list's own header. */
|
|
1379
|
+
probeListIntro: "Some models reason but declare no selectable effort levels.",
|
|
1380
|
+
/** What a run costs — the explanation belonging to the action. */
|
|
1381
|
+
probeCostNote: "Detecting sends a few real requests to one model and may consume a small amount of credit.",
|
|
1013
1382
|
probeStart: "Detect",
|
|
1014
1383
|
probeRedetect: "Detect again",
|
|
1015
1384
|
probeRunning: "Detecting {model}…",
|
|
1016
1385
|
probeClear: "Clear results",
|
|
1017
1386
|
probeResultNotValidating: "This model does not check the effort parameter",
|
|
1018
|
-
probeResultUnknown: "
|
|
1387
|
+
probeResultUnknown: "Not detected yet",
|
|
1388
|
+
probeResultDeclaredNone: "The provider declares no levels",
|
|
1019
1389
|
probeResultAt: "Detected {time}",
|
|
1020
|
-
probeResultEmpty: "No models
|
|
1021
|
-
|
|
1390
|
+
probeResultEmpty: "No reasoning models are being served right now.",
|
|
1391
|
+
probeActionDeclared: "Declared",
|
|
1022
1392
|
probeLabel: "Reasoning levels",
|
|
1023
1393
|
probePanelLevels: "Supported levels",
|
|
1024
|
-
|
|
1394
|
+
probePanelNoLevels: "No level can be selected",
|
|
1025
1395
|
probePanelNote: "Detection sends a few requests to this model and may consume a small amount of credit.",
|
|
1026
1396
|
probePanelDetect: "Detect",
|
|
1027
1397
|
probePanelDetecting: "Detecting…",
|
|
1028
1398
|
probePanelRedetect: "Detect again",
|
|
1399
|
+
probePanelNotNeeded: "Already declared — nothing to detect",
|
|
1029
1400
|
probePanelNotValidating: "This model does not check the effort parameter.",
|
|
1401
|
+
probePanelDeclaredNone: "This model declares no selectable levels.",
|
|
1030
1402
|
probePanelFailed: "Detection did not finish. You can run it again.",
|
|
1031
|
-
|
|
1032
|
-
|
|
1403
|
+
panelCrashed: "The {panel} section stopped working.",
|
|
1404
|
+
panelRetry: "Try again",
|
|
1405
|
+
probeTooltipIdle: "This model offers no levels yet — click to detect them",
|
|
1406
|
+
probeTooltipRunning: "Detecting…",
|
|
1407
|
+
probeTooltipLevels: "Detected levels: {levels}",
|
|
1408
|
+
probeTooltipDeclared: "Levels declared by the provider: {levels}",
|
|
1033
1409
|
probeTooltipNotValidating: "This model ignores the reasoning-level parameter",
|
|
1034
1410
|
probeTooltipFailed: "Detection did not finish · click to run it again",
|
|
1035
1411
|
authFile: "WorkBuddy auth file",
|
|
@@ -1073,35 +1449,33 @@ window.__ModuleLoader__.load({
|
|
|
1073
1449
|
signedOutHint: "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
1074
1450
|
signedOutHintAI: "在 WorkBuddy AI 国际版桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
1075
1451
|
signedIn: "已登录",
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1452
|
+
signedInLabel: "已登录",
|
|
1453
|
+
sessionExpiryLabel: "有效期至",
|
|
1454
|
+
catalogSourceFrom: "来自",
|
|
1455
|
+
catalogSourceLive: "客户端实时读取",
|
|
1456
|
+
catalogSourceSaved: "已保存的副本",
|
|
1457
|
+
catalogSourceFallback: "内置列表",
|
|
1458
|
+
catalogUpdatedAt: "更新于",
|
|
1079
1459
|
requestFailed: "请求失败",
|
|
1080
1460
|
statusRefreshFailed: "刷新失败:{message} — 当前显示的是上次成功获取的状态",
|
|
1081
1461
|
statusResponseInvalid: "WorkBuddy 返回的状态数据无法识别",
|
|
1082
|
-
catalogLive: "模型列表更新于 {time}",
|
|
1083
|
-
catalogSaved: "当前显示已保存的模型列表,更新于 {time}",
|
|
1084
|
-
catalogFallback: "当前显示内置模型列表(尚未从 WorkBuddy 更新)",
|
|
1085
1462
|
catalogError: "上次更新失败:{message}",
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
refreshing: "正在刷新…",
|
|
1089
|
-
refreshModels: "刷新模型列表",
|
|
1090
|
-
refreshingModels: "正在刷新模型…",
|
|
1463
|
+
refreshAll: "刷新全部",
|
|
1464
|
+
refreshingAll: "正在刷新…",
|
|
1091
1465
|
tabLabel: "WorkBuddy 详情",
|
|
1092
1466
|
tabCredits: "积分",
|
|
1093
1467
|
tabModels: "模型",
|
|
1094
1468
|
tabProbe: "检测",
|
|
1095
|
-
|
|
1096
|
-
creditsTotalUnlimited: "合计:不限额",
|
|
1469
|
+
creditsTotalUnlimitedValue: "不限额",
|
|
1097
1470
|
unlimitedQuota: "不限额",
|
|
1098
1471
|
packageEnterprise: "企业额度",
|
|
1099
|
-
|
|
1100
|
-
percentRemaining: "剩余 {percent}%",
|
|
1101
|
-
percentUnknown: "剩余占比未知",
|
|
1102
|
-
exactRemaining: "剩余 {remain} / {size}",
|
|
1472
|
+
packageRemaining: "剩余 {remain} / {size}({percent}%)",
|
|
1103
1473
|
creditPackageUnknownSize: "剩余 {remain}",
|
|
1104
1474
|
creditsError: "积分查询失败:{message}",
|
|
1475
|
+
creditConsumed: "共消耗 {credit}",
|
|
1476
|
+
creditConsumedTitle: "本条消息消耗的积分(由 WorkBuddy 返回)",
|
|
1477
|
+
creditRemainingUnlimited: "不限额",
|
|
1478
|
+
creditBalanceTitle: "{product} 账号剩余积分",
|
|
1105
1479
|
freeModel: "免费",
|
|
1106
1480
|
badgeLimitedFree: "限时免费",
|
|
1107
1481
|
badgeNightDiscount: "夜间折扣",
|
|
@@ -1113,27 +1487,38 @@ window.__ModuleLoader__.load({
|
|
|
1113
1487
|
contextUnknown: "未声明上下文窗口",
|
|
1114
1488
|
visibilityIntro: "取消勾选即可将该模型从选择器中隐藏;按登录账号分别保存,已在用该模型的会话不受影响。",
|
|
1115
1489
|
visibilityStaleAccount: "登录账号已切换——本次修改未保存。",
|
|
1116
|
-
|
|
1490
|
+
probeDetectedCaption: "已检测",
|
|
1491
|
+
probeDetectedUnit: "个模型",
|
|
1492
|
+
probeDetectedNone: "尚未检测",
|
|
1493
|
+
probeListIntro: "部分模型具备思考能力,但没有声明可选档位。",
|
|
1494
|
+
probeCostNote: "检测会向一个模型发送少量真实请求以确认可用档位,可能消耗少量积分。",
|
|
1117
1495
|
probeStart: "开始检测",
|
|
1118
1496
|
probeRedetect: "重新检测",
|
|
1119
1497
|
probeRunning: "正在检测 {model}…",
|
|
1120
1498
|
probeClear: "清除结果",
|
|
1121
1499
|
probeResultNotValidating: "该模型不校验档位参数",
|
|
1122
|
-
probeResultUnknown: "
|
|
1500
|
+
probeResultUnknown: "尚未检测",
|
|
1501
|
+
probeResultDeclaredNone: "上游未声明档位",
|
|
1123
1502
|
probeResultAt: "检测于 {time}",
|
|
1124
|
-
probeResultEmpty: "
|
|
1125
|
-
|
|
1503
|
+
probeResultEmpty: "当前没有提供可选档位的推理模型。",
|
|
1504
|
+
probeActionDeclared: "已声明",
|
|
1126
1505
|
probeLabel: "推理档位",
|
|
1127
1506
|
probePanelLevels: "支持的档位",
|
|
1128
|
-
|
|
1507
|
+
probePanelNoLevels: "无可选档位",
|
|
1129
1508
|
probePanelNote: "检测会向该模型发送若干请求,可能消耗少量积分。",
|
|
1130
1509
|
probePanelDetect: "检测",
|
|
1131
1510
|
probePanelDetecting: "检测中…",
|
|
1132
1511
|
probePanelRedetect: "重新检测",
|
|
1512
|
+
probePanelNotNeeded: "上游已声明档位,无需检测",
|
|
1133
1513
|
probePanelNotValidating: "该模型不校验档位参数。",
|
|
1514
|
+
probePanelDeclaredNone: "上游未声明可选档位。",
|
|
1134
1515
|
probePanelFailed: "检测未完成,可以再检测一次。",
|
|
1135
|
-
|
|
1136
|
-
|
|
1516
|
+
panelCrashed: "{panel}这一块无法显示。",
|
|
1517
|
+
panelRetry: "重试",
|
|
1518
|
+
probeTooltipIdle: "该模型暂无档位,点击可检测",
|
|
1519
|
+
probeTooltipRunning: "正在检测…",
|
|
1520
|
+
probeTooltipLevels: "检测出的档位:{levels}",
|
|
1521
|
+
probeTooltipDeclared: "上游声明的档位:{levels}",
|
|
1137
1522
|
probeTooltipNotValidating: "该模型忽略推理档位参数",
|
|
1138
1523
|
probeTooltipFailed: "检测未完成 · 点击可再检测一次",
|
|
1139
1524
|
authFile: "WorkBuddy 登录文件",
|
|
@@ -1279,13 +1664,13 @@ window.__ModuleLoader__.load({
|
|
|
1279
1664
|
}
|
|
1280
1665
|
//#endregion
|
|
1281
1666
|
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/probe-control.module.css.js
|
|
1282
|
-
const css = ".EVr7sG_wrapper{align-items:center;display:inline-flex;position:relative}.EVr7sG_trigger{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:999px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:relative}.EVr7sG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.EVr7sG_trigger:disabled{cursor:default;color:var(--dsw-alias-label-dimmed)}.EVr7sG_trigger:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}.EVr7sG_panel{isolation:isolate;z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(280px,100vw - 24px);max-width:min(360px,100vw - 24px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;padding:12px;font-size:12px;line-height:1.5;position:fixed}.EVr7sG_panel:before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:\"\";pointer-events:none;position:absolute;inset:0}.EVr7sG_panelTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:1.5;display:flex}.EVr7sG_panelTitleIcon{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;display:inline-flex}.EVr7sG_panelTitleText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.EVr7sG_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin:8px 0 10px}.EVr7sG_levelsLabel{color:var(--dsw-alias-label-tertiary);margin:0 0 2px;font-size:12px;line-height:1.5}.EVr7sG_levels{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:500;line-height:1.5}.EVr7sG_note,.EVr7sG_dim{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.6}.EVr7sG_dim{margin-top:2px}.EVr7sG_panelActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}";
|
|
1283
|
-
const tagId = "dsh-workbuddy-bridge/probe-control.module.css";
|
|
1284
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1667
|
+
const css$2 = ".EVr7sG_wrapper{align-items:center;display:inline-flex;position:relative}.EVr7sG_trigger{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:999px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex;position:relative}.EVr7sG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.EVr7sG_trigger:disabled{cursor:default;color:var(--dsw-alias-label-dimmed)}.EVr7sG_trigger:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}.EVr7sG_panel{isolation:isolate;z-index:1100;box-sizing:border-box;border-radius:var(--dsw-radius-lg);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(280px,100vw - 24px);max-width:min(360px,100vw - 24px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;border:0;padding:12px;font-size:12px;line-height:1.5;position:fixed}.EVr7sG_panel:before{z-index:-1;border-radius:inherit;background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);content:\"\";pointer-events:none;position:absolute;inset:0}.EVr7sG_panelTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:1.5;display:flex}.EVr7sG_panelTitleIcon{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;display:inline-flex}.EVr7sG_panelTitleText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.EVr7sG_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin:8px 0 10px}.EVr7sG_levelsLabel{color:var(--dsw-alias-label-tertiary);margin:0 0 2px;font-size:12px;line-height:1.5}.EVr7sG_levels{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:500;line-height:1.5}.EVr7sG_note,.EVr7sG_dim{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.6}.EVr7sG_dim{margin-top:2px}.EVr7sG_panelActions{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}";
|
|
1668
|
+
const tagId$2 = "dsh-workbuddy-bridge/probe-control.module.css";
|
|
1669
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
1285
1670
|
const tag = document.createElement("style");
|
|
1286
1671
|
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
1287
|
-
tag.dataset.pluginCss = tagId;
|
|
1288
|
-
tag.textContent = css;
|
|
1672
|
+
tag.dataset.pluginCss = tagId$2;
|
|
1673
|
+
tag.textContent = css$2;
|
|
1289
1674
|
document.head.appendChild(tag);
|
|
1290
1675
|
}
|
|
1291
1676
|
var probe_control_module_css_default = {
|
|
@@ -1358,55 +1743,110 @@ window.__ModuleLoader__.load({
|
|
|
1358
1743
|
/** Gap between the control and the popover, and the viewport margin it keeps. */
|
|
1359
1744
|
const POPOVER_GAP = 8;
|
|
1360
1745
|
const POPOVER_MARGIN = 12;
|
|
1361
|
-
/**
|
|
1362
|
-
function
|
|
1746
|
+
/** One model's row out of the probe section's single list. */
|
|
1747
|
+
function effortModelFor(status, model) {
|
|
1363
1748
|
if (status.status !== "signed-in") return void 0;
|
|
1364
|
-
return status.probe?.
|
|
1749
|
+
return status.probe?.models.find((entry) => entry.id === model);
|
|
1365
1750
|
}
|
|
1366
1751
|
/**
|
|
1367
|
-
* What hovering the icon says
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1752
|
+
* What hovering the icon says.
|
|
1753
|
+
*
|
|
1754
|
+
* The bulb answers one question — *can I pick a thinking level on this model?* —
|
|
1755
|
+
* so the tooltip leads with that answer and only then explains it. Three states,
|
|
1756
|
+
* and the distinction between the last two is the whole point:
|
|
1757
|
+
*
|
|
1758
|
+
* - levels are offered: name them, and where they came from;
|
|
1759
|
+
* - nothing is offered but a detection could still find some: say so, and that
|
|
1760
|
+
* pressing is what asks;
|
|
1761
|
+
* - nothing is offered and a detection already proved there is nothing to find:
|
|
1762
|
+
* say *that*, rather than implying the question is still open. Reporting a
|
|
1763
|
+
* completed answer as "not checked yet" was the old copy's defect.
|
|
1764
|
+
*
|
|
1765
|
+
* A remembered failure is the last resort: it describes this control's own run,
|
|
1766
|
+
* while `entry` describes the model, and the model is what the user asked about.
|
|
1379
1767
|
*/
|
|
1380
|
-
function tooltipText(t,
|
|
1381
|
-
if (state.busy) return t("
|
|
1382
|
-
const
|
|
1383
|
-
if (
|
|
1384
|
-
if (
|
|
1385
|
-
if (
|
|
1768
|
+
function tooltipText(t, state) {
|
|
1769
|
+
if (state.busy) return t("probeTooltipRunning");
|
|
1770
|
+
const entry = state.entry;
|
|
1771
|
+
if (entry !== void 0) {
|
|
1772
|
+
if (entry.efforts.length > 0) return t(entry.source === "declared" ? "probeTooltipDeclared" : "probeTooltipLevels", { levels: entry.efforts.join(" / ") });
|
|
1773
|
+
if (entry.validation === "non-validating") return t("probeTooltipNotValidating");
|
|
1774
|
+
if (entry.detectable) return t("probeTooltipIdle");
|
|
1386
1775
|
return t("probeTooltipFailed");
|
|
1387
1776
|
}
|
|
1388
|
-
return state.failed ? t("probeTooltipFailed") : t("probeTooltipIdle"
|
|
1777
|
+
return state.failed ? t("probeTooltipFailed") : t("probeTooltipIdle");
|
|
1389
1778
|
}
|
|
1390
|
-
/** The levels this model
|
|
1391
|
-
function levelsLine(
|
|
1392
|
-
if (
|
|
1393
|
-
|
|
1779
|
+
/** The levels this model offers as one line; undefined when there are none. */
|
|
1780
|
+
function levelsLine(entry) {
|
|
1781
|
+
if (entry === void 0 || entry.efforts.length === 0) return void 0;
|
|
1782
|
+
return entry.efforts.join(" / ");
|
|
1394
1783
|
}
|
|
1395
|
-
/** Model-independent shell: resolves the selection, then delegates per
|
|
1784
|
+
/** Model-independent shell: resolves the selection, then delegates per variant. */
|
|
1396
1785
|
function WorkBuddyProbeControl({ directory, t }) {
|
|
1397
1786
|
const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
|
|
1398
1787
|
const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
1399
1788
|
const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
|
|
1400
|
-
const card = selection
|
|
1401
|
-
if (card === void 0 || selection
|
|
1402
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1403
|
-
model: selection.model,
|
|
1789
|
+
const card = selection == null ? void 0 : cardVariantFor(selection.provider);
|
|
1790
|
+
if (card === void 0 || selection == null) return null;
|
|
1791
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VariantProbe, {
|
|
1404
1792
|
card,
|
|
1793
|
+
model: selection.model,
|
|
1405
1794
|
t
|
|
1406
|
-
},
|
|
1795
|
+
}, card.id);
|
|
1407
1796
|
}
|
|
1408
|
-
|
|
1797
|
+
/**
|
|
1798
|
+
* One variant's status document, held above the model-keyed layer.
|
|
1799
|
+
*
|
|
1800
|
+
* Living here means a model switch is a prop update rather than a remount, so
|
|
1801
|
+
* the document read for the previous model is already in hand and the new
|
|
1802
|
+
* model's row is rendered from it on the first frame — no gap, and no request.
|
|
1803
|
+
*
|
|
1804
|
+
* The reads that *do* belong here are the ones keyed to the account: the
|
|
1805
|
+
* mount-time load, the reconcile poll (a detection run in another conversation
|
|
1806
|
+
* or in the settings card has to reach this control), and the focus refresh.
|
|
1807
|
+
*/
|
|
1808
|
+
function VariantProbe({ card, model, t }) {
|
|
1409
1809
|
const [status, setStatus] = (0, react.useState)();
|
|
1810
|
+
const mounted = (0, react.useRef)(false);
|
|
1811
|
+
/** Number of the newest read that may write; assigned when a read starts. */
|
|
1812
|
+
const readSeq = (0, react.useRef)(0);
|
|
1813
|
+
const refresh = (0, react.useCallback)(async (signal) => {
|
|
1814
|
+
const seq = ++readSeq.current;
|
|
1815
|
+
const response = await fetch(card.statusPath, {
|
|
1816
|
+
credentials: "same-origin",
|
|
1817
|
+
headers: { accept: "application/json" },
|
|
1818
|
+
...signal === void 0 ? {} : { signal }
|
|
1819
|
+
});
|
|
1820
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1821
|
+
const value = await response.json().catch(() => void 0);
|
|
1822
|
+
if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
|
|
1823
|
+
if (mounted.current && signal?.aborted !== true && seq === readSeq.current) setStatus(value);
|
|
1824
|
+
}, [card.statusPath, t]);
|
|
1825
|
+
(0, react.useEffect)(() => {
|
|
1826
|
+
mounted.current = true;
|
|
1827
|
+
const controller = new AbortController();
|
|
1828
|
+
const load = () => {
|
|
1829
|
+
refresh(controller.signal).catch(() => {});
|
|
1830
|
+
};
|
|
1831
|
+
load();
|
|
1832
|
+
const timer = window.setInterval(load, RECONCILE_MS);
|
|
1833
|
+
window.addEventListener("focus", load);
|
|
1834
|
+
return () => {
|
|
1835
|
+
mounted.current = false;
|
|
1836
|
+
controller.abort();
|
|
1837
|
+
window.clearInterval(timer);
|
|
1838
|
+
window.removeEventListener("focus", load);
|
|
1839
|
+
};
|
|
1840
|
+
}, [refresh]);
|
|
1841
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
|
|
1842
|
+
model,
|
|
1843
|
+
status,
|
|
1844
|
+
probePath: card.probePath,
|
|
1845
|
+
refresh,
|
|
1846
|
+
t
|
|
1847
|
+
}, model);
|
|
1848
|
+
}
|
|
1849
|
+
function ModelProbe({ model, status, probePath, refresh, t }) {
|
|
1410
1850
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
1411
1851
|
/**
|
|
1412
1852
|
* Whether the panel is open.
|
|
@@ -1422,12 +1862,11 @@ window.__ModuleLoader__.load({
|
|
|
1422
1862
|
* The outcome of the run this control just performed.
|
|
1423
1863
|
*
|
|
1424
1864
|
* Kept so a fresh answer is shown immediately, without waiting for the next
|
|
1425
|
-
* status read;
|
|
1865
|
+
* status read; the document's own row is what stands when there is none.
|
|
1426
1866
|
*/
|
|
1427
1867
|
const [fresh, setFresh] = (0, react.useState)();
|
|
1428
1868
|
const inFlight = (0, react.useRef)(false);
|
|
1429
1869
|
const mounted = (0, react.useRef)(false);
|
|
1430
|
-
const readSeq = (0, react.useRef)(0);
|
|
1431
1870
|
/** The control itself: the popover's anchor and the "inside" test for dismissal. */
|
|
1432
1871
|
const rootRef = (0, react.useRef)(null);
|
|
1433
1872
|
/**
|
|
@@ -1436,41 +1875,29 @@ window.__ModuleLoader__.load({
|
|
|
1436
1875
|
* which would otherwise read a click on the panel as a click outside it.
|
|
1437
1876
|
*/
|
|
1438
1877
|
const panelRef = (0, react.useRef)(null);
|
|
1439
|
-
const refresh = (0, react.useCallback)(async (signal) => {
|
|
1440
|
-
const seq = ++readSeq.current;
|
|
1441
|
-
const response = await fetch(card.statusPath, {
|
|
1442
|
-
credentials: "same-origin",
|
|
1443
|
-
headers: { accept: "application/json" },
|
|
1444
|
-
...signal === void 0 ? {} : { signal }
|
|
1445
|
-
});
|
|
1446
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1447
|
-
const value = await response.json().catch(() => void 0);
|
|
1448
|
-
if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
|
|
1449
|
-
if (mounted.current && signal?.aborted !== true && seq === readSeq.current) setStatus(value);
|
|
1450
|
-
}, [card.statusPath, t]);
|
|
1451
1878
|
(0, react.useEffect)(() => {
|
|
1452
1879
|
mounted.current = true;
|
|
1453
|
-
const controller = new AbortController();
|
|
1454
|
-
const load = () => {
|
|
1455
|
-
refresh(controller.signal).catch(() => {});
|
|
1456
|
-
};
|
|
1457
|
-
load();
|
|
1458
|
-
const timer = window.setInterval(load, RECONCILE_MS);
|
|
1459
|
-
window.addEventListener("focus", load);
|
|
1460
1880
|
return () => {
|
|
1461
1881
|
mounted.current = false;
|
|
1462
|
-
controller.abort();
|
|
1463
|
-
window.clearInterval(timer);
|
|
1464
|
-
window.removeEventListener("focus", load);
|
|
1465
1882
|
};
|
|
1466
|
-
}, [
|
|
1883
|
+
}, []);
|
|
1467
1884
|
const probe = status?.status === "signed-in" ? status.probe : void 0;
|
|
1468
1885
|
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1886
|
+
const entry = status === void 0 ? void 0 : effortModelFor(status, model);
|
|
1887
|
+
/**
|
|
1888
|
+
* The bulb is shown for every reasoning model the host reports, and hidden
|
|
1889
|
+
* only where the host says there is nothing to say.
|
|
1890
|
+
*
|
|
1891
|
+
* The old gate was `candidates.includes(model) || hasRecord`, i.e. "this model
|
|
1892
|
+
* could be probed, or has been". That is a statement about *detection*, and it
|
|
1893
|
+
* left the bulb off exactly the models whose switch already worked — a
|
|
1894
|
+
* declared set made a model ineligible, so the best-configured models were the
|
|
1895
|
+
* ones with no control beside them, and the user read that as a bug.
|
|
1896
|
+
*/
|
|
1897
|
+
const visible = entry !== void 0;
|
|
1471
1898
|
(0, react.useEffect)(() => {
|
|
1472
|
-
if (
|
|
1473
|
-
}, [
|
|
1899
|
+
if (entry?.validation !== void 0) setFailed(false);
|
|
1900
|
+
}, [entry?.validation]);
|
|
1474
1901
|
(0, react.useEffect)(() => {
|
|
1475
1902
|
setOpen(false);
|
|
1476
1903
|
setFresh(void 0);
|
|
@@ -1482,7 +1909,7 @@ window.__ModuleLoader__.load({
|
|
|
1482
1909
|
setBusy(true);
|
|
1483
1910
|
setFailed(false);
|
|
1484
1911
|
try {
|
|
1485
|
-
const response = await fetch(
|
|
1912
|
+
const response = await fetch(probePath, {
|
|
1486
1913
|
method: "POST",
|
|
1487
1914
|
credentials: "same-origin",
|
|
1488
1915
|
headers: {
|
|
@@ -1495,14 +1922,20 @@ window.__ModuleLoader__.load({
|
|
|
1495
1922
|
})
|
|
1496
1923
|
});
|
|
1497
1924
|
const body = await response.json();
|
|
1498
|
-
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1925
|
+
const validation = body.validation;
|
|
1926
|
+
if (!response.ok || body.state !== "ok" || validation !== "validating" && validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
|
|
1927
|
+
if (mounted.current) {
|
|
1928
|
+
const validating = validation === "validating";
|
|
1929
|
+
setFresh({
|
|
1930
|
+
id: model,
|
|
1931
|
+
name: model,
|
|
1932
|
+
efforts: validating ? body.efforts : [],
|
|
1933
|
+
source: validating ? "observed" : "none",
|
|
1934
|
+
detectable: true,
|
|
1935
|
+
validation,
|
|
1936
|
+
probedAt: Date.now()
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1506
1939
|
refresh().catch(() => {});
|
|
1507
1940
|
} catch {
|
|
1508
1941
|
if (mounted.current) setFailed(true);
|
|
@@ -1523,16 +1956,18 @@ window.__ModuleLoader__.load({
|
|
|
1523
1956
|
setOpen(false);
|
|
1524
1957
|
}, panelRef);
|
|
1525
1958
|
if (!visible) return null;
|
|
1526
|
-
const text = tooltipText(t,
|
|
1959
|
+
const text = tooltipText(t, {
|
|
1527
1960
|
busy,
|
|
1528
|
-
|
|
1961
|
+
entry,
|
|
1529
1962
|
failed
|
|
1530
1963
|
});
|
|
1531
|
-
const
|
|
1532
|
-
const shown = fresh ?? result;
|
|
1964
|
+
const shown = fresh ?? entry;
|
|
1533
1965
|
const levels = levelsLine(shown);
|
|
1534
1966
|
const notValidating = shown?.validation === "non-validating";
|
|
1535
|
-
const lit =
|
|
1967
|
+
const lit = levels !== void 0;
|
|
1968
|
+
const detectable = shown?.detectable === true;
|
|
1969
|
+
const triggerDisabled = busy || probe?.running === true || key === void 0;
|
|
1970
|
+
const actionDisabled = !detectable || triggerDisabled;
|
|
1536
1971
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1537
1972
|
className: probe_control_module_css_default.wrapper,
|
|
1538
1973
|
ref: rootRef,
|
|
@@ -1547,71 +1982,121 @@ window.__ModuleLoader__.load({
|
|
|
1547
1982
|
"aria-label": text,
|
|
1548
1983
|
"aria-busy": busy,
|
|
1549
1984
|
"aria-expanded": open,
|
|
1550
|
-
disabled,
|
|
1985
|
+
disabled: triggerDisabled,
|
|
1551
1986
|
onClick: () => {
|
|
1552
1987
|
setOpen(!open);
|
|
1553
1988
|
},
|
|
1554
1989
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeIcon, { lit })
|
|
1555
1990
|
})
|
|
1556
|
-
}), open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1557
|
-
|
|
1558
|
-
className: probe_control_module_css_default.panel,
|
|
1991
|
+
}), open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbePanel, {
|
|
1992
|
+
panelRef,
|
|
1559
1993
|
style: position ?? MEASURE_STYLE,
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1574
|
-
className: probe_control_module_css_default.titleRule,
|
|
1575
|
-
"aria-hidden": true
|
|
1576
|
-
}),
|
|
1577
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1578
|
-
className: probe_control_module_css_default.levelsLabel,
|
|
1579
|
-
children: t("probePanelLevels")
|
|
1580
|
-
}),
|
|
1581
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1582
|
-
className: probe_control_module_css_default.levels,
|
|
1583
|
-
role: "status",
|
|
1584
|
-
"aria-live": "polite",
|
|
1585
|
-
children: levels ?? t("probePanelNone")
|
|
1586
|
-
}),
|
|
1587
|
-
notValidating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1588
|
-
className: probe_control_module_css_default.dim,
|
|
1589
|
-
children: t("probePanelNotValidating")
|
|
1590
|
-
}) : null,
|
|
1591
|
-
failed && shown === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1592
|
-
className: probe_control_module_css_default.dim,
|
|
1593
|
-
children: t("probePanelFailed")
|
|
1594
|
-
}) : null,
|
|
1595
|
-
levels === void 0 && !notValidating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1596
|
-
className: probe_control_module_css_default.note,
|
|
1597
|
-
children: t("probePanelNote")
|
|
1598
|
-
}) : null,
|
|
1599
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1600
|
-
className: probe_control_module_css_default.panelActions,
|
|
1601
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1602
|
-
size: "sm",
|
|
1603
|
-
variant: "primary",
|
|
1604
|
-
disabled,
|
|
1605
|
-
onClick: () => {
|
|
1606
|
-
detect();
|
|
1607
|
-
},
|
|
1608
|
-
children: busy ? t("probePanelDetecting") : levels === void 0 ? t("probePanelDetect") : t("probePanelRedetect")
|
|
1609
|
-
})
|
|
1610
|
-
})
|
|
1611
|
-
]
|
|
1994
|
+
model,
|
|
1995
|
+
lit,
|
|
1996
|
+
levels,
|
|
1997
|
+
notValidating,
|
|
1998
|
+
detectable,
|
|
1999
|
+
failed,
|
|
2000
|
+
shown,
|
|
2001
|
+
busy,
|
|
2002
|
+
actionDisabled,
|
|
2003
|
+
onDetect: () => {
|
|
2004
|
+
detect();
|
|
2005
|
+
},
|
|
2006
|
+
t
|
|
1612
2007
|
}), document.body) : null]
|
|
1613
2008
|
});
|
|
1614
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* The open popover's contents.
|
|
2012
|
+
*
|
|
2013
|
+
* Split out of {@link ModelProbe} because the panel is a self-contained reading
|
|
2014
|
+
* of one model — a title, the levels, why there are none, and the one action —
|
|
2015
|
+
* while the parent is a state machine (reads, races, an anchored portal). The
|
|
2016
|
+
* boundary is *what the panel needs to know*, not a slice of the parent's state:
|
|
2017
|
+
* every prop here is already computed by the time the panel renders, and the
|
|
2018
|
+
* panel holds none of its own. That is what keeps the split from moving the
|
|
2019
|
+
* implicit constraints the parent carries.
|
|
2020
|
+
*/
|
|
2021
|
+
function ProbePanel({ panelRef, style, model, lit, levels, notValidating, detectable, failed, shown, busy, actionDisabled, onDetect, t }) {
|
|
2022
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2023
|
+
ref: panelRef,
|
|
2024
|
+
className: probe_control_module_css_default.panel,
|
|
2025
|
+
style,
|
|
2026
|
+
role: "dialog",
|
|
2027
|
+
"aria-label": t("probeLabel"),
|
|
2028
|
+
children: [
|
|
2029
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2030
|
+
className: probe_control_module_css_default.panelTitle,
|
|
2031
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2032
|
+
className: probe_control_module_css_default.panelTitleIcon,
|
|
2033
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeIcon, { lit })
|
|
2034
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2035
|
+
className: probe_control_module_css_default.panelTitleText,
|
|
2036
|
+
children: model
|
|
2037
|
+
})]
|
|
2038
|
+
}),
|
|
2039
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2040
|
+
className: probe_control_module_css_default.titleRule,
|
|
2041
|
+
"aria-hidden": true
|
|
2042
|
+
}),
|
|
2043
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2044
|
+
className: probe_control_module_css_default.levelsLabel,
|
|
2045
|
+
children: t("probePanelLevels")
|
|
2046
|
+
}),
|
|
2047
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2048
|
+
className: probe_control_module_css_default.levels,
|
|
2049
|
+
role: "status",
|
|
2050
|
+
"aria-live": "polite",
|
|
2051
|
+
children: levels ?? t("probePanelNoLevels")
|
|
2052
|
+
}),
|
|
2053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PanelReason, {
|
|
2054
|
+
notValidating,
|
|
2055
|
+
levels,
|
|
2056
|
+
detectable,
|
|
2057
|
+
shown,
|
|
2058
|
+
failed,
|
|
2059
|
+
t
|
|
2060
|
+
}),
|
|
2061
|
+
detectable && levels === void 0 && !notValidating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2062
|
+
className: probe_control_module_css_default.note,
|
|
2063
|
+
children: t("probePanelNote")
|
|
2064
|
+
}) : null,
|
|
2065
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2066
|
+
className: probe_control_module_css_default.panelActions,
|
|
2067
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2068
|
+
size: "sm",
|
|
2069
|
+
variant: "primary",
|
|
2070
|
+
disabled: actionDisabled,
|
|
2071
|
+
onClick: onDetect,
|
|
2072
|
+
children: busy ? t("probePanelDetecting") : !detectable ? t("probePanelNotNeeded") : levels === void 0 ? t("probePanelDetect") : t("probePanelRedetect")
|
|
2073
|
+
})
|
|
2074
|
+
})
|
|
2075
|
+
]
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* The one line explaining *why* there are no levels, when that is the answer.
|
|
2080
|
+
*
|
|
2081
|
+
* Three facts, kept apart on purpose — see the call site. Extracted so the three
|
|
2082
|
+
* mutually exclusive branches read as one decision rather than three adjacent
|
|
2083
|
+
* conditionals interleaved with unrelated markup.
|
|
2084
|
+
*/
|
|
2085
|
+
function PanelReason({ notValidating, levels, detectable, shown, failed, t }) {
|
|
2086
|
+
if (notValidating) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2087
|
+
className: probe_control_module_css_default.dim,
|
|
2088
|
+
children: t("probePanelNotValidating")
|
|
2089
|
+
});
|
|
2090
|
+
if (levels === void 0 && !detectable && shown !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2091
|
+
className: probe_control_module_css_default.dim,
|
|
2092
|
+
children: t("probePanelDeclaredNone")
|
|
2093
|
+
});
|
|
2094
|
+
if (failed && shown === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2095
|
+
className: probe_control_module_css_default.dim,
|
|
2096
|
+
children: t("probePanelFailed")
|
|
2097
|
+
});
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
1615
2100
|
/** The bulb's body — the maintainer's artwork, one filled path with the base cut out. */
|
|
1616
2101
|
const BULB_BODY = "M496 64C681.6 64 832 208.896 832 387.648c0 124.544-73.152 232.704-180.352 286.784v24.256c0 6.912-0.64 13.76-1.856 20.288a56.32 56.32 0 0 1 20.672 20.416 54.848 54.848 0 0 1 7.552 27.712v37.312c0 29.312-23.04 53.312-52.288 55.808l-2.816 0.192h-23.04c0 54.976-46.336 99.584-103.424 99.584-57.024 0-103.296-44.544-103.296-99.584h-19.776l-2.24-0.128h-3.84a58.24 58.24 0 0 1-36.48-18.432 55.808 55.808 0 0 1-14.72-37.44v-37.312c0-20.16 11.008-37.888 27.328-47.744a104.064 104.064 0 0 1-1.984-20.672v-23.744C233.6 621.056 160 512.576 160 387.584 160.064 208.96 310.4 64 496 64z m-45.632 796.288c0 24.064 20.48 43.712 46.08 43.712 25.728 0 46.144-19.712 46.208-43.52v-0.192H450.368z m-76.992-56h247.296l0.064-37.056-0.512-0.32H373.632l-0.256 37.376zM496 120.064c-154.112 0-278.656 120-278.656 267.52 0 100.8 58.624 191.68 150.208 237.44l31.104 15.68 0.064 50.56c0 3.968 0.384 7.552 0.896 10.816l0.576 1.984H467.84V704h70.4v-0.128l55.488-0.32 0.512-1.472c0.64-3.776 0.96-7.552 0.96-11.328l-0.96-50.56 31.04-15.616c91.2-45.952 149.312-136.576 149.312-236.992 0-147.584-124.544-267.648-278.656-267.648v0.128z";
|
|
1617
2102
|
/** The bolt drawn inside the bulb once a detection has landed. */
|
|
@@ -1657,6 +2142,272 @@ window.__ModuleLoader__.load({
|
|
|
1657
2142
|
});
|
|
1658
2143
|
}
|
|
1659
2144
|
//#endregion
|
|
2145
|
+
//#region src/client/use-session-credits.ts
|
|
2146
|
+
/**
|
|
2147
|
+
* One Session's per-message credit accounting, as the browser half reads it.
|
|
2148
|
+
*
|
|
2149
|
+
* The Host half observes what each response cost on the upstream stream and
|
|
2150
|
+
* attributes it to a DSH message id; this hook fetches that map and keeps it
|
|
2151
|
+
* fresh. Two surfaces share it — the composer balance label and the per-message
|
|
2152
|
+
* credit row — so the fetch policy lives here once rather than in both.
|
|
2153
|
+
*
|
|
2154
|
+
* **A failure is silent, and that is the intended degradation.** The number is
|
|
2155
|
+
* an annotation on a message that already rendered; a failed read must never
|
|
2156
|
+
* turn into an error strip over a conversation, and an unreadable map must
|
|
2157
|
+
* never be confused with "these messages were free". So a failure leaves the
|
|
2158
|
+
* last good map in place and the affected rows simply show no credit.
|
|
2159
|
+
*
|
|
2160
|
+
* @module dsh-workbuddy-bridge/client/use-session-credits
|
|
2161
|
+
*/
|
|
2162
|
+
/** How often the map is re-read while a Session is open, in milliseconds. */
|
|
2163
|
+
const POLL_INTERVAL_MS$1 = 3e4;
|
|
2164
|
+
/**
|
|
2165
|
+
* Read and poll one Session's credit map.
|
|
2166
|
+
*
|
|
2167
|
+
* @param path - the variant's credit route.
|
|
2168
|
+
* @param sessionId - the Session to read for; the hook is inert without one.
|
|
2169
|
+
* @param enabled - whether the reading surface is currently mounted. The
|
|
2170
|
+
* composer label and the message row both come and go, and this stops the poll
|
|
2171
|
+
* when neither is showing.
|
|
2172
|
+
*/
|
|
2173
|
+
function useWorkBuddySessionCredits(path, sessionId, enabled) {
|
|
2174
|
+
const [credits, setCredits] = (0, react.useState)();
|
|
2175
|
+
const mounted = (0, react.useRef)(true);
|
|
2176
|
+
/** Number of the newest read allowed to write; assigned when a read starts. */
|
|
2177
|
+
const readSeq = (0, react.useRef)(0);
|
|
2178
|
+
const read = (0, react.useCallback)(async (signal) => {
|
|
2179
|
+
if (sessionId === void 0 || sessionId === "") return;
|
|
2180
|
+
const seq = ++readSeq.current;
|
|
2181
|
+
try {
|
|
2182
|
+
const response = await fetch(`${path}?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
2183
|
+
credentials: "same-origin",
|
|
2184
|
+
headers: { accept: "application/json" },
|
|
2185
|
+
signal
|
|
2186
|
+
});
|
|
2187
|
+
if (!response.ok) return;
|
|
2188
|
+
const value = await response.json().catch(() => void 0);
|
|
2189
|
+
if (!isWorkBuddySessionCredits(value)) return;
|
|
2190
|
+
if (!mounted.current || signal.aborted || seq !== readSeq.current) return;
|
|
2191
|
+
setCredits(value.credits);
|
|
2192
|
+
} catch {}
|
|
2193
|
+
}, [path, sessionId]);
|
|
2194
|
+
(0, react.useEffect)(() => {
|
|
2195
|
+
mounted.current = true;
|
|
2196
|
+
if (!enabled || sessionId === void 0 || sessionId === "") return;
|
|
2197
|
+
const controller = new AbortController();
|
|
2198
|
+
read(controller.signal);
|
|
2199
|
+
const timer = window.setInterval(() => {
|
|
2200
|
+
read(controller.signal);
|
|
2201
|
+
}, POLL_INTERVAL_MS$1);
|
|
2202
|
+
const onFocus = () => {
|
|
2203
|
+
read(controller.signal);
|
|
2204
|
+
};
|
|
2205
|
+
window.addEventListener("focus", onFocus);
|
|
2206
|
+
return () => {
|
|
2207
|
+
mounted.current = false;
|
|
2208
|
+
controller.abort();
|
|
2209
|
+
window.clearInterval(timer);
|
|
2210
|
+
window.removeEventListener("focus", onFocus);
|
|
2211
|
+
};
|
|
2212
|
+
}, [
|
|
2213
|
+
enabled,
|
|
2214
|
+
read,
|
|
2215
|
+
sessionId
|
|
2216
|
+
]);
|
|
2217
|
+
return { credits };
|
|
2218
|
+
}
|
|
2219
|
+
//#endregion
|
|
2220
|
+
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/credit-label.module.css.js
|
|
2221
|
+
const css$1 = "._9Bx4Wa_label{white-space:nowrap;font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);color:var(--dsw-alias-label-tertiary);padding-left:4px;line-height:1}";
|
|
2222
|
+
const tagId$1 = "dsh-workbuddy-bridge/credit-label.module.css";
|
|
2223
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
2224
|
+
const tag = document.createElement("style");
|
|
2225
|
+
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
2226
|
+
tag.dataset.pluginCss = tagId$1;
|
|
2227
|
+
tag.textContent = css$1;
|
|
2228
|
+
document.head.appendChild(tag);
|
|
2229
|
+
}
|
|
2230
|
+
var credit_label_module_css_default = { "label": "_9Bx4Wa_label" };
|
|
2231
|
+
//#endregion
|
|
2232
|
+
//#region src/client/credit-label.tsx
|
|
2233
|
+
/**
|
|
2234
|
+
* The per-message credit label, in the assistant message's action row.
|
|
2235
|
+
*
|
|
2236
|
+
* WorkBuddy's own client puts "共消耗 X" under a finished answer; this is the
|
|
2237
|
+
* same fact for the same reason — the cost of a message is not visible anywhere
|
|
2238
|
+
* else, and a user comparing models is asking exactly "what did that one cost
|
|
2239
|
+
* me?".
|
|
2240
|
+
*
|
|
2241
|
+
* **It belongs to the WorkBuddy models and is shown only for them.** When the
|
|
2242
|
+
* session's current selection is a model from another provider, the label
|
|
2243
|
+
* renders nothing and reads nothing: a WorkBuddy credit figure next to a
|
|
2244
|
+
* DeepSeek-account answer would describe a ledger that answer never touched.
|
|
2245
|
+
* Gating on the *current* selection (rather than on each message's own
|
|
2246
|
+
* provider) is deliberate — the user asked for exactly this, and it keeps the
|
|
2247
|
+
* row's meaning consistent with the model picker sitting in the same UI.
|
|
2248
|
+
*
|
|
2249
|
+
* **It renders nothing when there is nothing to say.** The plugin only shows a
|
|
2250
|
+
* number it actually observed on the upstream stream (see
|
|
2251
|
+
* `src/llm/credit-log.ts`), so a message predating the feature, a failed
|
|
2252
|
+
* stream, or an upstream answer that carried no cost produces an empty row
|
|
2253
|
+
* rather than a confident `0`. That choice is deliberate and load-bearing: a
|
|
2254
|
+
* fabricated zero is worse than a blank, because the user cannot tell it apart
|
|
2255
|
+
* from free.
|
|
2256
|
+
*
|
|
2257
|
+
* Styling copies the sibling `endInfo` cluster — tertiary label tone at the
|
|
2258
|
+
* secondary font size — so the label reads as part of the same row as the clock
|
|
2259
|
+
* and the usage pill rather than as something bolted on.
|
|
2260
|
+
*
|
|
2261
|
+
* @module dsh-workbuddy-bridge/client/credit-label
|
|
2262
|
+
*/
|
|
2263
|
+
/** Show what one message cost, when this session runs a WorkBuddy model. */
|
|
2264
|
+
function WorkBuddyCreditLabel({ messageId, sessionId, directory, t }) {
|
|
2265
|
+
const selection = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => directory.subscribe(listener), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory])).current;
|
|
2266
|
+
const onWorkBuddy = selection !== null && cardVariantFor(selection.provider) !== void 0;
|
|
2267
|
+
const { credits } = useWorkBuddySessionCredits(WORKBUDDY_CREDIT_PATH, sessionId, onWorkBuddy && sessionId !== "");
|
|
2268
|
+
if (!onWorkBuddy) return null;
|
|
2269
|
+
const credit = credits?.[messageId];
|
|
2270
|
+
if (credit === void 0) return null;
|
|
2271
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2272
|
+
className: credit_label_module_css_default.label,
|
|
2273
|
+
title: t("creditConsumedTitle"),
|
|
2274
|
+
children: t("creditConsumed", { credit: formatCredit(credit) })
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
//#endregion
|
|
2278
|
+
//#region \0workbuddy-css:/home/runner/work/dsh-workbuddy-bridge/dsh-workbuddy-bridge/src/client/credit-balance.module.css.js
|
|
2279
|
+
const css = ".Rbiksq_readout{white-space:nowrap;height:28px;color:var(--dsw-alias-label-tertiary);align-items:center;gap:4px;display:inline-flex}.Rbiksq_amount{font-size:calc(var(--dsh-content-font-size-secondary,13px) - 1px);font-variant-numeric:tabular-nums;line-height:1}";
|
|
2280
|
+
const tagId = "dsh-workbuddy-bridge/credit-balance.module.css";
|
|
2281
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
2282
|
+
const tag = document.createElement("style");
|
|
2283
|
+
tag.dataset.plugin = "dsh-workbuddy-bridge";
|
|
2284
|
+
tag.dataset.pluginCss = tagId;
|
|
2285
|
+
tag.textContent = css;
|
|
2286
|
+
document.head.appendChild(tag);
|
|
2287
|
+
}
|
|
2288
|
+
var credit_balance_module_css_default = {
|
|
2289
|
+
"amount": "Rbiksq_amount",
|
|
2290
|
+
"readout": "Rbiksq_readout"
|
|
2291
|
+
};
|
|
2292
|
+
//#endregion
|
|
2293
|
+
//#region src/client/credit-balance.tsx
|
|
2294
|
+
/**
|
|
2295
|
+
* Remaining credit, in the composer row immediately left of the model picker.
|
|
2296
|
+
*
|
|
2297
|
+
* This is the one place the figure is worth carrying: choosing a model is the
|
|
2298
|
+
* moment the balance decides anything, and the card's own Credits tab is two
|
|
2299
|
+
* clicks away from where the choice is made. It answers "how much have I got?"
|
|
2300
|
+
* next to the control that spends it.
|
|
2301
|
+
*
|
|
2302
|
+
* **It is a readout, not a control.** A coin glyph followed by the number, in
|
|
2303
|
+
* the same 28px row as the reasoning-level bulb beside it. The words "remaining"
|
|
2304
|
+
* / "剩余" are deliberately absent: at this size a sentence competes with the
|
|
2305
|
+
* number it is labelling, and the glyph already says *what* the number is. The
|
|
2306
|
+
* full sentence survives as the `title` for anyone who hovers, and the card's
|
|
2307
|
+
* Credits tab remains the place that spells the figure out.
|
|
2308
|
+
*
|
|
2309
|
+
* **It is shown only while the session runs a WorkBuddy model.** The balance
|
|
2310
|
+
* belongs to the WorkBuddy account, so it is offered exactly where that account
|
|
2311
|
+
* is the one being spent; a session on any other provider renders nothing and
|
|
2312
|
+
* reads nothing. That keeps the composer row describing the model sitting in
|
|
2313
|
+
* it, and avoids putting a second provider's ledger next to a model it cannot
|
|
2314
|
+
* pay for.
|
|
2315
|
+
*
|
|
2316
|
+
* **It renders nothing rather than a guess.** Three states produce no label: the
|
|
2317
|
+
* selection is not a WorkBuddy model, the status read has not answered yet, and
|
|
2318
|
+
* the read failed. A blanket `0` or a `—` would be read as a balance, and being
|
|
2319
|
+
* wrong about a balance is worse than being quiet — the same rule the message
|
|
2320
|
+
* label follows.
|
|
2321
|
+
*
|
|
2322
|
+
* The seat is `conversation.input.right`, a `list` slot whose occupant becomes
|
|
2323
|
+
* the immediate flex sibling of the model seat. Registering there rather than
|
|
2324
|
+
* at `conversation.input.model` is deliberate and is pinned by
|
|
2325
|
+
* `tests/redlines.spec.ts`: that seat is `single` and already holds the shipped
|
|
2326
|
+
* `ModelSelect`, so a second registration at the same priority throws and takes
|
|
2327
|
+
* the host's own picker down with it.
|
|
2328
|
+
*
|
|
2329
|
+
* @module dsh-workbuddy-bridge/client/credit-balance
|
|
2330
|
+
*/
|
|
2331
|
+
/**
|
|
2332
|
+
* How often the balance is re-read while the composer is mounted.
|
|
2333
|
+
*
|
|
2334
|
+
* Two minutes rather than the status card's one: each read is a real billing
|
|
2335
|
+
* request upstream, the figure only moves when credit is spent, and a window
|
|
2336
|
+
* regaining focus forces a fresh read anyway.
|
|
2337
|
+
*/
|
|
2338
|
+
const POLL_INTERVAL_MS = 12e4;
|
|
2339
|
+
/** Read one variant's status route and reduce it to a balance. */
|
|
2340
|
+
async function readBalance(card, signal) {
|
|
2341
|
+
try {
|
|
2342
|
+
const response = await fetch(card.statusPath, {
|
|
2343
|
+
credentials: "same-origin",
|
|
2344
|
+
headers: { accept: "application/json" },
|
|
2345
|
+
signal
|
|
2346
|
+
});
|
|
2347
|
+
if (!response.ok) return { kind: "unknown" };
|
|
2348
|
+
const value = await response.json().catch(() => void 0);
|
|
2349
|
+
if (!isWorkBuddyWebStatus(value) || value.status !== "signed-in") return { kind: "unknown" };
|
|
2350
|
+
const credits = value.credits;
|
|
2351
|
+
if (credits === void 0) return { kind: "unknown" };
|
|
2352
|
+
return credits.unlimited === true ? { kind: "unlimited" } : {
|
|
2353
|
+
kind: "total",
|
|
2354
|
+
total: credits.total
|
|
2355
|
+
};
|
|
2356
|
+
} catch {
|
|
2357
|
+
return { kind: "unknown" };
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
/**
|
|
2361
|
+
* Show the signed-in WorkBuddy account's remaining credit beside the picker.
|
|
2362
|
+
*
|
|
2363
|
+
* The account is not per-session, so this reads the status route rather than
|
|
2364
|
+
* the credit route: the balance belongs to whoever is signed in, while the
|
|
2365
|
+
* per-message costs belong to the conversation.
|
|
2366
|
+
*/
|
|
2367
|
+
function WorkBuddyCreditBalance({ directory, t }) {
|
|
2368
|
+
const selection = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => directory.subscribe(listener), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory]), (0, react.useCallback)(() => directory.getSnapshot(), [directory])).current;
|
|
2369
|
+
const card = selection === null ? void 0 : cardVariantFor(selection.provider);
|
|
2370
|
+
const [balance, setBalance] = (0, react.useState)({ kind: "unknown" });
|
|
2371
|
+
const mounted = (0, react.useRef)(true);
|
|
2372
|
+
(0, react.useEffect)(() => {
|
|
2373
|
+
mounted.current = true;
|
|
2374
|
+
if (card === void 0) {
|
|
2375
|
+
setBalance({ kind: "unknown" });
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const controller = new AbortController();
|
|
2379
|
+
const read = async () => {
|
|
2380
|
+
const next = await readBalance(card, controller.signal);
|
|
2381
|
+
if (!mounted.current || controller.signal.aborted) return;
|
|
2382
|
+
setBalance(next);
|
|
2383
|
+
};
|
|
2384
|
+
read();
|
|
2385
|
+
const timer = window.setInterval(() => {
|
|
2386
|
+
read();
|
|
2387
|
+
}, POLL_INTERVAL_MS);
|
|
2388
|
+
const onFocus = () => {
|
|
2389
|
+
read();
|
|
2390
|
+
};
|
|
2391
|
+
window.addEventListener("focus", onFocus);
|
|
2392
|
+
return () => {
|
|
2393
|
+
mounted.current = false;
|
|
2394
|
+
controller.abort();
|
|
2395
|
+
window.clearInterval(timer);
|
|
2396
|
+
window.removeEventListener("focus", onFocus);
|
|
2397
|
+
};
|
|
2398
|
+
}, [card]);
|
|
2399
|
+
if (card === void 0 || balance.kind === "unknown") return null;
|
|
2400
|
+
const figure = balance.kind === "unlimited" ? t("creditRemainingUnlimited") : formatCredit(balance.total);
|
|
2401
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2402
|
+
className: credit_balance_module_css_default.readout,
|
|
2403
|
+
title: t("creditBalanceTitle", { product: card.appName }),
|
|
2404
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CoinGlyph, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2405
|
+
className: credit_balance_module_css_default.amount,
|
|
2406
|
+
children: figure
|
|
2407
|
+
})]
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
//#endregion
|
|
1660
2411
|
//#region src/client/config-controller.ts
|
|
1661
2412
|
/**
|
|
1662
2413
|
* A boolean field. Staged as text like every other field, so one save covers
|
|
@@ -1769,6 +2520,29 @@ window.__ModuleLoader__.load({
|
|
|
1769
2520
|
const PROBE_SEAT = "conversation.input.right";
|
|
1770
2521
|
/** Entry id within that seat, so the registration is named in exactly one place. */
|
|
1771
2522
|
const PROBE_SEAT_ID = "workbuddy-probe";
|
|
2523
|
+
/**
|
|
2524
|
+
* The remaining-credit label's entry id, in the same `list` seat as the probe
|
|
2525
|
+
* control.
|
|
2526
|
+
*
|
|
2527
|
+
* Sharing {@link PROBE_SEAT} rather than taking a seat of its own is what keeps
|
|
2528
|
+
* the balance flush against the model picker: `conversation.input.right` renders
|
|
2529
|
+
* as the immediate previous flex sibling of `conversation.input.model`, and two
|
|
2530
|
+
* entries in one `list` seat keep that position. Both are ordered, so the
|
|
2531
|
+
* balance can sit closer to the picker than the probe icon.
|
|
2532
|
+
*/
|
|
2533
|
+
const BALANCE_SEAT_ID = "workbuddy-credit-balance";
|
|
2534
|
+
/**
|
|
2535
|
+
* The assistant message's action strip — the row holding copy, the plugin
|
|
2536
|
+
* actions, branch and the usage pill.
|
|
2537
|
+
*
|
|
2538
|
+
* A `list` seat declared by `dsh-client-ui-chat`, scoped to the session, whose
|
|
2539
|
+
* owner currency is `{ messageId }`. The shipped Like/Dislike entry registers
|
|
2540
|
+
* into it too, which is why this is the supported place for a per-message
|
|
2541
|
+
* annotation: it is the row WorkBuddy's own client puts "共消耗" in.
|
|
2542
|
+
*/
|
|
2543
|
+
const MESSAGE_ACTIONS_SEAT = "conversation.chat.assistant-actions";
|
|
2544
|
+
/** Entry id for the per-message credit label. */
|
|
2545
|
+
const CREDIT_LABEL_SEAT_ID = "workbuddy-credit-label";
|
|
1772
2546
|
function apply(ctx) {
|
|
1773
2547
|
const t = ctx.locale.bind(NS);
|
|
1774
2548
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
@@ -1786,15 +2560,40 @@ window.__ModuleLoader__.load({
|
|
|
1786
2560
|
inject: () => config.inject()
|
|
1787
2561
|
}, WorkBuddyConfigPage))), "dsh-workbuddy-bridge: configuration page");
|
|
1788
2562
|
ctx.inject(["modelDirectories"], (scope) => {
|
|
1789
|
-
scope.slots.inject(PROBE_SEAT, () =>
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2563
|
+
scope.slots.inject(PROBE_SEAT, () => {
|
|
2564
|
+
const releaseProbe = scope.slots.register({
|
|
2565
|
+
name: PROBE_SEAT,
|
|
2566
|
+
id: PROBE_SEAT_ID,
|
|
2567
|
+
inject: (sessionId) => ({
|
|
2568
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2569
|
+
t
|
|
2570
|
+
})
|
|
2571
|
+
}, WorkBuddyProbeControl);
|
|
2572
|
+
const releaseBalance = scope.slots.register({
|
|
2573
|
+
name: PROBE_SEAT,
|
|
2574
|
+
id: BALANCE_SEAT_ID,
|
|
2575
|
+
order: -10,
|
|
2576
|
+
inject: (sessionId) => ({
|
|
2577
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2578
|
+
t
|
|
2579
|
+
})
|
|
2580
|
+
}, WorkBuddyCreditBalance);
|
|
2581
|
+
return () => {
|
|
2582
|
+
releaseBalance();
|
|
2583
|
+
releaseProbe();
|
|
2584
|
+
};
|
|
2585
|
+
});
|
|
1797
2586
|
});
|
|
2587
|
+
ctx.inject(["modelDirectories"], (scope) => scope.slots.inject(MESSAGE_ACTIONS_SEAT, () => scope.slots.register({
|
|
2588
|
+
name: MESSAGE_ACTIONS_SEAT,
|
|
2589
|
+
id: CREDIT_LABEL_SEAT_ID,
|
|
2590
|
+
locale: NS,
|
|
2591
|
+
inject: (sessionId) => ({
|
|
2592
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2593
|
+
sessionId,
|
|
2594
|
+
t
|
|
2595
|
+
})
|
|
2596
|
+
}, WorkBuddyCreditLabel)));
|
|
1798
2597
|
}
|
|
1799
2598
|
//#endregion
|
|
1800
2599
|
exports.BUNDLE_NAME = BUNDLE_NAME;
|