dsh-workbuddy-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1780 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-workbuddy-bridge",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react_dom = require("react-dom");
11
+ //#region src/client/format.ts
12
+ /** Display formatting shared by the card's panels. */
13
+ /**
14
+ * Compact token count: the catalog's own round numbers (`200000`, `1000000`)
15
+ * read better as `200K` / `1M`, and no precision is lost because these values
16
+ * are always whole thousands.
17
+ */
18
+ function formatTokens(tokens) {
19
+ if (tokens >= 1e6 && tokens % 1e6 === 0) return `${tokens / 1e6}M`;
20
+ if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
21
+ return String(tokens);
22
+ }
23
+ /** Locale-formatted count. */
24
+ function formatNumber(value) {
25
+ return new Intl.NumberFormat(void 0).format(value);
26
+ }
27
+ /** Locale-formatted date and time from epoch milliseconds. */
28
+ function formatTime(ms) {
29
+ return new Intl.DateTimeFormat(void 0, {
30
+ dateStyle: "medium",
31
+ timeStyle: "short"
32
+ }).format(new Date(ms));
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
+ /** Locale-formatted percentage, at most one fraction digit. */
45
+ function formatPercent(percent) {
46
+ return new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent);
47
+ }
48
+ //#endregion
49
+ //#region \0workbuddy-css:D:\ProjectSomething\dsh-plugins\dsh-workbuddy-bridge\src\client\workbuddy.module.css.js
50
+ const css$1 = "._1kHyqG_page,._1kHyqG_cards{flex-direction:column;gap:12px;display:flex}._1kHyqG_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}._1kHyqG_toggleLabel{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}._1kHyqG_toggleHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}._1kHyqG_body{flex-direction:column;padding:4px 0 12px;display:flex}._1kHyqG_section{flex-direction:column;gap:6px;margin:12px 0;display:flex}._1kHyqG_title{color:var(--dsw-alias-label-primary);margin:0;font-size:13px;font-weight:500;line-height:1.5}._1kHyqG_row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;padding:12px 0;display:flex}._1kHyqG_summaryRow{flex-wrap:wrap;gap:4px;height:auto;min-height:28px}._1kHyqG_summaryTitle{flex:1;min-width:0}._1kHyqG_statusLine{color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;font-size:12px;line-height:1.5;display:inline-flex}._1kHyqG_text{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:1.5}._1kHyqG_dim{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}._1kHyqG_error{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:1.5}._1kHyqG_package{flex-direction:column;gap:8px;display:flex}._1kHyqG_packageLabel{color:var(--dsw-alias-label-secondary);justify-content:space-between;gap:8px;font-size:13px;line-height:1.5;display:flex}._1kHyqG_track{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;height:8px;overflow:hidden}._1kHyqG_fill{border-radius:inherit;background:var(--dsw-alias-brand-primary);height:100%}._1kHyqG_list{border:.5px solid var(--dsw-alias-border-l1);border-radius:10px;gap:6px;min-width:0;max-height:280px;margin:0;padding:14px;display:grid;overflow:auto}._1kHyqG_modelRow{justify-content:space-between;align-items:flex-start;gap:8px;display:flex}._1kHyqG_modelMain{align-items:center;gap:8px;min-width:0;display:flex}._1kHyqG_modelStack{flex-direction:column;gap:2px;min-width:0;display:flex}._1kHyqG_name{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}._1kHyqG_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}._1kHyqG_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5}._1kHyqG_badges{flex-wrap:wrap;align-items:center;gap:6px;display:flex}._1kHyqG_sectionActions{justify-content:flex-end;display:flex}._1kHyqG_probeRow{justify-content:space-between;align-items:center;gap:8px;display:flex}._1kHyqG_probeEnd{flex:none;align-items:center;gap:8px;display:inline-flex}._1kHyqG_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}._1kHyqG_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}._1kHyqG_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}";
51
+ const tagId$1 = "dsh-workbuddy-bridge/workbuddy.module.css";
52
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
53
+ const tag = document.createElement("style");
54
+ tag.dataset.plugin = "dsh-workbuddy-bridge";
55
+ tag.dataset.pluginCss = tagId$1;
56
+ tag.textContent = css$1;
57
+ document.head.appendChild(tag);
58
+ }
59
+ var workbuddy_module_css_default = {
60
+ "assist": "_1kHyqG_assist",
61
+ "badges": "_1kHyqG_badges",
62
+ "body": "_1kHyqG_body",
63
+ "cards": "_1kHyqG_cards",
64
+ "dim": "_1kHyqG_dim",
65
+ "error": "_1kHyqG_error",
66
+ "fill": "_1kHyqG_fill",
67
+ "list": "_1kHyqG_list",
68
+ "meta": "_1kHyqG_meta",
69
+ "modelEnd": "_1kHyqG_modelEnd",
70
+ "modelMain": "_1kHyqG_modelMain",
71
+ "modelRow": "_1kHyqG_modelRow",
72
+ "modelStack": "_1kHyqG_modelStack",
73
+ "name": "_1kHyqG_name",
74
+ "package": "_1kHyqG_package",
75
+ "packageLabel": "_1kHyqG_packageLabel",
76
+ "page": "_1kHyqG_page",
77
+ "probeEnd": "_1kHyqG_probeEnd",
78
+ "probeRow": "_1kHyqG_probeRow",
79
+ "prompt": "_1kHyqG_prompt",
80
+ "promptRow": "_1kHyqG_promptRow",
81
+ "row": "_1kHyqG_row",
82
+ "section": "_1kHyqG_section",
83
+ "sectionActions": "_1kHyqG_sectionActions",
84
+ "statusLine": "_1kHyqG_statusLine",
85
+ "summaryRow": "_1kHyqG_summaryRow",
86
+ "summaryTitle": "_1kHyqG_summaryTitle",
87
+ "text": "_1kHyqG_text",
88
+ "title": "_1kHyqG_title",
89
+ "toggleHint": "_1kHyqG_toggleHint",
90
+ "toggleLabel": "_1kHyqG_toggleLabel",
91
+ "toggleRow": "_1kHyqG_toggleRow",
92
+ "track": "_1kHyqG_track"
93
+ };
94
+ //#endregion
95
+ //#region src/client/panels.tsx
96
+ /**
97
+ * The card's three panels and the Agent assist block.
98
+ *
99
+ * Every control here is an official primitive (`Tag`, `Checkbox`, `Button`,
100
+ * `Pill`), so the panels inherit the host theme and keyboard behaviour without
101
+ * this plugin shipping a single hand-built widget.
102
+ */
103
+ /**
104
+ * The reason codes whose failures the Agent assist block covers: the plugin
105
+ * cannot reach a decryption program, for any of the five reasons the host
106
+ * reports.
107
+ *
108
+ * This is the *only* place the card decides whether the block applies. It
109
+ * branches on the code, never on `reason` text: the prose is written for a
110
+ * human and is expected to change, so matching it would silently stop matching
111
+ * after any wording edit.
112
+ *
113
+ * `encrypted-credential-unreadable` is deliberately absent — the app was found
114
+ * and ran, so "look for the app" is not the fix for it.
115
+ */
116
+ const ASSIST_REASON_CODES = [
117
+ "electron-binary-not-found",
118
+ "electron-binary-ambiguous",
119
+ "electron-binary-unavailable",
120
+ "electron-path-invalid",
121
+ "electron-discovery-incomplete"
122
+ ];
123
+ /** Whether a reason code is one the assist block covers. */
124
+ function assistCodeFor(reasonCode) {
125
+ return reasonCode !== void 0 && ASSIST_REASON_CODES.includes(reasonCode) ? reasonCode : void 0;
126
+ }
127
+ /** Locale key for one failure's summary inside the Agent prompt. */
128
+ function assistSummaryKey(code, variant) {
129
+ switch (code) {
130
+ case "electron-binary-not-found": return "assistNotFound";
131
+ case "electron-binary-ambiguous": return "assistAmbiguous";
132
+ case "electron-discovery-incomplete": return "assistIncomplete";
133
+ case "electron-path-invalid": return "assistPathInvalid";
134
+ default: return variant.unavailableKey;
135
+ }
136
+ }
137
+ /**
138
+ * Localize an upstream promotional badge label, with an unknown-badge fallback.
139
+ *
140
+ * The CN catalog spells badges in Chinese (`限时免费`, `夜间折扣`); the
141
+ * international document carries English (`Free now`). Both are mapped so the
142
+ * same promotion reads consistently in either UI language, and anything else
143
+ * passes through verbatim — an unrecognized badge is still information the
144
+ * upstream chose to show.
145
+ */
146
+ function badgeLabel(badge, t) {
147
+ if (badge === "限时免费") return t("badgeLimitedFree");
148
+ if (badge === "夜间折扣") return t("badgeNightDiscount");
149
+ if (badge === "Free now") return t("badgeFreeNow");
150
+ return badge;
151
+ }
152
+ /** One model's promotional badges, plus `Free` when the upstream says so. */
153
+ function ModelBadges({ model, t }) {
154
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
155
+ className: workbuddy_module_css_default.badges,
156
+ children: [model.badges?.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
157
+ tone: "success",
158
+ children: badgeLabel(badge, t)
159
+ }, badge)), model.free === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
160
+ tone: "success",
161
+ children: t("freeModel")
162
+ }) : null]
163
+ });
164
+ }
165
+ /**
166
+ * One billing package as a labelled progress bar.
167
+ *
168
+ * A package whose allowance the upstream never reported (`size` not positive)
169
+ * has no percentage to state. It must not fall back to 100%: the plugin would
170
+ * be claiming a full quota it knows nothing about, which is the opposite of the
171
+ * honest "remaining N" line printed below it. Unknown size therefore renders the
172
+ * percent slot as unknown copy and an unfilled, indeterminate track.
173
+ */
174
+ function CreditBar({ label, remain, size, unlimited, t }) {
175
+ const quota = t("unlimitedQuota");
176
+ if (unlimited === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
177
+ className: workbuddy_module_css_default.package,
178
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
179
+ className: workbuddy_module_css_default.packageLabel,
180
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: quota })]
181
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
182
+ className: workbuddy_module_css_default.track,
183
+ role: "progressbar",
184
+ "aria-label": label,
185
+ "aria-valuetext": quota
186
+ })]
187
+ });
188
+ const sizeKnown = size > 0;
189
+ const detail = sizeKnown ? t("exactRemaining", {
190
+ remain: formatNumber(remain),
191
+ size: formatNumber(size)
192
+ }) : t("creditPackageUnknownSize", { remain: formatNumber(remain) });
193
+ const percent = sizeKnown ? remain / size * 100 : void 0;
194
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
195
+ className: workbuddy_module_css_default.package,
196
+ children: [
197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
198
+ className: workbuddy_module_css_default.packageLabel,
199
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: percent === void 0 ? t("percentUnknown") : t("percentRemaining", { percent: formatPercent(percent) }) })]
200
+ }),
201
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
202
+ className: workbuddy_module_css_default.track,
203
+ role: "progressbar",
204
+ "aria-label": label,
205
+ ...percent === void 0 ? { "aria-valuetext": detail } : {
206
+ "aria-valuemin": 0,
207
+ "aria-valuemax": 100,
208
+ "aria-valuenow": percent
209
+ },
210
+ children: percent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
211
+ className: workbuddy_module_css_default.fill,
212
+ style: { width: `${Math.max(0, Math.min(100, percent))}%` }
213
+ })
214
+ }),
215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
216
+ className: workbuddy_module_css_default.meta,
217
+ children: detail
218
+ })
219
+ ]
220
+ });
221
+ }
222
+ /** The per-package credit breakdown, under the card's own "Credits" tab. */
223
+ function CreditsPanel({ credits, t }) {
224
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
225
+ className: workbuddy_module_css_default.section,
226
+ children: credits.accounts.filter((account) => account.packageName === "enterprise" || account.remain > 0 || account.unlimited === true).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
227
+ label: account.packageName === "enterprise" ? t("packageEnterprise") : account.packageName,
228
+ remain: account.remain,
229
+ size: account.size,
230
+ unlimited: account.unlimited,
231
+ t
232
+ }, `${account.packageName}-${String(index)}`))
233
+ });
234
+ }
235
+ /**
236
+ * The model catalog: one row per model, visibility checkbox on the left,
237
+ * context window, rate and badges on the right.
238
+ *
239
+ * The list is driven by the full current catalog, not by context metadata:
240
+ * hiding a model is a statement about the picker, and a model without a
241
+ * declared window is still hideable — its row just shows an em dash where the
242
+ * capacity would be. Rows with a window keep the original ordering (largest
243
+ * first); rows without one trail at the end in catalog order.
244
+ *
245
+ * Purely a report of the upstream's own numbers. The plugin offers no tier
246
+ * picker: the CN catalog declares one capacity per model and publishes no
247
+ * alternatives, so a menu there would mean inventing client-side policy. The
248
+ * international document does declare alternatives, and they are shown as a
249
+ * secondary figure rather than merged into one number — the default is the
250
+ * budget actually requested, while the larger value is a ceiling the upstream
251
+ * would accept.
252
+ */
253
+ function ModelsPanel({ models, visibility, toggling, disabled, onToggle, t }) {
254
+ const rows = [...models ?? []].sort((a, b) => {
255
+ if (a.contextWindow === void 0) return b.contextWindow === void 0 ? 0 : 1;
256
+ if (b.contextWindow === void 0) return -1;
257
+ return b.contextWindow - a.contextWindow;
258
+ });
259
+ if (rows.length === 0) return null;
260
+ const hidden = new Set(visibility?.hidden ?? []);
261
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
262
+ className: workbuddy_module_css_default.section,
263
+ children: [visibility === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
264
+ className: workbuddy_module_css_default.text,
265
+ children: t("visibilityIntro")
266
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
267
+ className: workbuddy_module_css_default.list,
268
+ children: rows.map((model) => {
269
+ const capacity = model.contextWindow;
270
+ const alternative = capacity !== void 0 && model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
271
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
272
+ className: workbuddy_module_css_default.modelRow,
273
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
274
+ className: workbuddy_module_css_default.modelMain,
275
+ children: visibility === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
276
+ className: workbuddy_module_css_default.name,
277
+ children: model.name
278
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
279
+ checked: !hidden.has(model.id),
280
+ disabled: disabled || toggling.has(model.id),
281
+ label: model.name,
282
+ onChange: (visible) => {
283
+ onToggle(model.id, visible, visibility.account);
284
+ }
285
+ })
286
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
287
+ className: workbuddy_module_css_default.modelEnd,
288
+ children: [
289
+ model.credits === void 0 ? model.rateUnknown === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
290
+ className: workbuddy_module_css_default.dim,
291
+ children: t("rateUnknown")
292
+ }) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
293
+ className: workbuddy_module_css_default.dim,
294
+ children: t("rate", { rate: model.credits })
295
+ }),
296
+ capacity === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
297
+ className: workbuddy_module_css_default.meta,
298
+ "aria-label": t("contextUnknown"),
299
+ children: "—"
300
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatTokens(capacity) }),
301
+ alternative !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
302
+ className: workbuddy_module_css_default.dim,
303
+ children: t("contextUpTo", { size: formatTokens(alternative) })
304
+ }) : capacity !== void 0 && model.defaultContextWindow !== void 0 && model.defaultContextWindow < capacity ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
305
+ className: workbuddy_module_css_default.dim,
306
+ children: t("contextDefault", { size: formatTokens(model.defaultContextWindow) })
307
+ }) : null,
308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
309
+ className: workbuddy_module_css_default.badges,
310
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelBadges, {
311
+ model,
312
+ t
313
+ })
314
+ })
315
+ ]
316
+ })]
317
+ }, model.id);
318
+ })
319
+ })]
320
+ });
321
+ }
322
+ /**
323
+ * Reasoning-effort detection: one row per detectable model.
324
+ *
325
+ * Two deliberate UX rules:
326
+ * - **one press detects.** The row states what the model accepts and the button
327
+ * beside it runs the check; the cost is stated once, above the list, instead
328
+ * of being asked again per row. A confirmation whose only other option is
329
+ * "cancel" costs a click and answers nothing;
330
+ * - a `non-validating` result is presented as an observation about the
331
+ * parameter ("this model does not check it"), never as a statement that a
332
+ * level is unsupported.
333
+ *
334
+ * The tab bar owns the heading, so this panel renders no heading of its own.
335
+ */
336
+ function ProbePanel({ probe, models, busy, onDetect, onClear, t }) {
337
+ const [runningModel, setRunningModel] = (0, react.useState)();
338
+ const armed = (0, react.useRef)(false);
339
+ (0, react.useEffect)(() => {
340
+ if (runningModel === void 0) return;
341
+ if (busy || probe.running) {
342
+ armed.current = true;
343
+ return;
344
+ }
345
+ if (!armed.current) return;
346
+ armed.current = false;
347
+ setRunningModel(void 0);
348
+ }, [
349
+ runningModel,
350
+ busy,
351
+ probe.running
352
+ ]);
353
+ if (probe.candidates.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
354
+ className: workbuddy_module_css_default.text,
355
+ children: t("probeResultEmpty")
356
+ });
357
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
358
+ className: workbuddy_module_css_default.section,
359
+ children: [
360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
361
+ className: workbuddy_module_css_default.text,
362
+ children: t("probeCostNote")
363
+ }),
364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
365
+ className: workbuddy_module_css_default.list,
366
+ children: probe.candidates.map((id) => {
367
+ const result = probe.results.find((entry) => entry.id === id);
368
+ const name = models?.find((model) => model.id === id)?.name ?? result?.name ?? id;
369
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
370
+ className: workbuddy_module_css_default.modelStack,
371
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
372
+ className: workbuddy_module_css_default.probeRow,
373
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
374
+ className: workbuddy_module_css_default.name,
375
+ children: name
376
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
377
+ className: workbuddy_module_css_default.probeEnd,
378
+ children: [result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
379
+ tone: result.validation === "validating" ? "success" : "neutral",
380
+ children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : t(result.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown")
381
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
382
+ size: "sm",
383
+ disabled: probe.running || busy,
384
+ onClick: () => {
385
+ setRunningModel(id);
386
+ onDetect(id);
387
+ },
388
+ children: runningModel === id ? t("probeRunning", { model: name }) : t(result === void 0 ? "probeStart" : "probeRedetect")
389
+ })]
390
+ })]
391
+ }), result === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
+ className: workbuddy_module_css_default.meta,
393
+ children: t("probeResultAt", { time: formatTime(result.probedAt) })
394
+ })]
395
+ }, id);
396
+ })
397
+ }),
398
+ probe.results.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
399
+ className: workbuddy_module_css_default.sectionActions,
400
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
401
+ size: "sm",
402
+ disabled: busy,
403
+ onClick: onClear,
404
+ children: t("probeClear")
405
+ })
406
+ })
407
+ ]
408
+ });
409
+ }
410
+ /**
411
+ * The Agent assist block for a path failure: what is wrong, one copyable
412
+ * request, and a re-check. Rendered only for the codes the host can do
413
+ * something about.
414
+ */
415
+ function AssistBlock({ variant, code, busy, onRecheck, t }) {
416
+ const [copied, setCopied] = (0, react.useState)(false);
417
+ const [copyFailed, setCopyFailed] = (0, react.useState)(false);
418
+ const prompt = t("assistantPrompt", {
419
+ appName: variant.appName,
420
+ failureSummary: t(assistSummaryKey(code, variant))
421
+ });
422
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
423
+ className: workbuddy_module_css_default.assist,
424
+ children: [
425
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
426
+ className: workbuddy_module_css_default.title,
427
+ children: t("assistantHeading")
428
+ }),
429
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
430
+ className: workbuddy_module_css_default.text,
431
+ children: t("assistantIntro")
432
+ }),
433
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
434
+ className: workbuddy_module_css_default.promptRow,
435
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
436
+ className: workbuddy_module_css_default.prompt,
437
+ children: prompt
438
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
439
+ size: "sm",
440
+ onClick: () => {
441
+ (0, _deepseek_ai_dsh_client_ui_primitives.writeClipboard)(prompt).then((ok) => {
442
+ setCopied(ok);
443
+ setCopyFailed(!ok);
444
+ });
445
+ },
446
+ children: t("assistantCopy")
447
+ })]
448
+ }),
449
+ copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
450
+ className: workbuddy_module_css_default.dim,
451
+ role: "status",
452
+ children: t("assistantCopied")
453
+ }) : null,
454
+ copyFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
455
+ className: workbuddy_module_css_default.dim,
456
+ role: "status",
457
+ children: t("assistantCopyFailed")
458
+ }) : null,
459
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
460
+ className: workbuddy_module_css_default.text,
461
+ children: t("assistantAfter")
462
+ }),
463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
464
+ size: "sm",
465
+ disabled: busy,
466
+ onClick: onRecheck,
467
+ children: busy ? t("assistantRechecking") : t("assistantRecheck")
468
+ })
469
+ ]
470
+ });
471
+ }
472
+ //#endregion
473
+ //#region src/client/status-document.ts
474
+ /**
475
+ * Whether a parsed status response really is a status document.
476
+ *
477
+ * A 200 is not a promise about the body: it may be empty, literal `null`, a
478
+ * non-JSON page from a proxy, or an array. Both halves of the browser plugin
479
+ * read the same route, so both must agree on what is valid — storing an
480
+ * unreadable value puts something in state that the next render dereferences.
481
+ *
482
+ * The check is deliberately limited to the discriminator (plus `error`'s
483
+ * `message`, which the error paragraph renders): validating optional fields
484
+ * here would reject documents the host legitimately omits fields from.
485
+ *
486
+ * `reasonCode` is therefore *not* rejected here — a card renders `reason`
487
+ * either way — but every reader must narrow it with
488
+ * `isWorkBuddySignedOutReasonCode` before branching on it, since the wire
489
+ * value is not guaranteed to be inside the enum.
490
+ */
491
+ function isWorkBuddyWebStatus(value) {
492
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
493
+ const wrapped = value;
494
+ const status = wrapped["status"];
495
+ if (status === "signed-out" || status === "signed-in") return true;
496
+ return status === "error" && typeof wrapped["message"] === "string";
497
+ }
498
+ //#endregion
499
+ //#region src/client/use-status.ts
500
+ /**
501
+ * One variant's live status: the read, the poll, and every write.
502
+ *
503
+ * Three policies this hook exists to hold, each of which was a real defect
504
+ * before it was named:
505
+ *
506
+ * 1. **A failed read never erases a document already on screen.** It is
507
+ * recorded beside that document instead. Blanking the card over one
508
+ * transient error loses the account, credits, and model list the reader was
509
+ * looking at, which is worse than the error.
510
+ * 2. **The newest read wins.** A slow poll begun before a manual action must
511
+ * not settle after that action's own refresh and restore the older
512
+ * document, so every read is numbered when it *starts*.
513
+ * 3. **The poll's liveness depends on the last successful read, not on what is
514
+ * rendered.** A failed read must not disarm the interval, or one blip
515
+ * leaves the card blank until the user clicks Refresh.
516
+ *
517
+ * Writes (refresh the catalog, detect reasoning levels, hide a model) share the
518
+ * control route and the read-back: each one re-reads afterwards so the host's
519
+ * truth is what stays on screen, and each reports its refusal beside the
520
+ * document rather than in place of it.
521
+ */
522
+ /** How often the status document is re-read while a variant is signed in. */
523
+ const POLL_INTERVAL_MS = 6e4;
524
+ /**
525
+ * Own one variant's status document.
526
+ * @param variant - which product's routes and copy to use.
527
+ * @param t - the dictionary reader, for the two messages this hook composes itself.
528
+ */
529
+ function useWorkBuddyStatus(variant, t) {
530
+ const [status, setStatus] = (0, react.useState)();
531
+ const [readFailure, setReadFailure] = (0, react.useState)();
532
+ const [busy, setBusy] = (0, react.useState)(false);
533
+ const [toggling, setToggling] = (0, react.useState)(() => /* @__PURE__ */ new Set());
534
+ /**
535
+ * Whether the last read that *stated* the session found none — a genuine
536
+ * signed-out answer, as opposed to a read that failed. Kept apart from
537
+ * `status` on purpose: a failed read leaves it untouched, so a transient
538
+ * failure cannot silence the poll, while a real signed-out stops the asking.
539
+ *
540
+ * A ref, not state: making it a dependency would re-arm the interval on every
541
+ * sign-in, restarting the minute and re-reading immediately on top of the
542
+ * read that just changed it.
543
+ */
544
+ const signedOut = (0, react.useRef)(false);
545
+ const mounted = (0, react.useRef)(true);
546
+ /** Number of the newest read that may write; assigned when a read starts. */
547
+ const readSeq = (0, react.useRef)(0);
548
+ /** Manual requests in flight, so unmount can abort them like the poll's. */
549
+ const inFlight = (0, react.useRef)(/* @__PURE__ */ new Set());
550
+ (0, react.useEffect)(() => {
551
+ mounted.current = true;
552
+ return () => {
553
+ mounted.current = false;
554
+ for (const controller of inFlight.current) controller.abort();
555
+ inFlight.current.clear();
556
+ };
557
+ }, []);
558
+ /** Register a manual request so unmount aborts it. */
559
+ const track = (0, react.useCallback)(() => {
560
+ const controller = new AbortController();
561
+ inFlight.current.add(controller);
562
+ return controller;
563
+ }, []);
564
+ /**
565
+ * Read the status document and apply it.
566
+ * @returns whether this read produced the document now on screen.
567
+ */
568
+ const read = (0, react.useCallback)(async (signal) => {
569
+ const seq = ++readSeq.current;
570
+ const current = () => mounted.current && signal?.aborted !== true && seq === readSeq.current;
571
+ try {
572
+ const response = await fetch(variant.statusPath, {
573
+ headers: { accept: "application/json" },
574
+ credentials: "same-origin",
575
+ ...signal === void 0 ? {} : { signal }
576
+ });
577
+ const value = await response.json().catch(() => void 0);
578
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
579
+ if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
580
+ if (!current()) return false;
581
+ setStatus(value);
582
+ if (value.status === "signed-in") signedOut.current = false;
583
+ else if (value.status === "signed-out") signedOut.current = true;
584
+ setReadFailure(void 0);
585
+ return true;
586
+ } catch (error) {
587
+ const message = error instanceof Error ? error.message : t("requestFailed");
588
+ if (current()) {
589
+ setReadFailure(message);
590
+ setStatus((previous) => previous ?? {
591
+ status: "error",
592
+ message
593
+ });
594
+ }
595
+ return false;
596
+ }
597
+ }, [t, variant.statusPath]);
598
+ /**
599
+ * POST one control action, then re-read so the host's truth is what stays.
600
+ *
601
+ * The key travels in a header, not the body: it authorizes the write, and
602
+ * the host never accepts a prompt, a sentinel, or a model outside its own
603
+ * catalog from here.
604
+ */
605
+ const control = (0, react.useCallback)(async (action) => {
606
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
607
+ if (key === void 0) return;
608
+ const perRow = action.action === "set-model-visibility";
609
+ if (perRow) setToggling((previous) => new Set(previous).add(action.model));
610
+ else setBusy(true);
611
+ const controller = track();
612
+ try {
613
+ const response = await fetch(variant.probePath, {
614
+ method: "POST",
615
+ headers: {
616
+ "Content-Type": "application/json",
617
+ "X-WorkBuddy-Probe-Key": key
618
+ },
619
+ credentials: "same-origin",
620
+ signal: controller.signal,
621
+ body: JSON.stringify(action)
622
+ });
623
+ const value = await response.json().catch(() => void 0);
624
+ if (!response.ok) throw new Error(errorMessage(value, response.status));
625
+ if (action.action === "set-model-visibility" && fieldOf(value, "state") !== "updated") {
626
+ if (fieldOf(value, "state") === "stale-account") {
627
+ await read(controller.signal);
628
+ throw new Error(t("visibilityStaleAccount"));
629
+ }
630
+ throw new Error(fieldOf(value, "reason") ?? t("requestFailed"));
631
+ }
632
+ await read(controller.signal);
633
+ } catch (error) {
634
+ if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
635
+ } finally {
636
+ inFlight.current.delete(controller);
637
+ if (mounted.current) {
638
+ if (perRow) setToggling((previous) => {
639
+ const next = new Set(previous);
640
+ if (action.action === "set-model-visibility") next.delete(action.model);
641
+ return next;
642
+ });
643
+ else setBusy(false);
644
+ }
645
+ }
646
+ }, [
647
+ read,
648
+ status,
649
+ t,
650
+ track,
651
+ variant.probePath
652
+ ]);
653
+ /**
654
+ * The poll. Armed on mount and disarmed on unmount; a genuine signed-out
655
+ * answer makes it skip its turns rather than clear itself, so signing in on
656
+ * the desktop is picked up within the minute instead of needing a click.
657
+ */
658
+ (0, react.useEffect)(() => {
659
+ const controller = new AbortController();
660
+ read(controller.signal);
661
+ const timer = window.setInterval(() => {
662
+ if (signedOut.current) return;
663
+ read(controller.signal);
664
+ }, POLL_INTERVAL_MS);
665
+ return () => {
666
+ window.clearInterval(timer);
667
+ controller.abort();
668
+ };
669
+ }, [read]);
670
+ return {
671
+ status,
672
+ readFailure,
673
+ busy,
674
+ toggling,
675
+ refresh: (0, react.useCallback)(async () => {
676
+ setBusy(true);
677
+ const controller = track();
678
+ try {
679
+ await read(controller.signal);
680
+ } finally {
681
+ inFlight.current.delete(controller);
682
+ if (mounted.current) setBusy(false);
683
+ }
684
+ }, [read, track]),
685
+ refreshModels: (0, react.useCallback)(() => control({ action: "refresh" }), [control]),
686
+ detect: (0, react.useCallback)((model) => control({
687
+ action: "probe",
688
+ model
689
+ }), [control]),
690
+ clearDetections: (0, react.useCallback)(() => control({ action: "clear" }), [control]),
691
+ setVisibility: (0, react.useCallback)((model, visible, account) => control({
692
+ action: "set-model-visibility",
693
+ model,
694
+ visible,
695
+ account
696
+ }), [control])
697
+ };
698
+ }
699
+ /** Read one string field out of an unknown JSON body. */
700
+ function fieldOf(value, field) {
701
+ if (typeof value !== "object" || value === null) return void 0;
702
+ const found = value[field];
703
+ return typeof found === "string" ? found : void 0;
704
+ }
705
+ /** The reason a rejected control response carries, or the HTTP status. */
706
+ function errorMessage(value, status) {
707
+ return fieldOf(value, "error") ?? fieldOf(value, "reason") ?? `HTTP ${status}`;
708
+ }
709
+ //#endregion
710
+ //#region src/client/WorkBuddyCard.tsx
711
+ /**
712
+ * One WorkBuddy variant's card.
713
+ *
714
+ * The shell is the host's own `DisclosureRow`, so the card inherits the Plugins
715
+ * page's disclosure chrome, keyboard handling, and theme instead of shipping a
716
+ * hand-built expandable container. What remains plugin-owned is the *content*:
717
+ * the account state, where the model list came from, the credit breakdown, the
718
+ * context windows with their per-account visibility controls, and the
719
+ * reasoning-effort detection.
720
+ */
721
+ /**
722
+ * The state dot's semantic.
723
+ *
724
+ * Takes `'loading'` as well as the document's own states: before the first
725
+ * response the card knows nothing about the account, so it must not borrow the
726
+ * signed-out grey — that would read as "nothing is wrong, nobody is signed in"
727
+ * when the truth is "not read yet".
728
+ */
729
+ function dotState(status) {
730
+ if (status === void 0) return "ongoing";
731
+ if (status.status === "signed-in") return "done";
732
+ return status.status === "error" ? "error" : "idle";
733
+ }
734
+ /** The one-line account state. `undefined` status is "not read yet", not signed-out. */
735
+ function accountLabel(status, t) {
736
+ if (status === void 0) return t("loading");
737
+ if (status.status === "signed-in") return status.nickname === void 0 ? t("signedIn") : t("signedInAs", { nickname: status.nickname });
738
+ return status.status === "error" ? t("requestFailed") : t("signedOut");
739
+ }
740
+ /** Where the model list on screen came from, and when. */
741
+ function catalogProvenance(catalog, t) {
742
+ const when = catalog.fetchedAt === void 0 ? void 0 : formatTime(catalog.fetchedAt);
743
+ const line = catalog.source === "live" && when !== void 0 ? t("catalogLive", { time: when }) : catalog.source === "saved" && when !== void 0 ? t("catalogSaved", { time: when }) : t("catalogFallback");
744
+ return catalog.appVersion === void 0 ? line : `${line} · ${t("catalogAppVersion", { version: catalog.appVersion })}`;
745
+ }
746
+ /** Render one variant's live status card. */
747
+ function WorkBuddyCard({ variant, t }) {
748
+ const [open, setOpen] = (0, react.useState)(false);
749
+ const [tab, setTab] = (0, react.useState)("credits");
750
+ const { status, readFailure, busy, toggling, refresh, refreshModels, detect, clearDetections, setVisibility } = useWorkBuddyStatus(variant, t);
751
+ const baseId = (0, react.useId)();
752
+ const creditsTab = {
753
+ value: "credits",
754
+ label: t("tabCredits"),
755
+ id: `${baseId}-tab-credits`,
756
+ panelId: `${baseId}-panel-credits`
757
+ };
758
+ const modelsTab = {
759
+ value: "models",
760
+ label: t("tabModels"),
761
+ id: `${baseId}-tab-models`,
762
+ panelId: `${baseId}-panel-models`
763
+ };
764
+ const probeTab = {
765
+ value: "probe",
766
+ label: t("tabProbe"),
767
+ id: `${baseId}-tab-probe`,
768
+ panelId: `${baseId}-panel-probe`
769
+ };
770
+ const tabs = [
771
+ creditsTab,
772
+ modelsTab,
773
+ probeTab
774
+ ];
775
+ /**
776
+ * The failure the assist block covers, when this document has one. Computed
777
+ * once so the block and the refresh button agree on which one owns the
778
+ * re-check — showing both would put two buttons with the same effect side by
779
+ * side.
780
+ */
781
+ const assistCode = status?.status === "signed-out" ? assistCodeFor(status.reasonCode) : void 0;
782
+ const signedIn = status?.status === "signed-in" ? status : void 0;
783
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.DisclosureRow, {
784
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState(status) }),
785
+ title: t(variant.titleKey),
786
+ titleClassName: workbuddy_module_css_default.summaryTitle,
787
+ rowClassName: workbuddy_module_css_default.summaryRow,
788
+ open,
789
+ expandable: true,
790
+ expandOnRowClick: true,
791
+ onToggle: () => {
792
+ setOpen(!open);
793
+ },
794
+ collapsedContent: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
795
+ className: workbuddy_module_css_default.statusLine,
796
+ role: "status",
797
+ "aria-busy": status === void 0,
798
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: dotState(status) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: accountLabel(status, t) })]
799
+ }),
800
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
801
+ className: workbuddy_module_css_default.body,
802
+ children: [
803
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
804
+ className: workbuddy_module_css_default.section,
805
+ children: [
806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
807
+ className: workbuddy_module_css_default.title,
808
+ children: t("accountHeading")
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 })
824
+ }),
825
+ signedIn?.expiresAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
826
+ className: workbuddy_module_css_default.text,
827
+ children: t("accessTokenExpires", { time: formatTime(signedIn.expiresAt) })
828
+ })
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", {
847
+ className: workbuddy_module_css_default.error,
848
+ children: t("catalogError", { message: signedIn.catalog.error })
849
+ }),
850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
851
+ items: tabs,
852
+ value: tab,
853
+ onChange: setTab,
854
+ label: t("tabLabel")
855
+ }),
856
+ tab === "credits" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
857
+ className: workbuddy_module_css_default.section,
858
+ id: creditsTab.panelId,
859
+ role: "tabpanel",
860
+ "aria-labelledby": creditsTab.id,
861
+ children: [
862
+ signedIn.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
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
+ }),
875
+ signedIn.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditsPanel, {
876
+ credits: signedIn.credits,
877
+ t
878
+ }),
879
+ signedIn.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
880
+ className: workbuddy_module_css_default.error,
881
+ children: t("creditsError", { message: signedIn.creditsError })
882
+ })
883
+ ]
884
+ }) : tab === "models" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
885
+ className: workbuddy_module_css_default.section,
886
+ id: modelsTab.panelId,
887
+ role: "tabpanel",
888
+ "aria-labelledby": modelsTab.id,
889
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelsPanel, {
890
+ models: signedIn.models,
891
+ visibility: signedIn.visibility,
892
+ toggling,
893
+ disabled: busy,
894
+ t,
895
+ onToggle: (model, visible, account) => {
896
+ setVisibility(model, visible, account);
897
+ }
898
+ })
899
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
900
+ className: workbuddy_module_css_default.section,
901
+ id: probeTab.panelId,
902
+ role: "tabpanel",
903
+ "aria-labelledby": probeTab.id,
904
+ children: signedIn.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbePanel, {
905
+ probe: signedIn.probe,
906
+ models: signedIn.models,
907
+ busy,
908
+ t,
909
+ onDetect: (model) => {
910
+ detect(model);
911
+ },
912
+ onClear: () => {
913
+ clearDetections();
914
+ }
915
+ })
916
+ })
917
+ ] }),
918
+ status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
919
+ className: status.reason === void 0 ? workbuddy_module_css_default.text : workbuddy_module_css_default.error,
920
+ children: status.reason ?? t(variant.signedOutKey)
921
+ }), assistCode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistBlock, {
922
+ variant,
923
+ code: assistCode,
924
+ busy,
925
+ t,
926
+ onRecheck: () => {
927
+ refresh();
928
+ }
929
+ })] }) : null,
930
+ status?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
931
+ className: workbuddy_module_css_default.error,
932
+ children: status.message
933
+ }) : null
934
+ ]
935
+ })
936
+ });
937
+ }
938
+ /** Both variants, in display order. */
939
+ const CARD_VARIANTS = [{
940
+ id: "workbuddy",
941
+ titleKey: "title",
942
+ signedOutKey: "signedOutHint",
943
+ statusPath: "/plugins/dsh-workbuddy-bridge/status",
944
+ probePath: "/plugins/dsh-workbuddy-bridge/probe",
945
+ appName: "WorkBuddy",
946
+ unavailableKey: "assistUnavailableCN"
947
+ }, {
948
+ id: "workbuddy-ai",
949
+ titleKey: "titleAI",
950
+ signedOutKey: "signedOutHintAI",
951
+ statusPath: "/plugins/dsh-workbuddy-bridge/ai/status",
952
+ probePath: "/plugins/dsh-workbuddy-bridge/ai/probe",
953
+ appName: "WorkBuddy AI",
954
+ unavailableKey: "assistUnavailableAI"
955
+ }];
956
+ /** The card (and therefore the routes) a selected provider belongs to. */
957
+ function cardVariantFor(provider) {
958
+ return CARD_VARIANTS.find((card) => card.id === provider);
959
+ }
960
+ //#endregion
961
+ //#region src/client/locales.ts
962
+ /** English copy. */
963
+ const en = {
964
+ title: "WorkBuddy (CN)",
965
+ intro: "Follows the sign-in of the WorkBuddy desktop app and serves its models here.",
966
+ titleAI: "WorkBuddy AI (Global)",
967
+ loading: "Loading account…",
968
+ signedOut: "Not signed in",
969
+ signedOutHint: "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
970
+ signedOutHintAI: "Sign in once in the WorkBuddy AI desktop app; this plugin follows that sign-in automatically.",
971
+ signedIn: "Signed in",
972
+ signedInAs: "Signed in as {nickname}",
973
+ accessTokenExpires: "Sign-in expires {time}; it renews automatically.",
974
+ accountHeading: "Account",
975
+ requestFailed: "Request failed",
976
+ statusRefreshFailed: "Refresh failed: {message} — showing the last known state",
977
+ statusResponseInvalid: "WorkBuddy returned an unreadable status reply",
978
+ catalogLive: "Model list updated {time}",
979
+ catalogSaved: "Showing the saved model list from {time}",
980
+ catalogFallback: "Showing the built-in model list (not yet updated from WorkBuddy)",
981
+ catalogError: "Last update failed: {message}",
982
+ catalogAppVersion: "App version {version}",
983
+ refresh: "Refresh",
984
+ refreshing: "Refreshing…",
985
+ refreshModels: "Refresh model list",
986
+ refreshingModels: "Refreshing models…",
987
+ tabLabel: "WorkBuddy detail",
988
+ tabCredits: "Credits",
989
+ tabModels: "Models",
990
+ tabProbe: "Detection",
991
+ creditsTotal: "Total: {total}",
992
+ creditsTotalUnlimited: "Total: Unlimited",
993
+ unlimitedQuota: "Unlimited",
994
+ packageEnterprise: "Enterprise quota",
995
+ cycleResetAt: "Resets {time}",
996
+ percentRemaining: "{percent}% remaining",
997
+ percentUnknown: "Remaining share unknown",
998
+ exactRemaining: "{remain} / {size} remaining",
999
+ creditPackageUnknownSize: "{remain} remaining",
1000
+ creditsError: "Credit unavailable: {message}",
1001
+ freeModel: "Free",
1002
+ badgeLimitedFree: "Limited-time free",
1003
+ badgeNightDiscount: "Night discount",
1004
+ badgeFreeNow: "Free for now",
1005
+ rate: "{rate} credits per message",
1006
+ rateUnknown: "Price unavailable — refresh to update",
1007
+ contextUpTo: "up to {size}",
1008
+ contextDefault: "default {size}",
1009
+ contextUnknown: "no declared context window",
1010
+ 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
+ visibilityStaleAccount: "The signed-in account changed — this change was not saved.",
1012
+ probeCostNote: "Some models reason but declare no selectable effort levels. Detecting sends a few real requests to one model and may consume a small amount of credit.",
1013
+ probeStart: "Detect",
1014
+ probeRedetect: "Detect again",
1015
+ probeRunning: "Detecting {model}…",
1016
+ probeClear: "Clear results",
1017
+ probeResultNotValidating: "This model does not check the effort parameter",
1018
+ probeResultUnknown: "Detection did not complete",
1019
+ probeResultAt: "Detected {time}",
1020
+ probeResultEmpty: "No models need detecting right now.",
1021
+ probeFailed: "Detection failed: {message}",
1022
+ probeLabel: "Reasoning levels",
1023
+ probePanelLevels: "Supported levels",
1024
+ probePanelNone: "Not detected yet",
1025
+ probePanelNote: "Detection sends a few requests to this model and may consume a small amount of credit.",
1026
+ probePanelDetect: "Detect",
1027
+ probePanelDetecting: "Detecting…",
1028
+ probePanelRedetect: "Detect again",
1029
+ probePanelNotValidating: "This model does not check the effort parameter.",
1030
+ probePanelFailed: "Detection did not finish. You can run it again.",
1031
+ probeTooltipIdle: "Detect the reasoning levels {model} supports",
1032
+ probeTooltipLevels: "Supported levels: {levels}",
1033
+ probeTooltipNotValidating: "This model ignores the reasoning-level parameter",
1034
+ probeTooltipFailed: "Detection did not finish · click to run it again",
1035
+ authFile: "WorkBuddy auth file",
1036
+ authFileHint: "Path to the WorkBuddy desktop auth file. Leave blank to use the app’s own location.",
1037
+ authFileAI: "WorkBuddy AI auth file",
1038
+ authFileAIHint: "Path to the WorkBuddy AI desktop auth file. Leave blank to use the app’s own location.",
1039
+ probeConsent: "Allow reasoning-effort detection",
1040
+ probeConsentHint: "Lets the plugin send test requests to a model to confirm which effort levels it accepts; each detection may consume a small amount of credit.",
1041
+ maximumContextWindow: "Use the largest declared context window",
1042
+ maximumContextWindowHint: "Applies to WorkBuddy AI models that offer a larger window.",
1043
+ overridden: "Overridden",
1044
+ reset: "Reset to default",
1045
+ readOnly: "This deployment stores settings read-only.",
1046
+ unavailable: "This plugin is not loaded, so it cannot be configured right now.",
1047
+ save: "Save",
1048
+ saving: "Saving…",
1049
+ saveFailed: "The deployment did not accept these values; they were left for you to correct.",
1050
+ assistantHeading: "Let an Agent sort this out",
1051
+ assistantIntro: "Send the request below to your Agent; it will check the app location and the launch configuration for you.",
1052
+ assistantCopy: "Copy for Agent",
1053
+ assistantCopied: "Copied",
1054
+ assistantCopyFailed: "Copy failed — select the text above and copy it manually",
1055
+ assistantAfter: "When your Agent is done, come back and check again. If the DSH launch environment was changed, restart DSH first as instructed.",
1056
+ assistantRecheck: "Done — check again",
1057
+ assistantRechecking: "Checking…",
1058
+ assistantPrompt: "DSH's dsh-workbuddy-bridge cannot use my {appName}: {failureSummary}. Please check the actual installation location and any existing path configuration, help the plugin use it correctly, and verify recovery. If the DSH launch environment must be changed or DSH restarted, give me clear steps; do not only set an environment variable temporarily in the current shell.",
1059
+ assistNotFound: "no usable decryption program was found",
1060
+ assistAmbiguous: "more than one WorkBuddy copy was found and none could be chosen safely",
1061
+ assistIncomplete: "the automatic search could not be completed",
1062
+ assistPathInvalid: "the configured program path is not usable",
1063
+ assistUnavailableCN: "no decryption program is configured for this platform",
1064
+ assistUnavailableAI: "no decryption program is configured for WorkBuddy AI"
1065
+ };
1066
+ /** Simplified Chinese copy. */
1067
+ const zh = {
1068
+ title: "WorkBuddy(国内版)",
1069
+ intro: "跟随 WorkBuddy 桌面 App 的登录状态,在此直接使用它的模型。",
1070
+ titleAI: "WorkBuddy AI(国际版)",
1071
+ loading: "正在读取账号…",
1072
+ signedOut: "未登录",
1073
+ signedOutHint: "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
1074
+ signedOutHintAI: "在 WorkBuddy AI 国际版桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
1075
+ signedIn: "已登录",
1076
+ signedInAs: "已登录:{nickname}",
1077
+ accessTokenExpires: "登录将于 {time} 过期,届时自动续期。",
1078
+ accountHeading: "账号",
1079
+ requestFailed: "请求失败",
1080
+ statusRefreshFailed: "刷新失败:{message} — 当前显示的是上次成功获取的状态",
1081
+ statusResponseInvalid: "WorkBuddy 返回的状态数据无法识别",
1082
+ catalogLive: "模型列表更新于 {time}",
1083
+ catalogSaved: "当前显示已保存的模型列表,更新于 {time}",
1084
+ catalogFallback: "当前显示内置模型列表(尚未从 WorkBuddy 更新)",
1085
+ catalogError: "上次更新失败:{message}",
1086
+ catalogAppVersion: "App 版本 {version}",
1087
+ refresh: "刷新",
1088
+ refreshing: "正在刷新…",
1089
+ refreshModels: "刷新模型列表",
1090
+ refreshingModels: "正在刷新模型…",
1091
+ tabLabel: "WorkBuddy 详情",
1092
+ tabCredits: "积分",
1093
+ tabModels: "模型",
1094
+ tabProbe: "检测",
1095
+ creditsTotal: "合计:{total}",
1096
+ creditsTotalUnlimited: "合计:不限额",
1097
+ unlimitedQuota: "不限额",
1098
+ packageEnterprise: "企业额度",
1099
+ cycleResetAt: "重置时间:{time}",
1100
+ percentRemaining: "剩余 {percent}%",
1101
+ percentUnknown: "剩余占比未知",
1102
+ exactRemaining: "剩余 {remain} / {size}",
1103
+ creditPackageUnknownSize: "剩余 {remain}",
1104
+ creditsError: "积分查询失败:{message}",
1105
+ freeModel: "免费",
1106
+ badgeLimitedFree: "限时免费",
1107
+ badgeNightDiscount: "夜间折扣",
1108
+ badgeFreeNow: "当前免费",
1109
+ rate: "{rate} 积分/次",
1110
+ rateUnknown: "价格未知 — 刷新后更新",
1111
+ contextUpTo: "最高 {size}",
1112
+ contextDefault: "默认 {size}",
1113
+ contextUnknown: "未声明上下文窗口",
1114
+ visibilityIntro: "取消勾选即可将该模型从选择器中隐藏;按登录账号分别保存,已在用该模型的会话不受影响。",
1115
+ visibilityStaleAccount: "登录账号已切换——本次修改未保存。",
1116
+ probeCostNote: "部分模型具备思考能力,但没有声明可选档位。检测会向一个模型发送少量真实请求以确认可用档位,可能消耗少量积分。",
1117
+ probeStart: "开始检测",
1118
+ probeRedetect: "重新检测",
1119
+ probeRunning: "正在检测 {model}…",
1120
+ probeClear: "清除结果",
1121
+ probeResultNotValidating: "该模型不校验档位参数",
1122
+ probeResultUnknown: "检测未完成",
1123
+ probeResultAt: "检测于 {time}",
1124
+ probeResultEmpty: "当前没有需要检测的模型。",
1125
+ probeFailed: "检测失败:{message}",
1126
+ probeLabel: "推理档位",
1127
+ probePanelLevels: "支持的档位",
1128
+ probePanelNone: "尚未检测",
1129
+ probePanelNote: "检测会向该模型发送若干请求,可能消耗少量积分。",
1130
+ probePanelDetect: "检测",
1131
+ probePanelDetecting: "检测中…",
1132
+ probePanelRedetect: "重新检测",
1133
+ probePanelNotValidating: "该模型不校验档位参数。",
1134
+ probePanelFailed: "检测未完成,可以再检测一次。",
1135
+ probeTooltipIdle: "检测 {model} 支持的推理档位",
1136
+ probeTooltipLevels: "支持的档位:{levels}",
1137
+ probeTooltipNotValidating: "该模型忽略推理档位参数",
1138
+ probeTooltipFailed: "检测未完成 · 点击可再检测一次",
1139
+ authFile: "WorkBuddy 登录文件",
1140
+ authFileHint: "WorkBuddy 桌面 App 登录文件的路径。留空表示使用应用自身的位置。",
1141
+ authFileAI: "WorkBuddy AI 登录文件",
1142
+ authFileAIHint: "WorkBuddy AI 桌面 App 登录文件的路径。留空表示使用应用自身的位置。",
1143
+ probeConsent: "允许推理档位检测",
1144
+ probeConsentHint: "允许插件向模型发送检测请求,以确认它接受哪些推理档位;每次检测可能消耗少量积分。",
1145
+ maximumContextWindow: "使用上游声明的最大上下文窗口",
1146
+ maximumContextWindowHint: "仅作用于 WorkBuddy AI 中声明了更大窗口的模型。",
1147
+ overridden: "已覆盖",
1148
+ reset: "恢复默认",
1149
+ readOnly: "本部署的设置为只读。",
1150
+ unavailable: "该插件当前未加载,暂时无法配置。",
1151
+ save: "保存",
1152
+ saving: "保存中…",
1153
+ saveFailed: "本部署没有接受这些值,已保留供你修改。",
1154
+ assistantHeading: "让 Agent 帮你处理",
1155
+ assistantIntro: "把下面这段请求发给你的 Agent,它会协助检查应用位置和启动配置。",
1156
+ assistantCopy: "复制给 Agent",
1157
+ assistantCopied: "已复制",
1158
+ assistantCopyFailed: "复制失败,请手动选择上方文字复制",
1159
+ assistantAfter: "Agent 处理完成后,回到这里重新检查;如果修改了 DSH 的启动环境,请先按指引重启 DSH。",
1160
+ assistantRecheck: "已处理,重新检查",
1161
+ assistantRechecking: "正在检查…",
1162
+ assistantPrompt: "DSH 的 dsh-workbuddy-bridge 无法使用我的 {appName}:{failureSummary}。请帮我检查实际安装位置和已有路径配置,让插件能正确使用它,并验证恢复结果;如果需要修改 DSH 的启动环境或重启,请给我明确的操作步骤,不要只在当前 shell 临时设置环境变量。",
1163
+ assistNotFound: "没有找到可用的解密程序",
1164
+ assistAmbiguous: "找到了多个 WorkBuddy 副本,无法安全自动选择",
1165
+ assistIncomplete: "自动定位未能完成",
1166
+ assistPathInvalid: "指定的程序路径不可用",
1167
+ assistUnavailableCN: "当前平台尚未配置解密程序",
1168
+ assistUnavailableAI: "尚未配置 WorkBuddy AI 的解密程序"
1169
+ };
1170
+ /** The form frame's copy, read from this plugin's dictionary. */
1171
+ function formLabels(t) {
1172
+ return {
1173
+ unavailable: t("unavailable"),
1174
+ readOnly: t("readOnly"),
1175
+ saveFailed: t("saveFailed"),
1176
+ save: t("save"),
1177
+ saving: t("saving")
1178
+ };
1179
+ }
1180
+ //#endregion
1181
+ //#region src/client/WorkBuddyConfigPage.tsx
1182
+ /** Stable field ids, so the labels and the panel ids agree across renders. */
1183
+ const FIELD_IDS = {
1184
+ authFile: "workbuddy-config-auth-file",
1185
+ authFileAI: "workbuddy-config-auth-file-ai"
1186
+ };
1187
+ /**
1188
+ * Render the bundle's page: the one-liner a summary seat asks for, or the form
1189
+ * and the live cards.
1190
+ */
1191
+ function WorkBuddyConfigPage(props) {
1192
+ const { t } = props;
1193
+ if (props.view === "summary") return t("intro");
1194
+ const state = props.useWorkbuddyConfig((snapshot) => snapshot);
1195
+ const disabled = !state.writable;
1196
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
+ className: workbuddy_module_css_default.page,
1198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.SettingsForm, {
1199
+ labels: formLabels(t),
1200
+ state,
1201
+ onSave: props.save,
1202
+ onDiscard: props.discard,
1203
+ children: [
1204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsValueField, {
1205
+ id: FIELD_IDS.authFile,
1206
+ label: t("authFile"),
1207
+ hint: t("authFileHint"),
1208
+ overriddenLabel: t("overridden"),
1209
+ resetLabel: t("reset"),
1210
+ invalidLabel: t("saveFailed"),
1211
+ disabled,
1212
+ ...state.authFile,
1213
+ onEdit: (text) => {
1214
+ props.edit("authFile", text);
1215
+ },
1216
+ onReset: () => {
1217
+ props.resetField("authFile");
1218
+ }
1219
+ }),
1220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.SettingsValueField, {
1221
+ id: FIELD_IDS.authFileAI,
1222
+ label: t("authFileAI"),
1223
+ hint: t("authFileAIHint"),
1224
+ overriddenLabel: t("overridden"),
1225
+ resetLabel: t("reset"),
1226
+ invalidLabel: t("saveFailed"),
1227
+ disabled,
1228
+ ...state.authFileAI,
1229
+ onEdit: (text) => {
1230
+ props.edit("authFileAI", text);
1231
+ },
1232
+ onReset: () => {
1233
+ props.resetField("authFileAI");
1234
+ }
1235
+ }),
1236
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1237
+ className: workbuddy_module_css_default.toggleRow,
1238
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1239
+ className: workbuddy_module_css_default.toggleLabel,
1240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeConsent") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1241
+ className: workbuddy_module_css_default.toggleHint,
1242
+ children: t("probeConsentHint")
1243
+ })]
1244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1245
+ checked: state.probeConsent.text === "true",
1246
+ disabled,
1247
+ label: t("probeConsent"),
1248
+ onChange: (next) => {
1249
+ props.edit("probeConsent", next ? "true" : "false");
1250
+ }
1251
+ })]
1252
+ }),
1253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1254
+ className: workbuddy_module_css_default.toggleRow,
1255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1256
+ className: workbuddy_module_css_default.toggleLabel,
1257
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("maximumContextWindow") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1258
+ className: workbuddy_module_css_default.toggleHint,
1259
+ children: t("maximumContextWindowHint")
1260
+ })]
1261
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
1262
+ checked: state.useMaximumContextWindow.text === "true",
1263
+ disabled,
1264
+ label: t("maximumContextWindow"),
1265
+ onChange: (next) => {
1266
+ props.edit("useMaximumContextWindow", next ? "true" : "false");
1267
+ }
1268
+ })]
1269
+ })
1270
+ ]
1271
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1272
+ className: workbuddy_module_css_default.cards,
1273
+ children: CARD_VARIANTS.map((variant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkBuddyCard, {
1274
+ variant,
1275
+ t
1276
+ }, variant.id))
1277
+ })]
1278
+ });
1279
+ }
1280
+ //#endregion
1281
+ //#region \0workbuddy-css:D:\ProjectSomething\dsh-plugins\dsh-workbuddy-bridge\src\client\probe-control.module.css.js
1282
+ const css = ".Ivd5xG_wrapper{align-items:center;display:inline-flex;position:relative}.Ivd5xG_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}.Ivd5xG_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.Ivd5xG_trigger:disabled{cursor:default;color:var(--dsw-alias-label-dimmed)}.Ivd5xG_trigger:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}.Ivd5xG_trigger[aria-busy=true] svg{transform-origin:50%;animation:1.2s linear infinite Ivd5xG_wb-probe-spin}@keyframes Ivd5xG_wb-probe-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.Ivd5xG_trigger[aria-busy=true] svg{animation:none}}.Ivd5xG_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}.Ivd5xG_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}.Ivd5xG_panelTitle{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:1.5;display:flex}.Ivd5xG_panelTitleIcon{color:var(--dsw-alias-label-secondary);flex:none;align-items:center;display:inline-flex}.Ivd5xG_panelTitleText{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Ivd5xG_titleRule{border-top:.5px solid var(--dsw-alias-border-l2);margin:8px 0 10px}.Ivd5xG_levelsLabel{color:var(--dsw-alias-label-tertiary);margin:0 0 2px;font-size:12px;line-height:1.5}.Ivd5xG_levels{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:13px;font-weight:500;line-height:1.5}.Ivd5xG_note,.Ivd5xG_dim{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.6}.Ivd5xG_dim{margin-top:2px}.Ivd5xG_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) {
1285
+ const tag = document.createElement("style");
1286
+ tag.dataset.plugin = "dsh-workbuddy-bridge";
1287
+ tag.dataset.pluginCss = tagId;
1288
+ tag.textContent = css;
1289
+ document.head.appendChild(tag);
1290
+ }
1291
+ var probe_control_module_css_default = {
1292
+ "dim": "Ivd5xG_dim",
1293
+ "levels": "Ivd5xG_levels",
1294
+ "levelsLabel": "Ivd5xG_levelsLabel",
1295
+ "note": "Ivd5xG_note",
1296
+ "panel": "Ivd5xG_panel",
1297
+ "panelActions": "Ivd5xG_panelActions",
1298
+ "panelTitle": "Ivd5xG_panelTitle",
1299
+ "panelTitleIcon": "Ivd5xG_panelTitleIcon",
1300
+ "panelTitleText": "Ivd5xG_panelTitleText",
1301
+ "titleRule": "Ivd5xG_titleRule",
1302
+ "trigger": "Ivd5xG_trigger",
1303
+ "wb-probe-spin": "Ivd5xG_wb-probe-spin",
1304
+ "wrapper": "Ivd5xG_wrapper"
1305
+ };
1306
+ //#endregion
1307
+ //#region src/client/probe-control.tsx
1308
+ /**
1309
+ * Per-model reasoning-effort entry beside the Composer's model selector.
1310
+ *
1311
+ * It is **only an icon** — no inline text. The composer row is shared with the
1312
+ * host's own model picker, and a word here competes with the model name for the
1313
+ * same glance while adding nothing: the verified levels already appear in the
1314
+ * model dropdown (the adapter exposes them as selectable efforts). What the
1315
+ * control offers is an *action*, so it is drawn like the other icon-only
1316
+ * buttons in this chrome, and its meaning lives in the tooltip and the
1317
+ * accessible name.
1318
+ *
1319
+ * Styling follows `dsh-ds-balance`'s popover button, which copies the host's own
1320
+ * sidebar `.iconButton` (28px circle, `--dsw-alias-label-secondary` icon on a
1321
+ * transparent background, `--dsw-alias-interactive-bg-hover` on hover). The
1322
+ * artwork strokes `currentColor`, so light and dark themes are handled by the
1323
+ * token rather than by two sets of colours — no `[data-ds-dark-theme]` selector
1324
+ * and no hard-coded colour anywhere.
1325
+ *
1326
+ * - a **hover/focus tooltip** carries the state and the click's purpose, and is
1327
+ * also the button's `aria-label`.
1328
+ * - the **confirmation** is a popover anchored to the control, not a
1329
+ * `window.confirm`. Probing spends real credit, so a confirmation stays — but
1330
+ * it belongs next to the thing it acts on.
1331
+ *
1332
+ * The popover surface follows `dsh-ds-balance`'s balance popover, which itself
1333
+ * copies the host's own stat dialog: a portal to `document.body`, positioned by
1334
+ * the host's `useAnchoredPosition`, dismissed by the host's
1335
+ * `useDismissOnOutsidePointer`, skinned with `--dsw-specific-menu` plus
1336
+ * `--dsw-menu-backdrop-filter` (the pair the host requires together — fill
1337
+ * alone is "translucent but not frosted").
1338
+ *
1339
+ * **The filter must not sit on the panel itself**: a non-`none`
1340
+ * `backdrop-filter` makes the element the containing block for its fixed
1341
+ * descendants, and this panel contains non-portal `Tooltip` bubbles. The fill
1342
+ * and the filter therefore live on an isolated `::before`, exactly as the host
1343
+ * does it.
1344
+ *
1345
+ * @module dsh-workbuddy-bridge/client/probe-control
1346
+ */
1347
+ /** How often the control re-checks state when the window regains focus. */
1348
+ const RECONCILE_MS = 6e4;
1349
+ /**
1350
+ * What the popover renders on its first frame: positioned but invisible, so the
1351
+ * anchor hook can measure it before deciding where it really goes. Copied
1352
+ * verbatim from the host's stat dialog via `dsh-ds-balance`.
1353
+ */
1354
+ const MEASURE_STYLE = {
1355
+ visibility: "hidden",
1356
+ left: 0,
1357
+ top: 0
1358
+ };
1359
+ /** Gap between the control and the popover, and the viewport margin it keeps. */
1360
+ const POPOVER_GAP = 8;
1361
+ const POPOVER_MARGIN = 12;
1362
+ /** Pick the model's recorded observation out of the probe section. */
1363
+ function resultFor(status, model) {
1364
+ if (status.status !== "signed-in") return void 0;
1365
+ return status.probe?.results.find((result) => result.id === model);
1366
+ }
1367
+ /**
1368
+ * What hovering the icon says: the levels this model accepts, when they are
1369
+ * known.
1370
+ *
1371
+ * The answer is the *result*, not the action — a user hovering a small glyph
1372
+ * beside the model picker is asking "what does this model support?", and the
1373
+ * action is what the panel they can open is for.
1374
+ *
1375
+ * A recorded result outranks a remembered failure: `failed` only means "the last
1376
+ * run from this control did not finish", and the host can record a result for
1377
+ * the same model at any time (a detection started from the settings card,
1378
+ * another conversation, or a finished sweep). The levels the user already paid
1379
+ * for are the better answer; failure copy is what remains when there is none.
1380
+ */
1381
+ function tooltipText(t, model, state) {
1382
+ if (state.busy) return t("probeRunning", { model });
1383
+ const result = state.result;
1384
+ if (result !== void 0) {
1385
+ if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipLevels", { levels: result.efforts.join(" / ") });
1386
+ if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1387
+ return t("probeTooltipFailed");
1388
+ }
1389
+ return state.failed ? t("probeTooltipFailed") : t("probeTooltipIdle", { model });
1390
+ }
1391
+ /** The levels this model accepts, as one line; undefined when there are none to show. */
1392
+ function levelsLine(result) {
1393
+ if (result === void 0) return void 0;
1394
+ if (result.validation === "validating" && result.efforts.length > 0) return result.efforts.join(" / ");
1395
+ }
1396
+ /** Model-independent shell: resolves the selection, then delegates per model. */
1397
+ function WorkBuddyProbeControl({ directory, t }) {
1398
+ const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
1399
+ const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
1400
+ const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
1401
+ const card = selection === void 0 ? void 0 : cardVariantFor(selection.provider);
1402
+ if (card === void 0 || selection === void 0) return null;
1403
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
1404
+ model: selection.model,
1405
+ card,
1406
+ t
1407
+ }, `${card.id}:${selection.model}`);
1408
+ }
1409
+ function ModelProbe({ model, card, t }) {
1410
+ const [status, setStatus] = (0, react.useState)();
1411
+ const [busy, setBusy] = (0, react.useState)(false);
1412
+ /**
1413
+ * Whether the panel is open.
1414
+ *
1415
+ * One flag, not a confirmation-plus-result pair: the panel shows the same
1416
+ * thing before and after a run (the levels, and the one button that gets
1417
+ * them), so there is no second state to model. Opening it is not a commitment
1418
+ * either — the button inside is.
1419
+ */
1420
+ const [open, setOpen] = (0, react.useState)(false);
1421
+ const [failed, setFailed] = (0, react.useState)(false);
1422
+ /**
1423
+ * The outcome of the run this control just performed.
1424
+ *
1425
+ * Kept so a fresh answer is shown immediately, without waiting for the next
1426
+ * status read; `result` from the document is what stands when there is none.
1427
+ */
1428
+ const [fresh, setFresh] = (0, react.useState)();
1429
+ const inFlight = (0, react.useRef)(false);
1430
+ const mounted = (0, react.useRef)(false);
1431
+ const readSeq = (0, react.useRef)(0);
1432
+ /** The control itself: the popover's anchor and the "inside" test for dismissal. */
1433
+ const rootRef = (0, react.useRef)(null);
1434
+ /**
1435
+ * The panel, portalled to `document.body` so it is not clipped by the composer
1436
+ * row. It is measured by the anchor hook and passed to the outside-pointer test,
1437
+ * which would otherwise read a click on the panel as a click outside it.
1438
+ */
1439
+ const panelRef = (0, react.useRef)(null);
1440
+ const refresh = (0, react.useCallback)(async (signal) => {
1441
+ const seq = ++readSeq.current;
1442
+ const response = await fetch(card.statusPath, {
1443
+ credentials: "same-origin",
1444
+ headers: { accept: "application/json" },
1445
+ ...signal === void 0 ? {} : { signal }
1446
+ });
1447
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1448
+ const value = await response.json().catch(() => void 0);
1449
+ if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
1450
+ if (mounted.current && signal?.aborted !== true && seq === readSeq.current) setStatus(value);
1451
+ }, [card.statusPath, t]);
1452
+ (0, react.useEffect)(() => {
1453
+ mounted.current = true;
1454
+ const controller = new AbortController();
1455
+ const load = () => {
1456
+ refresh(controller.signal).catch(() => {});
1457
+ };
1458
+ load();
1459
+ const timer = window.setInterval(load, RECONCILE_MS);
1460
+ window.addEventListener("focus", load);
1461
+ return () => {
1462
+ mounted.current = false;
1463
+ controller.abort();
1464
+ window.clearInterval(timer);
1465
+ window.removeEventListener("focus", load);
1466
+ };
1467
+ }, [refresh]);
1468
+ const probe = status?.status === "signed-in" ? status.probe : void 0;
1469
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
1470
+ const result = status === void 0 ? void 0 : resultFor(status, model);
1471
+ const visible = probe?.candidates.includes(model) === true || result !== void 0;
1472
+ (0, react.useEffect)(() => {
1473
+ if (result !== void 0) setFailed(false);
1474
+ }, [result]);
1475
+ (0, react.useEffect)(() => {
1476
+ setOpen(false);
1477
+ setFresh(void 0);
1478
+ }, [model]);
1479
+ const detect = async () => {
1480
+ if (key === void 0 || inFlight.current || probe?.running === true) return;
1481
+ inFlight.current = true;
1482
+ setFresh(void 0);
1483
+ setBusy(true);
1484
+ setFailed(false);
1485
+ try {
1486
+ const response = await fetch(card.probePath, {
1487
+ method: "POST",
1488
+ credentials: "same-origin",
1489
+ headers: {
1490
+ "Content-Type": "application/json",
1491
+ "X-WorkBuddy-Probe-Key": key
1492
+ },
1493
+ body: JSON.stringify({
1494
+ action: "probe",
1495
+ model
1496
+ })
1497
+ });
1498
+ const body = await response.json();
1499
+ if (!response.ok || body.state !== "ok" || body.validation !== "validating" && body.validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
1500
+ if (mounted.current) setFresh({
1501
+ id: model,
1502
+ name: model,
1503
+ validation: body.validation,
1504
+ efforts: body.efforts,
1505
+ probedAt: Date.now()
1506
+ });
1507
+ refresh().catch(() => {});
1508
+ } catch {
1509
+ if (mounted.current) setFailed(true);
1510
+ } finally {
1511
+ inFlight.current = false;
1512
+ if (mounted.current) setBusy(false);
1513
+ }
1514
+ };
1515
+ const position = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
1516
+ open,
1517
+ anchorRef: rootRef,
1518
+ panelRef,
1519
+ side: "top",
1520
+ gap: POPOVER_GAP,
1521
+ margin: POPOVER_MARGIN
1522
+ });
1523
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, () => {
1524
+ setOpen(false);
1525
+ }, panelRef);
1526
+ if (!visible) return null;
1527
+ const text = tooltipText(t, model, {
1528
+ busy,
1529
+ result,
1530
+ failed
1531
+ });
1532
+ const disabled = busy || probe?.running === true || key === void 0;
1533
+ const shown = fresh ?? result;
1534
+ const levels = levelsLine(shown);
1535
+ const notValidating = shown?.validation === "non-validating";
1536
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1537
+ className: probe_control_module_css_default.wrapper,
1538
+ ref: rootRef,
1539
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1540
+ label: text,
1541
+ side: "top",
1542
+ portal: true,
1543
+ disabled: open,
1544
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1545
+ type: "button",
1546
+ className: probe_control_module_css_default.trigger,
1547
+ "aria-label": text,
1548
+ "aria-busy": busy,
1549
+ "aria-expanded": open,
1550
+ disabled,
1551
+ onClick: () => {
1552
+ setOpen(!open);
1553
+ },
1554
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeIcon, {})
1555
+ })
1556
+ }), open ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1557
+ ref: panelRef,
1558
+ className: probe_control_module_css_default.panel,
1559
+ style: position ?? MEASURE_STYLE,
1560
+ role: "dialog",
1561
+ "aria-label": t("probeLabel"),
1562
+ children: [
1563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1564
+ className: probe_control_module_css_default.panelTitle,
1565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1566
+ className: probe_control_module_css_default.panelTitleIcon,
1567
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeIcon, {})
1568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1569
+ className: probe_control_module_css_default.panelTitleText,
1570
+ children: model
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
+ ]
1612
+ }), document.body) : null]
1613
+ });
1614
+ }
1615
+ /**
1616
+ * The control's icon — the host's own reasoning glyph.
1617
+ *
1618
+ * Deliberately **not** a hand-drawn shape. Every icon in this chrome is one of
1619
+ * the host's, drawn on the same grid (16-unit viewBox, 1px stroke) with the same
1620
+ * `currentColor` convention, so borrowing the host's artwork is the only way to
1621
+ * land in the same visual language — anything original reads as foreign beside
1622
+ * the model picker it sits next to.
1623
+ *
1624
+ * `IconThinkOutlineRegular` is the host's semantic icon for reasoning, which is
1625
+ * exactly what this control acts on. It is exported from a package this plugin
1626
+ * already depends on, so it costs no new dependency edge.
1627
+ */
1628
+ function ProbeIcon() {
1629
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconThinkOutlineRegular, { size: 16 });
1630
+ }
1631
+ //#endregion
1632
+ //#region src/client/config-controller.ts
1633
+ /**
1634
+ * A boolean field. Staged as text like every other field, so one save covers
1635
+ * the whole form; the control renders the two states, not the string.
1636
+ */
1637
+ function settingsBooleanField(field) {
1638
+ return {
1639
+ field,
1640
+ format: (value) => value === true ? "true" : "false",
1641
+ parse: (text) => {
1642
+ const trimmed = text.trim();
1643
+ return trimmed === "true" || trimmed === "false" ? {
1644
+ kind: "set",
1645
+ value: trimmed === "true"
1646
+ } : void 0;
1647
+ }
1648
+ };
1649
+ }
1650
+ /** Bridges one Host entry's form onto the page. */
1651
+ var WorkBuddyConfigController = class {
1652
+ form;
1653
+ store;
1654
+ constructor(scope) {
1655
+ this.form = new _deepseek_ai_dsh_client_ui_primitives.SettingsFormModel(scope, [
1656
+ (0, _deepseek_ai_dsh_client_ui_primitives.settingsTextField)("authFile"),
1657
+ (0, _deepseek_ai_dsh_client_ui_primitives.settingsTextField)("authFileAI"),
1658
+ settingsBooleanField("probeConsent"),
1659
+ settingsBooleanField("useMaximumContextWindow")
1660
+ ]);
1661
+ this.store = this.form.bind(() => this.projection());
1662
+ }
1663
+ projection() {
1664
+ return {
1665
+ ...this.form.shell(),
1666
+ authFile: this.form.field("authFile"),
1667
+ authFileAI: this.form.field("authFileAI"),
1668
+ probeConsent: this.form.field("probeConsent"),
1669
+ useMaximumContextWindow: this.form.field("useMaximumContextWindow")
1670
+ };
1671
+ }
1672
+ /** Build the face the page's slot registration injects. */
1673
+ inject() {
1674
+ return {
1675
+ hooks: { workbuddyConfig: this.store },
1676
+ ...this.form.actions()
1677
+ };
1678
+ }
1679
+ /** Release the form's subscriptions. */
1680
+ dispose() {
1681
+ this.form.dispose();
1682
+ }
1683
+ };
1684
+ //#endregion
1685
+ //#region src/client/index.tsx
1686
+ /** Stable browser-plugin name. */
1687
+ const name = "dsh-workbuddy-bridge-client";
1688
+ /** Dictionary namespace owned by this plugin. */
1689
+ const NS = "settings.workbuddy";
1690
+ /**
1691
+ * The bundle's package name. This is the **slot key**: the Plugins page renders
1692
+ * `plugins.bundle.config` as `{ entryKey: pkg.name }`, and only shows the
1693
+ * section at all when `ledger.bundles.has(pkg.name)` — and `ledger.bundles` is
1694
+ * `keysOf('plugins.bundle.config')`, i.e. the keys we register here.
1695
+ */
1696
+ const BUNDLE_NAME = "dsh-workbuddy-bridge";
1697
+ /**
1698
+ * The loader entry id — `cordis.patch.yml`'s `insert[].id`, and therefore the
1699
+ * **settings namespace**.
1700
+ *
1701
+ * Not the same string as {@link BUNDLE_NAME}, and the difference is load-bearing.
1702
+ * `dsh-settings/lib/index.js:432,443` publishes every plugin's `Config` document
1703
+ * as `ns: entry.options.id` — the *entry* id, not the package name. The composed
1704
+ * profile confirms it:
1705
+ *
1706
+ * ```yaml
1707
+ * # == dsh-workbuddy-bridge
1708
+ * - id: llm-workbuddy # entry id → settings namespace
1709
+ * name: dsh-workbuddy-bridge # package → slot key
1710
+ * ```
1711
+ *
1712
+ * `ctx.configForms.get()` takes a namespace, so this is its argument. Passing
1713
+ * the package name instead looks correct and fails **silently**: the host never
1714
+ * serves that namespace, `whileServed` never fires, and the configuration page
1715
+ * simply never appears — no error, no log, nothing.
1716
+ */
1717
+ const ENTRY_ID = "llm-workbuddy";
1718
+ /**
1719
+ * Client services required by this browser half.
1720
+ *
1721
+ * `modelDirectories` is absent on purpose: it may arrive after this fiber
1722
+ * starts, so the composer seat waits for it inside a scoped callback instead of
1723
+ * holding the whole entry back.
1724
+ */
1725
+ const inject = [
1726
+ "slots",
1727
+ "locale",
1728
+ "remote",
1729
+ "remote.session",
1730
+ "configForms"
1731
+ ];
1732
+ /**
1733
+ * The host seat this control occupies: the composer's compact-controls row,
1734
+ * which the host renders immediately left of its own model selector.
1735
+ *
1736
+ * Deliberately not `conversation.input.model`: that seat is `single`, the
1737
+ * shipped ModelSelect already occupies it at priority 0, and a second
1738
+ * registration at that priority throws — the seat's own fail-loud rule. A
1739
+ * `list` seat adds an entry beside the shipped one instead of fighting it.
1740
+ */
1741
+ const PROBE_SEAT = "conversation.input.right";
1742
+ /** Entry id within that seat, so the registration is named in exactly one place. */
1743
+ const PROBE_SEAT_ID = "workbuddy-probe";
1744
+ function apply(ctx) {
1745
+ const t = ctx.locale.bind(NS);
1746
+ ctx.effect(() => ctx.locale.register(NS, {
1747
+ zh,
1748
+ en
1749
+ }), "dsh-workbuddy-bridge: dictionaries");
1750
+ const config = new WorkBuddyConfigController(ctx.configForms.get(ENTRY_ID));
1751
+ ctx.effect(() => () => {
1752
+ config.dispose();
1753
+ }, "dsh-workbuddy-bridge: form subscription");
1754
+ ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () => ctx.slots.inject("plugins.bundle.config", () => ctx.slots.register({
1755
+ name: "plugins.bundle.config",
1756
+ key: BUNDLE_NAME,
1757
+ locale: NS,
1758
+ inject: () => config.inject()
1759
+ }, WorkBuddyConfigPage))), "dsh-workbuddy-bridge: configuration page");
1760
+ ctx.inject(["modelDirectories"], (scope) => {
1761
+ scope.slots.inject(PROBE_SEAT, () => scope.slots.register({
1762
+ name: PROBE_SEAT,
1763
+ id: PROBE_SEAT_ID,
1764
+ inject: (sessionId) => ({
1765
+ directory: scope.modelDirectories.directoryFor(sessionId).store,
1766
+ t
1767
+ })
1768
+ }, WorkBuddyProbeControl));
1769
+ });
1770
+ }
1771
+ //#endregion
1772
+ exports.BUNDLE_NAME = BUNDLE_NAME;
1773
+ exports.ENTRY_ID = ENTRY_ID;
1774
+ exports.NS = NS;
1775
+ exports.apply = apply;
1776
+ exports.inject = inject;
1777
+ exports.name = name;
1778
+ return module.exports;
1779
+ }
1780
+ });