dsh-workbuddy-connect 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +41 -5
- package/README.md +40 -5
- package/lib/bin.js +24 -8
- package/lib/client.js +537 -102
- package/lib/index.d.ts +399 -32
- package/lib/index.js +332 -76
- package/lib/{variants-XUStsapF.js → variants-Cdp_ii6z.js} +893 -113
- package/package.json +23 -23
package/lib/client.js
CHANGED
|
@@ -30,6 +30,21 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
*/
|
|
31
31
|
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
32
32
|
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
33
|
+
/** Every reason code, for validation without trusting a wire value. */
|
|
34
|
+
const SIGNED_OUT_REASON_CODES = [
|
|
35
|
+
"no-credential",
|
|
36
|
+
"credential-region-mismatch",
|
|
37
|
+
"encrypted-credential-unreadable",
|
|
38
|
+
"electron-binary-not-found",
|
|
39
|
+
"electron-binary-ambiguous",
|
|
40
|
+
"electron-binary-unavailable",
|
|
41
|
+
"electron-path-invalid",
|
|
42
|
+
"electron-discovery-incomplete"
|
|
43
|
+
];
|
|
44
|
+
/** Whether a value is one of the closed set of signed-out reason codes. */
|
|
45
|
+
function isWorkBuddySignedOutReasonCode(value) {
|
|
46
|
+
return typeof value === "string" && SIGNED_OUT_REASON_CODES.includes(value);
|
|
47
|
+
}
|
|
33
48
|
//#endregion
|
|
34
49
|
//#region src/client/status-document.ts
|
|
35
50
|
/**
|
|
@@ -43,6 +58,11 @@ window.__ModuleLoader__.load({
|
|
|
43
58
|
* The check is deliberately limited to the discriminator (plus `error`'s
|
|
44
59
|
* `message`, which the error paragraph renders): validating optional fields
|
|
45
60
|
* here would reject documents the host legitimately omits fields from.
|
|
61
|
+
*
|
|
62
|
+
* `reasonCode` is therefore *not* rejected here — a card renders `reason`
|
|
63
|
+
* either way — but every reader must narrow it with
|
|
64
|
+
* `isWorkBuddySignedOutReasonCode` before branching on it, since the wire
|
|
65
|
+
* value is not guaranteed to be inside the enum.
|
|
46
66
|
*/
|
|
47
67
|
function isWorkBuddyWebStatus(value) {
|
|
48
68
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -53,7 +73,13 @@ window.__ModuleLoader__.load({
|
|
|
53
73
|
}
|
|
54
74
|
//#endregion
|
|
55
75
|
//#region src/client/WorkBuddyPluginCard.tsx
|
|
56
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* WorkBuddy status card, rendered on whichever settings surface the host
|
|
78
|
+
* provides: dispatched directly by DSH 0.1.5's settings Plugins tab (one card
|
|
79
|
+
* per variant), or mounted by the bundle configuration page DSH 0.1.6+'s
|
|
80
|
+
* Plugins page renders. The component itself is surface-agnostic — its props
|
|
81
|
+
* are only the injected copy and variant.
|
|
82
|
+
*/
|
|
57
83
|
/** CN WorkBuddy; the plugin's long-standing card and default. */
|
|
58
84
|
const CN_CARD_VARIANT = {
|
|
59
85
|
id: "workbuddy",
|
|
@@ -61,7 +87,9 @@ window.__ModuleLoader__.load({
|
|
|
61
87
|
introKey: "intro",
|
|
62
88
|
signedOutKey: "signedOutHint",
|
|
63
89
|
statusPath: WORKBUDDY_STATUS_PATH,
|
|
64
|
-
probePath: WORKBUDDY_PROBE_PATH
|
|
90
|
+
probePath: WORKBUDDY_PROBE_PATH,
|
|
91
|
+
appName: "WorkBuddy",
|
|
92
|
+
unavailableKey: "assistUnavailableCN"
|
|
65
93
|
};
|
|
66
94
|
/** International WorkBuddy AI. */
|
|
67
95
|
const AI_CARD_VARIANT = {
|
|
@@ -70,16 +98,125 @@ window.__ModuleLoader__.load({
|
|
|
70
98
|
introKey: "introAI",
|
|
71
99
|
signedOutKey: "signedOutHintAI",
|
|
72
100
|
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
73
|
-
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
101
|
+
probePath: WORKBUDDY_AI_PROBE_PATH,
|
|
102
|
+
appName: "WorkBuddy AI",
|
|
103
|
+
unavailableKey: "assistUnavailableAI"
|
|
74
104
|
};
|
|
75
105
|
/** Both cards, in display order. */
|
|
76
106
|
const CARD_VARIANTS = [CN_CARD_VARIANT, AI_CARD_VARIANT];
|
|
77
107
|
const POLL_INTERVAL_MS = 6e4;
|
|
108
|
+
/**
|
|
109
|
+
* The reason codes whose failures the Agent assist block covers: the plugin
|
|
110
|
+
* cannot reach a decryption program, for any of the five reasons in §5.5.
|
|
111
|
+
*
|
|
112
|
+
* This is the *only* place the card decides whether the block applies. It
|
|
113
|
+
* branches on the code, never on `reason` text: the prose is written for a
|
|
114
|
+
* human and is expected to change, so matching it would silently stop
|
|
115
|
+
* matching after any wording edit.
|
|
116
|
+
*
|
|
117
|
+
* `encrypted-credential-unreadable` is deliberately absent — the app was found
|
|
118
|
+
* and ran, so "look for the app" is not the fix for it.
|
|
119
|
+
*/
|
|
120
|
+
const ASSIST_REASON_CODES = [
|
|
121
|
+
"electron-binary-not-found",
|
|
122
|
+
"electron-binary-ambiguous",
|
|
123
|
+
"electron-binary-unavailable",
|
|
124
|
+
"electron-path-invalid",
|
|
125
|
+
"electron-discovery-incomplete"
|
|
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
|
+
/** Copy text to the clipboard, reporting whether it worked. */
|
|
138
|
+
async function copyPrompt(text) {
|
|
139
|
+
try {
|
|
140
|
+
if (navigator.clipboard?.writeText === void 0) return false;
|
|
141
|
+
await navigator.clipboard.writeText(text);
|
|
142
|
+
return true;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The Agent assist block for a path failure: what is wrong, one copyable
|
|
149
|
+
* request, and a re-check. Rendered only for the codes above.
|
|
150
|
+
*/
|
|
151
|
+
function AssistBlock({ t, variant, code, busy, onRecheck }) {
|
|
152
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
153
|
+
const [copyFailed, setCopyFailed] = (0, react.useState)(false);
|
|
154
|
+
const prompt = t("assistantPrompt", {
|
|
155
|
+
appName: variant.appName,
|
|
156
|
+
failureSummary: t(assistSummaryKey(code, variant))
|
|
157
|
+
});
|
|
158
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
159
|
+
style: assistStyle,
|
|
160
|
+
children: [
|
|
161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
162
|
+
style: assistHeadingStyle,
|
|
163
|
+
children: t("assistantHeading")
|
|
164
|
+
}),
|
|
165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
166
|
+
style: bodyStyle,
|
|
167
|
+
children: t("assistantIntro")
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
170
|
+
style: assistPromptRowStyle,
|
|
171
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
172
|
+
style: assistPromptStyle,
|
|
173
|
+
children: prompt
|
|
174
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
175
|
+
type: "button",
|
|
176
|
+
style: buttonStyle$1,
|
|
177
|
+
onClick: () => {
|
|
178
|
+
(async () => {
|
|
179
|
+
const ok = await copyPrompt(prompt);
|
|
180
|
+
setCopied(ok);
|
|
181
|
+
setCopyFailed(!ok);
|
|
182
|
+
})();
|
|
183
|
+
},
|
|
184
|
+
children: t("assistantCopy")
|
|
185
|
+
})]
|
|
186
|
+
}),
|
|
187
|
+
copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
188
|
+
style: assistFeedbackStyle,
|
|
189
|
+
role: "status",
|
|
190
|
+
children: t("assistantCopied")
|
|
191
|
+
}) : null,
|
|
192
|
+
copyFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
193
|
+
style: assistFeedbackStyle,
|
|
194
|
+
role: "status",
|
|
195
|
+
children: t("assistantCopyFailed")
|
|
196
|
+
}) : null,
|
|
197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
198
|
+
style: bodyStyle,
|
|
199
|
+
children: t("assistantAfter")
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
202
|
+
style: rowStyle,
|
|
203
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
204
|
+
type: "button",
|
|
205
|
+
style: buttonStyle$1,
|
|
206
|
+
disabled: busy,
|
|
207
|
+
onClick: onRecheck,
|
|
208
|
+
children: busy ? t("assistantRechecking") : t("assistantRecheck")
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
}
|
|
78
214
|
const cardStyle = {
|
|
79
215
|
overflow: "hidden",
|
|
80
216
|
border: "1px solid var(--dsw-alias-border-l2)",
|
|
81
217
|
borderRadius: 10,
|
|
82
|
-
background: "var(--dsw-alias-bg-module-platform)"
|
|
218
|
+
background: "var(--dsw-alias-bg-module-platform)",
|
|
219
|
+
listStyle: "none"
|
|
83
220
|
};
|
|
84
221
|
const headerStyle = {
|
|
85
222
|
boxSizing: "border-box",
|
|
@@ -217,6 +354,64 @@ window.__ModuleLoader__.load({
|
|
|
217
354
|
flexDirection: "column",
|
|
218
355
|
gap: 2
|
|
219
356
|
};
|
|
357
|
+
/**
|
|
358
|
+
* The Agent assist block. Sits beside the existing error line rather than
|
|
359
|
+
* replacing it: the short diagnosis stays the headline, this adds the way out.
|
|
360
|
+
*/
|
|
361
|
+
const assistStyle = {
|
|
362
|
+
display: "flex",
|
|
363
|
+
flexDirection: "column",
|
|
364
|
+
gap: 10,
|
|
365
|
+
padding: "12px 14px",
|
|
366
|
+
marginTop: 12,
|
|
367
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
368
|
+
borderRadius: 10,
|
|
369
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04))"
|
|
370
|
+
};
|
|
371
|
+
const assistHeadingStyle = {
|
|
372
|
+
margin: 0,
|
|
373
|
+
fontSize: 14,
|
|
374
|
+
lineHeight: "20px",
|
|
375
|
+
fontWeight: 600,
|
|
376
|
+
color: "var(--dsw-alias-label-primary)"
|
|
377
|
+
};
|
|
378
|
+
const assistPromptRowStyle = {
|
|
379
|
+
display: "flex",
|
|
380
|
+
flexWrap: "wrap",
|
|
381
|
+
alignItems: "flex-start",
|
|
382
|
+
gap: 8,
|
|
383
|
+
padding: "8px 9px 8px 11px",
|
|
384
|
+
borderRadius: 7,
|
|
385
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
386
|
+
};
|
|
387
|
+
const assistPromptStyle = {
|
|
388
|
+
flex: "1 1 220px",
|
|
389
|
+
minWidth: 0,
|
|
390
|
+
margin: 0,
|
|
391
|
+
color: "var(--dsw-alias-label-primary)",
|
|
392
|
+
fontSize: 12,
|
|
393
|
+
lineHeight: "19px",
|
|
394
|
+
whiteSpace: "pre-wrap",
|
|
395
|
+
overflowWrap: "anywhere",
|
|
396
|
+
userSelect: "text"
|
|
397
|
+
};
|
|
398
|
+
const assistFeedbackStyle = {
|
|
399
|
+
margin: 0,
|
|
400
|
+
fontSize: 12,
|
|
401
|
+
lineHeight: "19px",
|
|
402
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* Left half of one merged context/visibility row: the visibility checkbox (when
|
|
406
|
+
* the account has one) beside the model's name and rate. Kept as a flex span so
|
|
407
|
+
* the capacity column stays flush right no matter how long the name runs.
|
|
408
|
+
*/
|
|
409
|
+
const contextRowMainStyle = {
|
|
410
|
+
display: "flex",
|
|
411
|
+
alignItems: "center",
|
|
412
|
+
gap: 9,
|
|
413
|
+
minWidth: 0
|
|
414
|
+
};
|
|
220
415
|
const modelBadgeChipStyle = {
|
|
221
416
|
padding: "1px 8px",
|
|
222
417
|
borderRadius: 999,
|
|
@@ -455,26 +650,41 @@ window.__ModuleLoader__.load({
|
|
|
455
650
|
});
|
|
456
651
|
}
|
|
457
652
|
/**
|
|
458
|
-
* Context
|
|
653
|
+
* Context window and model visibility, one row per catalog model.
|
|
654
|
+
*
|
|
655
|
+
* The list is driven by the full current catalog, not by context metadata:
|
|
656
|
+
* hiding a model is a statement about the picker, and a model without a
|
|
657
|
+
* declared window is still hideable — its row just shows an em dash where the
|
|
658
|
+
* capacity would be. Rows with a window keep the original ordering (largest
|
|
659
|
+
* first); rows without one trail at the end in catalog order.
|
|
459
660
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
661
|
+
* Each row is one <label>, so the checkbox is named by its row without a
|
|
662
|
+
* duplicated aria string. The checkbox state comes from the status document
|
|
663
|
+
* only — no optimistic flip — so a save that fails leaves the box where the
|
|
664
|
+
* host's truth says it is, next to the failure notice `control` records.
|
|
665
|
+
* Checkboxes render only when the document carries a visibility section (a
|
|
666
|
+
* signed-in account with a stable user id); a uid-less credential shows the
|
|
667
|
+
* plain capacity list rather than editing a bucket every such account would
|
|
668
|
+
* share.
|
|
464
669
|
*
|
|
465
|
-
* Purely a report of the upstream's own numbers. The plugin offers
|
|
466
|
-
* picker: the CN catalog declares one capacity per model and publishes
|
|
467
|
-
* alternatives, so a menu there would mean inventing client-side policy.
|
|
468
|
-
* international document does declare alternatives (`supportedLengths`),
|
|
469
|
-
* they are shown as a secondary figure rather than merged into one number —
|
|
670
|
+
* Purely a report of the upstream's own numbers otherwise. The plugin offers
|
|
671
|
+
* no tier picker: the CN catalog declares one capacity per model and publishes
|
|
672
|
+
* no alternatives, so a menu there would mean inventing client-side policy.
|
|
673
|
+
* The international document does declare alternatives (`supportedLengths`),
|
|
674
|
+
* and they are shown as a secondary figure rather than merged into one number —
|
|
470
675
|
* the default is the budget actually requested, while the larger value is a
|
|
471
676
|
* ceiling the upstream would accept.
|
|
472
677
|
*/
|
|
473
|
-
function ContextTable({ models, t, useMaximumContextWindow,
|
|
474
|
-
const
|
|
475
|
-
|
|
678
|
+
function ContextTable({ models, t, useMaximumContextWindow, contextPreferenceDisabled, onUseMaximumContextWindow, visibility, visibilityControlsDisabled, visibilityToggling, onVisibilityToggle }) {
|
|
679
|
+
const rows = [...models ?? []].sort((a, b) => {
|
|
680
|
+
if (a.contextWindow === void 0) return b.contextWindow === void 0 ? 0 : 1;
|
|
681
|
+
if (b.contextWindow === void 0) return -1;
|
|
682
|
+
return b.contextWindow - a.contextWindow;
|
|
683
|
+
});
|
|
684
|
+
const canSelectMaximum = rows.some((model) => model.maxContextWindow !== void 0 && model.maxContextWindow > (model.defaultContextWindow ?? model.contextWindow ?? 0));
|
|
476
685
|
const showPreference = onUseMaximumContextWindow !== void 0 && (canSelectMaximum || useMaximumContextWindow === true);
|
|
477
|
-
if (
|
|
686
|
+
if (rows.length === 0 && !showPreference) return null;
|
|
687
|
+
const hidden = new Set(visibility?.disabled ?? []);
|
|
478
688
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
479
689
|
style: quotaListStyle,
|
|
480
690
|
children: [
|
|
@@ -487,7 +697,7 @@ window.__ModuleLoader__.load({
|
|
|
487
697
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
488
698
|
type: "checkbox",
|
|
489
699
|
checked: useMaximumContextWindow === true,
|
|
490
|
-
disabled,
|
|
700
|
+
disabled: contextPreferenceDisabled,
|
|
491
701
|
onChange: (event) => {
|
|
492
702
|
onUseMaximumContextWindow(event.currentTarget.checked);
|
|
493
703
|
}
|
|
@@ -499,25 +709,65 @@ window.__ModuleLoader__.load({
|
|
|
499
709
|
})]
|
|
500
710
|
})]
|
|
501
711
|
}) : null,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
712
|
+
visibility === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
713
|
+
style: bodyStyle,
|
|
714
|
+
children: t("visibilityIntro")
|
|
715
|
+
}),
|
|
716
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
717
|
+
style: quotaGroupStyle,
|
|
718
|
+
children: rows.map((model) => {
|
|
719
|
+
const capacity = model.contextWindow;
|
|
720
|
+
const alternative = capacity !== void 0 && model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
|
|
721
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
722
|
+
style: quotaLabelStyle,
|
|
723
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
724
|
+
style: contextRowMainStyle,
|
|
725
|
+
children: [visibility === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
726
|
+
type: "checkbox",
|
|
727
|
+
checked: !hidden.has(model.id),
|
|
728
|
+
disabled: visibilityControlsDisabled || visibilityToggling.has(model.id),
|
|
729
|
+
onChange: (event) => {
|
|
730
|
+
onVisibilityToggle?.(model.id, event.currentTarget.checked);
|
|
731
|
+
}
|
|
732
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
733
|
+
style: modelOfferStyle,
|
|
734
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
735
|
+
style: modelBadgeStyle,
|
|
736
|
+
children: [
|
|
737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }),
|
|
738
|
+
model.badges?.map((badge) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
739
|
+
style: modelBadgeChipStyle,
|
|
740
|
+
children: modelBadgeLabel(badge, t)
|
|
741
|
+
}, badge)),
|
|
742
|
+
model.free === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
743
|
+
style: modelBadgeChipStyle,
|
|
744
|
+
children: t("freeModel")
|
|
745
|
+
}) : null
|
|
746
|
+
]
|
|
747
|
+
}), model.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
748
|
+
style: modelRateStyle,
|
|
749
|
+
children: model.credits
|
|
750
|
+
})]
|
|
751
|
+
})]
|
|
752
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
753
|
+
style: modelOfferStyle,
|
|
754
|
+
children: [capacity === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
755
|
+
style: modelRateStyle,
|
|
756
|
+
"aria-label": t("contextUnknown"),
|
|
757
|
+
children: "—"
|
|
758
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
759
|
+
style: { textAlign: "right" },
|
|
760
|
+
children: formatTokens(capacity)
|
|
761
|
+
}), alternative !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
762
|
+
style: modelRateStyle,
|
|
763
|
+
children: t("contextUpTo", { size: formatTokens(alternative) })
|
|
764
|
+
}) : capacity !== void 0 && model.defaultContextWindow !== void 0 && model.defaultContextWindow < capacity ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
765
|
+
style: modelRateStyle,
|
|
766
|
+
children: t("contextDefault", { size: formatTokens(model.defaultContextWindow) })
|
|
767
|
+
}) : null]
|
|
768
|
+
})]
|
|
769
|
+
}, model.id);
|
|
770
|
+
})
|
|
521
771
|
})
|
|
522
772
|
]
|
|
523
773
|
});
|
|
@@ -543,7 +793,7 @@ window.__ModuleLoader__.load({
|
|
|
543
793
|
* parameter ("this model does not check it"), never as a statement that a
|
|
544
794
|
* level is unsupported.
|
|
545
795
|
*/
|
|
546
|
-
function ProbeSection({ probe, t, onDetect, onClear, busy }) {
|
|
796
|
+
function ProbeSection({ probe, models, t, onDetect, onClear, busy }) {
|
|
547
797
|
const [pending, setPending] = (0, react.useState)();
|
|
548
798
|
const [runningModel, setRunningModel] = (0, react.useState)();
|
|
549
799
|
(0, react.useEffect)(() => {
|
|
@@ -590,7 +840,7 @@ window.__ModuleLoader__.load({
|
|
|
590
840
|
style: quotaGroupStyle,
|
|
591
841
|
children: probe.candidates.map((id) => {
|
|
592
842
|
const result = probe.results.find((entry) => entry.id === id);
|
|
593
|
-
const name = result?.name ?? id;
|
|
843
|
+
const name = models?.find((model) => model.id === id)?.name ?? result?.name ?? id;
|
|
594
844
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
595
845
|
style: modelOfferStyle,
|
|
596
846
|
children: [
|
|
@@ -608,7 +858,7 @@ window.__ModuleLoader__.load({
|
|
|
608
858
|
onClick: () => {
|
|
609
859
|
setPending(id);
|
|
610
860
|
},
|
|
611
|
-
children: runningModel === id ? t("probeRunning", { model:
|
|
861
|
+
children: runningModel === id ? t("probeRunning", { model: name }) : t(result === void 0 ? "probeStart" : "probeRedetect")
|
|
612
862
|
})]
|
|
613
863
|
})]
|
|
614
864
|
}),
|
|
@@ -688,6 +938,17 @@ window.__ModuleLoader__.load({
|
|
|
688
938
|
*/
|
|
689
939
|
const [readFailure, setReadFailure] = (0, react.useState)();
|
|
690
940
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
941
|
+
/**
|
|
942
|
+
* The model ids whose visibility writes are in flight, empty when none are.
|
|
943
|
+
* Deliberately NOT the card-wide `busy` (that flag drives the Refresh
|
|
944
|
+
* buttons' labels, which must not claim a refresh the user never pressed)
|
|
945
|
+
* and deliberately per-row rather than whole-list: a visibility write only
|
|
946
|
+
* adds or removes one model's id, so the rows are independent — locking
|
|
947
|
+
* every checkbox for one row's write made the whole list visibly blink for
|
|
948
|
+
* no correctness gain. A set, because two writes can be open at once when
|
|
949
|
+
* the user moves down the list; only the rows being written lock.
|
|
950
|
+
*/
|
|
951
|
+
const [togglingModels, setTogglingModels] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
691
952
|
const [tab, setTab] = (0, react.useState)("status");
|
|
692
953
|
const mounted = (0, react.useRef)(true);
|
|
693
954
|
/**
|
|
@@ -843,14 +1104,16 @@ window.__ModuleLoader__.load({
|
|
|
843
1104
|
const control = (0, react.useCallback)(async (action) => {
|
|
844
1105
|
const key = status?.status === "signed-in" ? status.probeKey : void 0;
|
|
845
1106
|
if (key === void 0) return;
|
|
846
|
-
|
|
1107
|
+
const visibility = action.action === "set-model-visibility";
|
|
1108
|
+
if (visibility) setTogglingModels((previous) => new Set(previous).add(action.model));
|
|
1109
|
+
else setBusy(true);
|
|
847
1110
|
const controller = trackController();
|
|
848
1111
|
try {
|
|
849
1112
|
const response = await fetch(variant.probePath, {
|
|
850
1113
|
method: "POST",
|
|
851
1114
|
headers: {
|
|
852
1115
|
"Content-Type": "application/json",
|
|
853
|
-
"X-
|
|
1116
|
+
"X-Workbuddy-Probe-Key": key
|
|
854
1117
|
},
|
|
855
1118
|
credentials: "same-origin",
|
|
856
1119
|
signal: controller.signal,
|
|
@@ -861,7 +1124,12 @@ window.__ModuleLoader__.load({
|
|
|
861
1124
|
const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
|
|
862
1125
|
throw new Error(message);
|
|
863
1126
|
}
|
|
864
|
-
if (action.action === "set-maximum-context-window" && (typeof value !== "object" || value === null || value["state"] !== "updated")) {
|
|
1127
|
+
if ((action.action === "set-maximum-context-window" || action.action === "set-model-visibility") && (typeof value !== "object" || value === null || value["state"] !== "updated")) {
|
|
1128
|
+
const state = typeof value === "object" && value !== null ? value["state"] : void 0;
|
|
1129
|
+
if (action.action === "set-model-visibility" && state === "stale-account") {
|
|
1130
|
+
await refresh(controller.signal);
|
|
1131
|
+
throw new Error(t("visibilityStaleAccount"));
|
|
1132
|
+
}
|
|
865
1133
|
const reason = typeof value === "object" && value !== null && "reason" in value ? String(value["reason"]) : t("requestFailed");
|
|
866
1134
|
throw new Error(reason);
|
|
867
1135
|
}
|
|
@@ -870,7 +1138,13 @@ window.__ModuleLoader__.load({
|
|
|
870
1138
|
if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
|
|
871
1139
|
} finally {
|
|
872
1140
|
manualControllers.current.delete(controller);
|
|
873
|
-
if (mounted.current)
|
|
1141
|
+
if (!mounted.current) return;
|
|
1142
|
+
if (visibility) setTogglingModels((previous) => {
|
|
1143
|
+
const next = new Set(previous);
|
|
1144
|
+
next.delete(action.model);
|
|
1145
|
+
return next;
|
|
1146
|
+
});
|
|
1147
|
+
else setBusy(false);
|
|
874
1148
|
}
|
|
875
1149
|
}, [
|
|
876
1150
|
refresh,
|
|
@@ -890,6 +1164,13 @@ window.__ModuleLoader__.load({
|
|
|
890
1164
|
});
|
|
891
1165
|
}, [control]);
|
|
892
1166
|
const title = t(variant.titleKey);
|
|
1167
|
+
/**
|
|
1168
|
+
* The failure the assist block covers, when this document has one. Computed
|
|
1169
|
+
* once so the block and the header's refresh button agree on whether the
|
|
1170
|
+
* block owns the re-check action — showing both would put two buttons with
|
|
1171
|
+
* the same effect side by side.
|
|
1172
|
+
*/
|
|
1173
|
+
const assistCode = status?.status === "signed-out" && isWorkBuddySignedOutReasonCode(status.reasonCode) && ASSIST_REASON_CODES.includes(status.reasonCode) ? status.reasonCode : void 0;
|
|
893
1174
|
const label = status === void 0 ? t("loading") : status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).trimEnd().replace(/[::]$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
|
|
894
1175
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
895
1176
|
style: cardStyle,
|
|
@@ -935,7 +1216,7 @@ window.__ModuleLoader__.load({
|
|
|
935
1216
|
"aria-hidden": "true",
|
|
936
1217
|
style: dotStyle(status === void 0 ? "loading" : status.status)
|
|
937
1218
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
938
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1219
|
+
}), assistCode === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
939
1220
|
type: "button",
|
|
940
1221
|
style: buttonStyle$1,
|
|
941
1222
|
disabled: busy,
|
|
@@ -943,7 +1224,7 @@ window.__ModuleLoader__.load({
|
|
|
943
1224
|
manualRefresh();
|
|
944
1225
|
},
|
|
945
1226
|
children: busy ? t("refreshing") : t("refresh")
|
|
946
|
-
})]
|
|
1227
|
+
}) : null]
|
|
947
1228
|
}),
|
|
948
1229
|
readFailure === void 0 || signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
949
1230
|
style: errorStyle,
|
|
@@ -1019,6 +1300,7 @@ window.__ModuleLoader__.load({
|
|
|
1019
1300
|
}),
|
|
1020
1301
|
status.probe === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProbeSection, {
|
|
1021
1302
|
probe: status.probe,
|
|
1303
|
+
models: status.models,
|
|
1022
1304
|
t,
|
|
1023
1305
|
busy,
|
|
1024
1306
|
onDetect: confirmDetect,
|
|
@@ -1032,14 +1314,27 @@ window.__ModuleLoader__.load({
|
|
|
1032
1314
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
|
|
1033
1315
|
models: status.models,
|
|
1034
1316
|
t,
|
|
1035
|
-
|
|
1036
|
-
...status.useMaximumContextWindow
|
|
1037
|
-
|
|
1317
|
+
contextPreferenceDisabled: busy,
|
|
1318
|
+
...variant.id === AI_CARD_VARIANT.id && status.useMaximumContextWindow !== void 0 ? {
|
|
1319
|
+
useMaximumContextWindow: status.useMaximumContextWindow,
|
|
1320
|
+
onUseMaximumContextWindow: (enabled) => {
|
|
1321
|
+
control({
|
|
1322
|
+
action: "set-maximum-context-window",
|
|
1323
|
+
enabled
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
} : {},
|
|
1327
|
+
visibility: status.visibility,
|
|
1328
|
+
visibilityControlsDisabled: busy,
|
|
1329
|
+
visibilityToggling: togglingModels,
|
|
1330
|
+
onVisibilityToggle: (modelId, visible) => {
|
|
1038
1331
|
control({
|
|
1039
|
-
action: "set-
|
|
1040
|
-
|
|
1332
|
+
action: "set-model-visibility",
|
|
1333
|
+
model: modelId,
|
|
1334
|
+
visible,
|
|
1335
|
+
account: status.visibility?.account ?? ""
|
|
1041
1336
|
});
|
|
1042
|
-
}
|
|
1337
|
+
}
|
|
1043
1338
|
})
|
|
1044
1339
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1045
1340
|
style: tabPanelStyle,
|
|
@@ -1067,10 +1362,18 @@ window.__ModuleLoader__.load({
|
|
|
1067
1362
|
})]
|
|
1068
1363
|
})
|
|
1069
1364
|
] }) : null,
|
|
1070
|
-
status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1365
|
+
status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1071
1366
|
style: status.reason === void 0 ? bodyStyle : errorStyle,
|
|
1072
1367
|
children: status.reason ?? t(variant.signedOutKey)
|
|
1073
|
-
}) :
|
|
1368
|
+
}), assistCode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistBlock, {
|
|
1369
|
+
t,
|
|
1370
|
+
variant,
|
|
1371
|
+
code: assistCode,
|
|
1372
|
+
busy,
|
|
1373
|
+
onRecheck: () => {
|
|
1374
|
+
manualRefresh();
|
|
1375
|
+
}
|
|
1376
|
+
})] }) : null,
|
|
1074
1377
|
status?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1075
1378
|
style: errorStyle,
|
|
1076
1379
|
children: status.message
|
|
@@ -1510,6 +1813,44 @@ window.__ModuleLoader__.load({
|
|
|
1510
1813
|
return t("probeNoteUnknown");
|
|
1511
1814
|
}
|
|
1512
1815
|
//#endregion
|
|
1816
|
+
//#region src/client/WorkBuddyConfigPage.tsx
|
|
1817
|
+
/**
|
|
1818
|
+
* The cards' list; the page supplies no other chrome. A semantic `<ul>` —
|
|
1819
|
+
* each card below is an `<li>` — with the user-agent list defaults cleared so
|
|
1820
|
+
* only the column/gap rhythm remains, keeping the layout identical to the
|
|
1821
|
+
* flex column it replaced while giving the page real list semantics.
|
|
1822
|
+
*/
|
|
1823
|
+
const pageStyle = {
|
|
1824
|
+
display: "flex",
|
|
1825
|
+
flexDirection: "column",
|
|
1826
|
+
gap: 12,
|
|
1827
|
+
listStyle: "none",
|
|
1828
|
+
margin: 0,
|
|
1829
|
+
padding: 0
|
|
1830
|
+
};
|
|
1831
|
+
/**
|
|
1832
|
+
* Render the bundle's configuration: one status card per WorkBuddy variant.
|
|
1833
|
+
*
|
|
1834
|
+
* Both variants share one page. DSH 0.1.6 replaced the settings section's
|
|
1835
|
+
* keyed `settings.plugin.item` slot — dispatched once per served settings
|
|
1836
|
+
* namespace, which is why the two variants used to be two cards — with the
|
|
1837
|
+
* Plugins page's `plugins.bundle.config`, keyed by the bundle's package name.
|
|
1838
|
+
* A bundle therefore carries exactly one configuration entry, so the cards are
|
|
1839
|
+
* stacked here instead of being dispatched separately. (The 0.1.5 settings tab
|
|
1840
|
+
* and its two dispatched cards still exist on 0.1.5 hosts; this page only
|
|
1841
|
+
* mounts where the Plugins page declares its slot.)
|
|
1842
|
+
*/
|
|
1843
|
+
function WorkBuddyConfigPage({ view, t }) {
|
|
1844
|
+
if (view === "summary") return t("intro");
|
|
1845
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1846
|
+
style: pageStyle,
|
|
1847
|
+
children: CARD_VARIANTS.map((variant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkBuddyPluginCard, {
|
|
1848
|
+
t,
|
|
1849
|
+
variant
|
|
1850
|
+
}, variant.id))
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
//#endregion
|
|
1513
1854
|
//#region src/client/locales.ts
|
|
1514
1855
|
/** Plugin-card copy registered under the settings.workbuddy locale namespace. */
|
|
1515
1856
|
const en = {
|
|
@@ -1557,8 +1898,11 @@ window.__ModuleLoader__.load({
|
|
|
1557
1898
|
contextHeading: "Context window",
|
|
1558
1899
|
contextUpTo: "up to {size}",
|
|
1559
1900
|
contextDefault: "default {size}",
|
|
1901
|
+
contextUnknown: "no declared context window",
|
|
1560
1902
|
useMaximumContextWindow: "Use the largest declared context window",
|
|
1561
1903
|
useMaximumContextWindowHint: "Applies to WorkBuddy AI models that offer a larger window.",
|
|
1904
|
+
visibilityIntro: "Uncheck a model to hide it from the model picker. Saved per signed-in account; chats already using a hidden model keep working.",
|
|
1905
|
+
visibilityStaleAccount: "The signed-in account changed — this change was not saved.",
|
|
1562
1906
|
freeModel: "Free",
|
|
1563
1907
|
badgeLimitedFree: "Limited-time free",
|
|
1564
1908
|
badgeNightDiscount: "Night discount",
|
|
@@ -1593,7 +1937,22 @@ window.__ModuleLoader__.load({
|
|
|
1593
1937
|
probeResultUnknown: "Detection did not complete",
|
|
1594
1938
|
probeResultAt: "Detected {time}",
|
|
1595
1939
|
probeResultEmpty: "No detectable models right now.",
|
|
1596
|
-
probeFailed: "Detection failed: {message}"
|
|
1940
|
+
probeFailed: "Detection failed: {message}",
|
|
1941
|
+
assistantHeading: "Let an Agent sort this out",
|
|
1942
|
+
assistantIntro: "Send the request below to your Agent; it will check the app location and the launch configuration for you.",
|
|
1943
|
+
assistantCopy: "Copy for Agent",
|
|
1944
|
+
assistantCopied: "Copied",
|
|
1945
|
+
assistantCopyFailed: "Copy failed — select the text above and copy it manually",
|
|
1946
|
+
assistantAfter: "When your Agent is done, come back and check again. If the DSH launch environment was changed, restart DSH first as instructed.",
|
|
1947
|
+
assistantRecheck: "Done — check again",
|
|
1948
|
+
assistantRechecking: "Checking…",
|
|
1949
|
+
assistantPrompt: "DSH's dsh-workbuddy-connect 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.",
|
|
1950
|
+
assistNotFound: "no usable decryption program was found",
|
|
1951
|
+
assistAmbiguous: "more than one WorkBuddy copy was found and none could be chosen safely",
|
|
1952
|
+
assistIncomplete: "the automatic search could not be completed",
|
|
1953
|
+
assistPathInvalid: "the configured program path is not usable",
|
|
1954
|
+
assistUnavailableCN: "no decryption program is configured for this platform",
|
|
1955
|
+
assistUnavailableAI: "no decryption program is configured for WorkBuddy AI"
|
|
1597
1956
|
};
|
|
1598
1957
|
const zh = {
|
|
1599
1958
|
title: "DSH WorkBuddy Connect",
|
|
@@ -1640,8 +1999,11 @@ window.__ModuleLoader__.load({
|
|
|
1640
1999
|
contextHeading: "上下文窗口",
|
|
1641
2000
|
contextUpTo: "最高 {size}",
|
|
1642
2001
|
contextDefault: "默认 {size}",
|
|
2002
|
+
contextUnknown: "未声明上下文窗口",
|
|
1643
2003
|
useMaximumContextWindow: "使用上游声明的最大上下文窗口",
|
|
1644
2004
|
useMaximumContextWindowHint: "仅作用于 WorkBuddy AI 中声明了更大窗口的模型。",
|
|
2005
|
+
visibilityIntro: "取消勾选即可在模型选择器中隐藏该模型;按当前登录账号分别保存,已在用该模型的会话不受影响。",
|
|
2006
|
+
visibilityStaleAccount: "登录账号已切换——本次修改未保存。",
|
|
1645
2007
|
freeModel: "免费",
|
|
1646
2008
|
badgeLimitedFree: "限时免费",
|
|
1647
2009
|
badgeNightDiscount: "夜间折扣",
|
|
@@ -1676,23 +2038,48 @@ window.__ModuleLoader__.load({
|
|
|
1676
2038
|
probeResultUnknown: "检测未完成",
|
|
1677
2039
|
probeResultAt: "检测于 {time}",
|
|
1678
2040
|
probeResultEmpty: "当前没有可检测的模型。",
|
|
1679
|
-
probeFailed: "检测失败:{message}"
|
|
2041
|
+
probeFailed: "检测失败:{message}",
|
|
2042
|
+
assistantHeading: "让 Agent 帮你处理",
|
|
2043
|
+
assistantIntro: "把下面这段请求发给你的 Agent,它会协助检查应用位置和启动配置。",
|
|
2044
|
+
assistantCopy: "复制给 Agent",
|
|
2045
|
+
assistantCopied: "已复制",
|
|
2046
|
+
assistantCopyFailed: "复制失败,请手动选择上方文字复制",
|
|
2047
|
+
assistantAfter: "Agent 处理完成后,回到这里重新检查;如果修改了 DSH 的启动环境,请先按指引重启 DSH。",
|
|
2048
|
+
assistantRecheck: "已处理,重新检查",
|
|
2049
|
+
assistantRechecking: "正在检查…",
|
|
2050
|
+
assistantPrompt: "DSH 的 dsh-workbuddy-connect 无法使用我的 {appName}:{failureSummary}。请帮我检查实际安装位置和已有路径配置,让插件能正确使用它,并验证恢复结果;如果需要修改 DSH 的启动环境或重启,请给我明确的操作步骤,不要只在当前 shell 临时设置环境变量。",
|
|
2051
|
+
assistNotFound: "没有找到可用的解密程序",
|
|
2052
|
+
assistAmbiguous: "找到了多个 WorkBuddy 副本,无法安全自动选择",
|
|
2053
|
+
assistIncomplete: "自动定位未能完成",
|
|
2054
|
+
assistPathInvalid: "指定的程序路径不可用",
|
|
2055
|
+
assistUnavailableCN: "当前平台尚未配置解密程序",
|
|
2056
|
+
assistUnavailableAI: "尚未配置 WorkBuddy AI 的解密程序"
|
|
1680
2057
|
};
|
|
1681
2058
|
//#endregion
|
|
1682
2059
|
//#region src/client/index.tsx
|
|
1683
2060
|
/** Stable browser-plugin name. */
|
|
1684
2061
|
const name = "dsh-workbuddy-connect-client";
|
|
1685
2062
|
/**
|
|
1686
|
-
*
|
|
2063
|
+
* The bundle's package name, which is also this half's configuration key.
|
|
2064
|
+
*
|
|
2065
|
+
* The Plugins page dispatches `plugins.bundle.config` by the bundle's package
|
|
2066
|
+
* name, so the key has to spell exactly what the profile installs.
|
|
2067
|
+
*/
|
|
2068
|
+
const BUNDLE_NAME = "dsh-workbuddy-connect";
|
|
2069
|
+
/**
|
|
2070
|
+
* Client services required by this browser half.
|
|
1687
2071
|
*
|
|
1688
2072
|
* DSH 0.1.2 removed `@deepseek-ai/dsh-client-runtime` (the package that used to
|
|
1689
|
-
* hold the browser `ClientContext` alias and the `slots` service)
|
|
1690
|
-
*
|
|
1691
|
-
*
|
|
1692
|
-
* `@deepseek-ai/dsh-client-locale
|
|
1693
|
-
*
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
2073
|
+
* hold the browser `ClientContext` alias and the `slots` service), so the
|
|
2074
|
+
* services come from narrower packages: the `slots` registry lives in
|
|
2075
|
+
* `@deepseek-ai/dsh-client-ui-renderer` and `locale` in
|
|
2076
|
+
* `@deepseek-ai/dsh-client-locale`. Neither slot owner is named here on
|
|
2077
|
+
* purpose: `settings.plugin.item`'s declarer (`…-ui-settings-plugins`) is
|
|
2078
|
+
* absent from 0.1.6+ hosts and `plugins.bundle.config`'s declarer
|
|
2079
|
+
* (`…-ui-plugin-manager`) is absent from 0.1.5 hosts, and the seam choice is
|
|
2080
|
+
* made by slot-declaration lifetime, not by activation order — `ctx.slots.inject`
|
|
2081
|
+
* fires whenever the declaring package commits the slot, before or after this
|
|
2082
|
+
* fiber starts.
|
|
1696
2083
|
*/
|
|
1697
2084
|
const inject = [
|
|
1698
2085
|
"slots",
|
|
@@ -1700,57 +2087,105 @@ window.__ModuleLoader__.load({
|
|
|
1700
2087
|
"remote",
|
|
1701
2088
|
"remote.session"
|
|
1702
2089
|
];
|
|
2090
|
+
/** Prefix every guarded client contribution's degradation logs with this. */
|
|
2091
|
+
const CLIENT_CONTRIBUTION_FAILED = "[dsh-workbuddy-connect] client contribution failed to load (host provider unaffected):";
|
|
2092
|
+
/** Disposer handed back when a deferred registration degraded: nothing to undo. */
|
|
2093
|
+
const NOOP_DISPOSER = () => {};
|
|
1703
2094
|
/**
|
|
1704
|
-
*
|
|
2095
|
+
* Run ONE browser-side contribution, degrading its failure to a `console.error`
|
|
2096
|
+
* instead of throwing into the DSH loader. Returns the contribution's own
|
|
2097
|
+
* value on success, or `undefined` when it degraded — the deferred slot
|
|
2098
|
+
* callbacks below substitute `NOOP_DISPOSER` for that, because the slot
|
|
2099
|
+
* runtime always expects a disposer back.
|
|
1705
2100
|
*
|
|
1706
|
-
*
|
|
1707
|
-
* example the rc.6→rc.7 `id`→`key` / `order`→`priority` rename) degrades
|
|
1708
|
-
* to a `console.error` instead of throwing into the DSH loader and raising
|
|
1709
|
-
* the red "Failed to load plugins" banner. The host provider keeps working:
|
|
1710
|
-
* the `workbuddy` model channel is unaffected, and `dsh-workbuddy-connect
|
|
1711
|
-
* status` reports host health via the heartbeat file.
|
|
2101
|
+
* Every contribution is guarded at BOTH boundaries where it can throw:
|
|
1712
2102
|
*
|
|
1713
|
-
*
|
|
1714
|
-
* `
|
|
1715
|
-
*
|
|
1716
|
-
*
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
2103
|
+
* 1. the eager `ctx.slots.inject(...)` / `ctx.inject(...)` call itself, which
|
|
2104
|
+
* runs synchronously inside `apply()` — e.g. a slot-API shape break such as
|
|
2105
|
+
* the rc.6→rc.7 `id`→`key` rename;
|
|
2106
|
+
* 2. the deferred callback, which the slot runtime invokes later — when the
|
|
2107
|
+
* owner commits the slot's declaration, or when the injected services
|
|
2108
|
+
* arrive — long after `apply()` has returned, where no enclosing try/catch
|
|
2109
|
+
* could still catch it.
|
|
2110
|
+
*
|
|
2111
|
+
* The pair is what makes the contributions independent: a failure in one
|
|
2112
|
+
* settings seam, or in the probe control, leaves every other registration
|
|
2113
|
+
* intact. Guards are for THIS browser half only; the host half reports its own
|
|
2114
|
+
* errors through `ctx.logger`.
|
|
1720
2115
|
*/
|
|
1721
|
-
function
|
|
2116
|
+
function guardClientContribution(label, fn) {
|
|
1722
2117
|
try {
|
|
1723
|
-
|
|
2118
|
+
return fn();
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
console.error(`${CLIENT_CONTRIBUTION_FAILED} ${label}`, error);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Register the card copy and both settings-surface seams, one guarded
|
|
2126
|
+
* contribution at a time.
|
|
2127
|
+
*
|
|
2128
|
+
* A DSH slot-API breaking change degrades to a `console.error` per
|
|
2129
|
+
* contribution instead of throwing into the DSH loader and raising the red
|
|
2130
|
+
* "Failed to load plugins" banner; because each contribution carries its own
|
|
2131
|
+
* guard, one failing registration never takes the others with it (the old
|
|
2132
|
+
* settings cards survive a broken Plugins-page seam, and the probe control
|
|
2133
|
+
* survives either). The host provider keeps working throughout: the
|
|
2134
|
+
* `workbuddy` model channel is unaffected, and `dsh-workbuddy-connect status`
|
|
2135
|
+
* reports host health via the heartbeat file.
|
|
2136
|
+
*
|
|
2137
|
+
* The tests import this function directly (`tests/client-fallback.spec.ts`),
|
|
2138
|
+
* so its isolation semantics are pinned against the real entry — keep any
|
|
2139
|
+
* change to the guarded structure in sync with that spec.
|
|
2140
|
+
*/
|
|
2141
|
+
function apply(ctx) {
|
|
2142
|
+
const namespace = "settings.workbuddy";
|
|
2143
|
+
guardClientContribution("settings copy", () => {
|
|
1724
2144
|
ctx.effect(() => ctx.locale.register(namespace, {
|
|
1725
2145
|
zh,
|
|
1726
2146
|
en
|
|
1727
2147
|
}), "dsh-workbuddy-connect: settings copy");
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
inject
|
|
1734
|
-
|
|
1735
|
-
variant
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
name: "conversation.input.right",
|
|
1741
|
-
id: "workbuddy-probe",
|
|
1742
|
-
order: 10,
|
|
1743
|
-
inject: (sessionId) => ({
|
|
1744
|
-
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
1745
|
-
t
|
|
2148
|
+
});
|
|
2149
|
+
const t = ctx.locale.bind(namespace);
|
|
2150
|
+
for (const [index, variant] of CARD_VARIANTS.entries()) {
|
|
2151
|
+
const label = `settings.plugin.item card "${variant.id}"`;
|
|
2152
|
+
guardClientContribution(label, () => {
|
|
2153
|
+
ctx.slots.inject("settings.plugin.item", () => guardClientContribution(label, () => ctx.slots.register({
|
|
2154
|
+
name: "settings.plugin.item",
|
|
2155
|
+
key: variant.id,
|
|
2156
|
+
priority: 30 - index,
|
|
2157
|
+
inject: () => ({
|
|
2158
|
+
t,
|
|
2159
|
+
variant
|
|
1746
2160
|
})
|
|
1747
|
-
},
|
|
2161
|
+
}, WorkBuddyPluginCard)) ?? NOOP_DISPOSER);
|
|
1748
2162
|
});
|
|
1749
|
-
} catch (error) {
|
|
1750
|
-
console.error("[dsh-workbuddy-connect] client card failed to load (host provider unaffected):", error);
|
|
1751
2163
|
}
|
|
2164
|
+
guardClientContribution("plugins.bundle.config page", () => {
|
|
2165
|
+
ctx.slots.inject("plugins.bundle.config", () => guardClientContribution("plugins.bundle.config page", () => ctx.slots.register({
|
|
2166
|
+
name: "plugins.bundle.config",
|
|
2167
|
+
key: "dsh-workbuddy-connect",
|
|
2168
|
+
locale: namespace
|
|
2169
|
+
}, WorkBuddyConfigPage)) ?? NOOP_DISPOSER);
|
|
2170
|
+
});
|
|
2171
|
+
guardClientContribution("conversation probe control", () => {
|
|
2172
|
+
ctx.inject(["modelDirectories"], (scope) => {
|
|
2173
|
+
guardClientContribution("conversation probe control", () => {
|
|
2174
|
+
scope.slots.inject("conversation.input.right", () => guardClientContribution("conversation probe control", () => scope.slots.register({
|
|
2175
|
+
name: "conversation.input.right",
|
|
2176
|
+
id: "workbuddy-probe",
|
|
2177
|
+
order: 10,
|
|
2178
|
+
inject: (sessionId) => ({
|
|
2179
|
+
directory: scope.modelDirectories.directoryFor(sessionId).store,
|
|
2180
|
+
t
|
|
2181
|
+
})
|
|
2182
|
+
}, WorkBuddyProbeControl)) ?? NOOP_DISPOSER);
|
|
2183
|
+
});
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
1752
2186
|
}
|
|
1753
2187
|
//#endregion
|
|
2188
|
+
exports.BUNDLE_NAME = BUNDLE_NAME;
|
|
1754
2189
|
exports.apply = apply;
|
|
1755
2190
|
exports.inject = inject;
|
|
1756
2191
|
exports.name = name;
|