dsh-workbuddy-connect 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,439 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-workbuddy-connect",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/status-paths.ts
10
+ /** Node-free constants and types shared by the Host and browser halves. */
11
+ /** Plugin-owned status endpoint consumed by its browser half. */
12
+ const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
13
+ //#endregion
14
+ //#region src/client/WorkBuddyPluginCard.tsx
15
+ /** WorkBuddy status card contributed to Harness Plugin configuration. */
16
+ const POLL_INTERVAL_MS = 6e4;
17
+ const cardStyle = {
18
+ overflow: "hidden",
19
+ border: "1px solid var(--dsw-alias-border-l2)",
20
+ borderRadius: 10,
21
+ background: "var(--dsw-alias-bg-module-platform)"
22
+ };
23
+ const headerStyle = {
24
+ boxSizing: "border-box",
25
+ width: "100%",
26
+ display: "flex",
27
+ alignItems: "center",
28
+ justifyContent: "space-between",
29
+ gap: 16,
30
+ border: 0,
31
+ padding: "13px 14px",
32
+ background: "transparent",
33
+ color: "var(--dsw-alias-label-primary)",
34
+ font: "inherit",
35
+ textAlign: "left",
36
+ cursor: "pointer"
37
+ };
38
+ const headTextStyle = {
39
+ display: "flex",
40
+ minWidth: 0,
41
+ flexDirection: "column",
42
+ gap: 3
43
+ };
44
+ const nameStyle = {
45
+ fontSize: 14,
46
+ lineHeight: "20px",
47
+ fontWeight: 600
48
+ };
49
+ const descriptionStyle = {
50
+ fontSize: 13,
51
+ lineHeight: "18px",
52
+ color: "var(--dsw-alias-label-tertiary)"
53
+ };
54
+ const chevronStyle = {
55
+ flex: "0 0 auto",
56
+ fontSize: 18,
57
+ lineHeight: 1,
58
+ transition: "transform 120ms ease"
59
+ };
60
+ const cardBodyStyle = {
61
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
62
+ padding: "16px 14px 18px"
63
+ };
64
+ const bodyStyle = {
65
+ margin: 0,
66
+ fontSize: 14,
67
+ lineHeight: "22px",
68
+ color: "var(--dsw-alias-label-secondary)"
69
+ };
70
+ const rowStyle = {
71
+ display: "flex",
72
+ alignItems: "center",
73
+ justifyContent: "space-between",
74
+ flexWrap: "wrap",
75
+ gap: 12
76
+ };
77
+ const statusStyle = {
78
+ display: "flex",
79
+ alignItems: "center",
80
+ gap: 9,
81
+ fontSize: 15,
82
+ fontWeight: 500,
83
+ color: "var(--dsw-alias-label-primary)"
84
+ };
85
+ const buttonStyle = {
86
+ boxSizing: "border-box",
87
+ minHeight: 34,
88
+ padding: "6px 14px",
89
+ border: "1px solid var(--dsw-alias-border-l2)",
90
+ borderRadius: 18,
91
+ background: "var(--dsw-alias-bg-layer-1)",
92
+ color: "var(--dsw-alias-label-primary)",
93
+ font: "inherit",
94
+ fontSize: 14,
95
+ cursor: "pointer"
96
+ };
97
+ const errorStyle = {
98
+ ...bodyStyle,
99
+ color: "var(--dsw-alias-state-error-primary)"
100
+ };
101
+ const quotaListStyle = {
102
+ display: "flex",
103
+ flexDirection: "column",
104
+ gap: 18,
105
+ paddingTop: 2
106
+ };
107
+ const quotaGroupStyle = {
108
+ display: "flex",
109
+ flexDirection: "column",
110
+ gap: 10
111
+ };
112
+ const quotaTitleStyle = {
113
+ margin: 0,
114
+ fontSize: 14,
115
+ lineHeight: "20px",
116
+ fontWeight: 600,
117
+ color: "var(--dsw-alias-label-primary)"
118
+ };
119
+ const quotaLabelStyle = {
120
+ display: "flex",
121
+ justifyContent: "space-between",
122
+ gap: 12,
123
+ fontSize: 13,
124
+ lineHeight: "20px",
125
+ color: "var(--dsw-alias-label-secondary)"
126
+ };
127
+ const progressTrackStyle = {
128
+ height: 8,
129
+ overflow: "hidden",
130
+ borderRadius: 999,
131
+ background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
132
+ };
133
+ function progressFillStyle(percent) {
134
+ return {
135
+ width: `${Math.max(0, Math.min(100, percent))}%`,
136
+ height: "100%",
137
+ borderRadius: "inherit",
138
+ background: "var(--dsw-alias-brand-primary, #1677ff)"
139
+ };
140
+ }
141
+ function dotStyle(status) {
142
+ return {
143
+ width: 9,
144
+ height: 9,
145
+ borderRadius: "50%",
146
+ flex: "0 0 auto",
147
+ background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)"
148
+ };
149
+ }
150
+ function formatNumber(value) {
151
+ return new Intl.NumberFormat(void 0).format(value);
152
+ }
153
+ function formatTime(ms) {
154
+ return new Intl.DateTimeFormat(void 0, {
155
+ dateStyle: "medium",
156
+ timeStyle: "short"
157
+ }).format(new Date(ms));
158
+ }
159
+ /** One billing package as a labeled progress bar. */
160
+ function CreditBar({ label, remain, size, t }) {
161
+ const detail = size > 0 ? t("exactRemaining", {
162
+ remain: formatNumber(remain),
163
+ size: formatNumber(size)
164
+ }) : t("creditPackageUnknownSize", { remain: formatNumber(remain) });
165
+ const percent = size > 0 ? remain / size * 100 : 100;
166
+ const display = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent);
167
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
168
+ style: quotaGroupStyle,
169
+ children: [
170
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
171
+ style: quotaLabelStyle,
172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("percentRemaining", { percent: display }) })]
173
+ }),
174
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
175
+ style: progressTrackStyle,
176
+ role: "progressbar",
177
+ "aria-label": label,
178
+ "aria-valuemin": 0,
179
+ "aria-valuemax": 100,
180
+ "aria-valuenow": percent,
181
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: progressFillStyle(percent) })
182
+ }),
183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
184
+ style: bodyStyle,
185
+ children: detail
186
+ })
187
+ ]
188
+ });
189
+ }
190
+ /** Render WorkBuddy sign-in state and credit as one expandable card. */
191
+ function WorkBuddyPluginCard({ t }) {
192
+ if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
193
+ const [open, setOpen] = (0, react.useState)(false);
194
+ const [status, setStatus] = (0, react.useState)({ status: "signed-out" });
195
+ const [busy, setBusy] = (0, react.useState)(false);
196
+ const mounted = (0, react.useRef)(true);
197
+ (0, react.useEffect)(() => {
198
+ mounted.current = true;
199
+ return () => {
200
+ mounted.current = false;
201
+ };
202
+ }, []);
203
+ const refresh = (0, react.useCallback)(async (signal) => {
204
+ try {
205
+ const response = await fetch(WORKBUDDY_STATUS_PATH, {
206
+ headers: { accept: "application/json" },
207
+ credentials: "same-origin",
208
+ ...signal === void 0 ? {} : { signal }
209
+ });
210
+ const value = await response.json().catch(() => void 0);
211
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
212
+ if (mounted.current && signal?.aborted !== true) setStatus(value);
213
+ } catch (error) {
214
+ if (mounted.current && signal?.aborted !== true) setStatus({
215
+ status: "error",
216
+ message: error instanceof Error ? error.message : t("requestFailed")
217
+ });
218
+ }
219
+ }, [t]);
220
+ (0, react.useEffect)(() => {
221
+ if (!open) return;
222
+ const controller = new AbortController();
223
+ refresh(controller.signal);
224
+ return () => {
225
+ controller.abort();
226
+ };
227
+ }, [open, refresh]);
228
+ (0, react.useEffect)(() => {
229
+ if (!open || status.status !== "signed-in") return;
230
+ const controller = new AbortController();
231
+ const timer = window.setInterval(() => {
232
+ refresh(controller.signal);
233
+ }, POLL_INTERVAL_MS);
234
+ return () => {
235
+ window.clearInterval(timer);
236
+ controller.abort();
237
+ };
238
+ }, [
239
+ open,
240
+ refresh,
241
+ status.status
242
+ ]);
243
+ const manualRefresh = async () => {
244
+ setBusy(true);
245
+ try {
246
+ await refresh();
247
+ } finally {
248
+ if (mounted.current) setBusy(false);
249
+ }
250
+ };
251
+ const title = t("title");
252
+ const label = status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).replace(/[::]\s*$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
253
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
254
+ style: cardStyle,
255
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
256
+ type: "button",
257
+ style: headerStyle,
258
+ "aria-expanded": open,
259
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
260
+ onClick: () => {
261
+ setOpen(!open);
262
+ },
263
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
264
+ style: headTextStyle,
265
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
266
+ style: nameStyle,
267
+ children: title
268
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
269
+ style: descriptionStyle,
270
+ children: t("intro")
271
+ })]
272
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
273
+ "aria-hidden": "true",
274
+ style: {
275
+ ...chevronStyle,
276
+ transform: open ? "rotate(180deg)" : "none"
277
+ },
278
+ children: "⌄"
279
+ })]
280
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
281
+ style: cardBodyStyle,
282
+ children: [
283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
284
+ style: quotaTitleStyle,
285
+ children: t("accountHeading")
286
+ }),
287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
288
+ style: rowStyle,
289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
290
+ style: statusStyle,
291
+ role: "status",
292
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
293
+ "aria-hidden": "true",
294
+ style: dotStyle(status.status)
295
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
296
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
297
+ type: "button",
298
+ style: buttonStyle,
299
+ disabled: busy,
300
+ onClick: () => {
301
+ manualRefresh();
302
+ },
303
+ children: busy ? t("refreshing") : t("refresh")
304
+ })]
305
+ }),
306
+ status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
307
+ status.expiresAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
308
+ style: bodyStyle,
309
+ children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
310
+ }),
311
+ status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
312
+ style: quotaListStyle,
313
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
314
+ style: rowStyle,
315
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
316
+ style: quotaTitleStyle,
317
+ children: t("creditsHeading")
318
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
319
+ style: bodyStyle,
320
+ children: t("creditsTotal", { total: formatNumber(status.credits.total) })
321
+ })]
322
+ }), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
323
+ label: account.packageName,
324
+ remain: account.remain,
325
+ size: account.size,
326
+ t
327
+ }, `${account.packageName}-${String(index)}`))]
328
+ }),
329
+ status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
330
+ style: errorStyle,
331
+ children: t("creditsError", { message: status.creditsError })
332
+ })
333
+ ] }) : null,
334
+ status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
335
+ style: bodyStyle,
336
+ children: t("signedOutHint")
337
+ }) : null,
338
+ status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
339
+ style: errorStyle,
340
+ children: status.message
341
+ }) : null
342
+ ]
343
+ }) : null]
344
+ });
345
+ }
346
+ //#endregion
347
+ //#region src/client/locales.ts
348
+ /** Plugin-card copy registered under the settings.workbuddy locale namespace. */
349
+ const en = {
350
+ title: "DSH WorkBuddy Connect",
351
+ intro: "Use the models in the WorkBuddy desktop app directly in DSH — zero configuration, ready out of the box.",
352
+ expand: "Expand",
353
+ collapse: "Collapse",
354
+ loading: "Loading account…",
355
+ signedOut: "Not signed in",
356
+ signedOutHint: "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
357
+ signedInAs: "Signed in as {nickname}",
358
+ accessTokenExpires: "Access token expires {time} (refresh is automatic)",
359
+ creditsHeading: "Remaining credit",
360
+ creditsTotal: "Total: {total}",
361
+ percentRemaining: "{percent}% remaining",
362
+ exactRemaining: "{remain} / {size} remaining",
363
+ creditPackageUnknownSize: "{remain} remaining",
364
+ creditsError: "Credit unavailable: {message}",
365
+ refresh: "Refresh",
366
+ refreshing: "Refreshing…",
367
+ requestFailed: "Request failed",
368
+ accountHeading: "Account"
369
+ };
370
+ const zh = {
371
+ title: "DSH WorkBuddy Connect",
372
+ intro: "在 DSH 中直接使用 WorkBuddy 桌面 App 包含的模型,开箱即用,无需额外配置。",
373
+ expand: "展开",
374
+ collapse: "收起",
375
+ loading: "正在读取账号…",
376
+ signedOut: "未登录",
377
+ signedOutHint: "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
378
+ signedInAs: "已登录:{nickname}",
379
+ accessTokenExpires: "访问令牌 {time} 过期(自动续期)",
380
+ creditsHeading: "剩余积分",
381
+ creditsTotal: "合计:{total}",
382
+ percentRemaining: "剩余 {percent}%",
383
+ exactRemaining: "剩余 {remain} / {size}",
384
+ creditPackageUnknownSize: "剩余 {remain}",
385
+ creditsError: "积分查询失败:{message}",
386
+ refresh: "刷新",
387
+ refreshing: "正在刷新…",
388
+ requestFailed: "请求失败",
389
+ accountHeading: "账号"
390
+ };
391
+ //#endregion
392
+ //#region src/client/index.tsx
393
+ /** Stable browser-plugin name. */
394
+ const name = "dsh-workbuddy-connect-client";
395
+ /** Client services required by the Plugin configuration contribution. */
396
+ const inject = ["slots", "locale"];
397
+ /**
398
+ * Register card copy and the WorkBuddy card under Plugin configuration.
399
+ *
400
+ * The entire body is wrapped so that a DSH slot-API breaking change (for
401
+ * example the rc.6→rc.7 `id`→`key` / `order`→`priority` rename) degrades
402
+ * to a `console.error` instead of throwing into the DSH loader and raising
403
+ * the red "Failed to load plugins" banner. The host provider keeps working:
404
+ * the `workbuddy` model channel is unaffected, and `dsh-workbuddy-connect
405
+ * status` reports host health via the heartbeat file.
406
+ *
407
+ * NOTE: the try/catch boundary of this function is mirrored (duplicated) in
408
+ * `tests/client-fallback.spec.ts`, because the real client entry imports
409
+ * browser-only DSH packages that cannot load in the Node test environment.
410
+ * That test therefore does not import this function — it replicates its
411
+ * shape. If you change the guarded body or the `console.error` message here,
412
+ * update the mirrored `apply()` in that spec too, or the fallback test will
413
+ * silently diverge from this real implementation.
414
+ */
415
+ function apply(ctx) {
416
+ try {
417
+ const namespace = "settings.workbuddy";
418
+ ctx.effect(() => ctx.locale.register(namespace, {
419
+ zh,
420
+ en
421
+ }), "dsh-workbuddy-connect: settings copy");
422
+ const t = ctx.locale.bind(namespace);
423
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
424
+ name: "settings.plugin.item",
425
+ key: "workbuddy",
426
+ priority: 30,
427
+ inject: () => ({ t })
428
+ }, WorkBuddyPluginCard));
429
+ } catch (error) {
430
+ console.error("[dsh-workbuddy-connect] client card failed to load (host provider unaffected):", error);
431
+ }
432
+ }
433
+ //#endregion
434
+ exports.apply = apply;
435
+ exports.inject = inject;
436
+ exports.name = name;
437
+ return module.exports;
438
+ }
439
+ });