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/LICENSE +22 -0
- package/NOTICE +15 -0
- package/README.md +226 -0
- package/README_en.md +249 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +31 -0
- package/lib/bin.d.ts +6 -0
- package/lib/bin.js +237 -0
- package/lib/client.js +1780 -0
- package/lib/index.d.ts +1642 -0
- package/lib/index.js +2000 -0
- package/lib/variants-CFO_LIBm.js +3807 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +155 -0
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
|
+
});
|