dsh-connect-workbuddy 2.0.10 → 2.0.12

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 CHANGED
@@ -5,7 +5,6 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
8
  let react_jsx_runtime = require("react/jsx-runtime");
10
9
  //#region src/status-paths.ts
11
10
  /**
@@ -130,16 +129,75 @@ window.__ModuleLoader__.load({
130
129
  var WorkBuddySettingsWriteError = class extends Error {
131
130
  /** The settings field that did not land. */
132
131
  field;
133
- constructor(field) {
134
- super(`workbuddy: settings field "${field}" was not persisted by the settings write`);
132
+ constructor(field, reason) {
133
+ super(`workbuddy: settings field "${field}" was not persisted by the settings write${reason === void 0 ? "" : `: ${reason}`}`);
135
134
  this.name = "WorkBuddySettingsWriteError";
136
135
  this.field = field;
137
136
  }
138
137
  };
139
- /** Read the per-region account selections out of the settings snapshot. */
140
- function configuredAccountsOf(configured) {
141
- const accounts = configured?.accounts;
142
- return typeof accounts === "object" && accounts !== null ? accounts : {};
138
+ /** Read one settings field's current object value from the scope snapshot. */
139
+ function fieldSnapshotOf(scope, field) {
140
+ const value = scope.getSnapshot().value?.[field];
141
+ return typeof value === "object" && value !== null ? value : {};
142
+ }
143
+ /**
144
+ * Fallback write: POST the field to the plugin's own Host endpoint.
145
+ *
146
+ * The endpoint handler runs the settings mutate inside the Host process and
147
+ * reports the raw refusal exception, so a failure here names its cause instead
148
+ * of arriving as a swallowed `ok:false`. It merges the posted region key into
149
+ * the field's live value, preserving the other region.
150
+ */
151
+ async function saveViaHostEndpoint(field, region, value) {
152
+ let response;
153
+ try {
154
+ response = await fetch("/plugins/dsh-connect-workbuddy/__save", {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({
158
+ field,
159
+ value: { [region]: value }
160
+ })
161
+ });
162
+ } catch (error) {
163
+ throw new WorkBuddySettingsWriteError(field, `Host save endpoint unreachable: ${String(error)}`);
164
+ }
165
+ if (!response.ok) {
166
+ const detail = await response.json().catch(() => ({ error: `HTTP ${String(response.status)}` }));
167
+ const reason = `${String(detail.errorName ?? "")} ${String(detail.error ?? "")}`.trim();
168
+ throw new WorkBuddySettingsWriteError(field, `Host save refused: ${reason === "" ? String(detail.error) : reason}`);
169
+ }
170
+ }
171
+ /**
172
+ * Write one volatile field, then confirm the value actually landed.
173
+ *
174
+ * The bound settings scope is the official path and is tried FIRST: it keeps
175
+ * the browser mirror in sync, and it is the only writer needed on hosts whose
176
+ * ConfigForm is healthy. On the affected DSH 0.1.7 deployment that scope
177
+ * settles without delivering anything — its `set()` resolves `false` while the
178
+ * Host's own mutate is perfectly healthy (proven by a direct in-Host probe) —
179
+ * so a write that does not read back falls through to the plugin's Host
180
+ * endpoint, which performs the mutate inside the Host process.
181
+ *
182
+ * `landed` decides whether a value read back from the scope is the value that
183
+ * was written; it is per-field because `''` (a cleared account slot) and an
184
+ * absent key are different states, and a truncated model catalog must not pass
185
+ * a shallow "is anything there?" check.
186
+ *
187
+ * @throws {WorkBuddySettingsWriteError} when neither path persists the value.
188
+ */
189
+ async function writeField(scope, field, region, value, landed) {
190
+ let scopeDelivered = false;
191
+ try {
192
+ scopeDelivered = await scope.set(field, {
193
+ ...fieldSnapshotOf(scope, field),
194
+ [region]: value
195
+ }) !== false;
196
+ } catch {
197
+ scopeDelivered = false;
198
+ }
199
+ if (scopeDelivered && landed(fieldSnapshotOf(scope, field)[region])) return;
200
+ await saveViaHostEndpoint(field, region, value);
143
201
  }
144
202
  /**
145
203
  * Write one region's account slot, then confirm the value actually landed.
@@ -168,12 +226,7 @@ window.__ModuleLoader__.load({
168
226
  * something else — i.e. the write silently did not persist.
169
227
  */
170
228
  async function writeAccountSlot(scope, region, value) {
171
- const accounts = configuredAccountsOf(scope.getSnapshot().value);
172
- await scope.set("accounts", {
173
- ...accounts,
174
- [region]: value
175
- });
176
- if (configuredAccountsOf(scope.getSnapshot().value)[region] !== value) throw new WorkBuddySettingsWriteError("accounts");
229
+ await writeField(scope, "accounts", region, value, (readBack) => readBack === value);
177
230
  }
178
231
  /**
179
232
  * Write one region's saved model state, then confirm it actually landed.
@@ -194,16 +247,43 @@ window.__ModuleLoader__.load({
194
247
  * @throws {WorkBuddySettingsWriteError} when the catalog does not read back.
195
248
  */
196
249
  async function writeRegionModels(scope, region, payload) {
197
- const configured = scope.getSnapshot().value?.regions;
198
- const regions = typeof configured === "object" && configured !== null ? configured : {};
199
- await scope.set("regions", {
200
- ...regions,
201
- [region]: payload
202
- });
203
- const landed = scope.getSnapshot().value?.regions?.[region];
204
250
  const written = payload.lastCatalog.map((model) => model.id);
205
- const stored = landed?.lastCatalog?.map((model) => model.id);
206
- if (!(stored !== void 0 && stored.length === written.length && stored.every((id, index) => id === written[index]))) throw new WorkBuddySettingsWriteError("regions");
251
+ await writeField(scope, "regions", region, payload, (readBack) => {
252
+ const stored = readBack?.lastCatalog?.map((model) => model.id);
253
+ return stored !== void 0 && stored.length === written.length && stored.every((id, index) => id === written[index]);
254
+ });
255
+ }
256
+ /**
257
+ * Switch one region's provider off or on, with the same landed-check as every
258
+ * other settings write.
259
+ *
260
+ * This exists because the toggle was the ONE write that still called
261
+ * `scope.set()` directly. On the affected 0.1.7 deployments that scope settles
262
+ * without storing anything, so flipping a provider off appeared to work and
263
+ * then silently reverted — the exact silent-failure mode
264
+ * {@link writeAccountSlot} and {@link writeRegionModels} were written to
265
+ * prevent for their own fields. Routing it through {@link writeField} gives it
266
+ * the same scope-first-then-Host-endpoint path, so the switch either lands or
267
+ * reports why it did not.
268
+ *
269
+ * The write carries the region's whole slot through untouched — only `enabled`
270
+ * changes — so the user's directory, model picks, image opt-ins and budgets
271
+ * survive the round trip. The Host withdraws or restores the provider route on
272
+ * the next `onChange`, which is what actually removes it from DSH's model
273
+ * picker.
274
+ *
275
+ * @param scope - the bound settings scope for this plugin's namespace.
276
+ * @param region - the region being switched.
277
+ * @param enabled - the desired state.
278
+ * @param slot - the region's COMPLETE next slot (the caller owns the snapshot
279
+ * read and the whole-slot merge, so this stays a pure write).
280
+ * @throws {WorkBuddySettingsWriteError} when neither writer persists the value.
281
+ */
282
+ async function writeRegionEnabled(scope, region, enabled, slot) {
283
+ await writeField(scope, "regions", region, slot, (readBack) => {
284
+ const stored = readBack;
285
+ return stored !== null && stored !== void 0 && stored["enabled"] === enabled;
286
+ });
207
287
  }
208
288
  //#endregion
209
289
  //#region src/client/icon.ts
@@ -244,7 +324,11 @@ window.__ModuleLoader__.load({
244
324
  .dsm-plugin-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
245
325
  .dsm-plugin-card-title{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;font-weight:600;line-height:1.4}
246
326
  .dsm-plugin-card-description{color:var(--dsw-alias-label-tertiary,#999);font-size:13px;line-height:1.5}
247
- .dsm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary,#999);flex:none;display:inline-flex;transition:transform .16s}
327
+ /* Pure-CSS caret: the host primitives' chevron icon names differ per DSH line
328
+ (0.1.5 Outline14 vs 0.1.7 OutlineRegular), so no static import can serve
329
+ both. A border caret in the plugin's own CSS is version-proof. */
330
+ .dsm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary,#999);flex:none;width:16px;height:16px;position:relative;transition:transform .16s}
331
+ .dsm-plugin-card-chevron::before{content:"";display:block;position:absolute;left:4px;top:5px;width:7px;height:7px;border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;transform:rotate(45deg)}
248
332
  .dsm-plugin-card-chevron-open{transform:rotate(180deg)}
249
333
  .dsm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2,#36373b);margin:0 16px;padding:0 0 8px}
250
334
  .dsm-plugin-card-icon{width:32px;height:32px;flex:none;border-radius:7px}
@@ -291,7 +375,6 @@ window.__ModuleLoader__.load({
291
375
  .dsm-workbuddy-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
292
376
  .dsm-workbuddy-searched-reason-encrypted{color:var(--dsw-alias-state-warning-primary,#e0a13a)}
293
377
  .dsm-workbuddy-searched-reason-wrong-region{color:var(--dsw-alias-brand-primary,#5686fe)}
294
- .dsm-workbuddy-searched-notice{margin:6px 0 0;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#36373b);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:12px;line-height:18px}
295
378
  .dsm-workbuddy-searched-more{margin-top:6px;padding:0;border:0;background:none;color:var(--dsw-alias-brand-primary,#5686fe);font:inherit;font-size:12px;line-height:18px;cursor:pointer;text-align:left}
296
379
  .dsm-workbuddy-searched-more:hover{text-decoration:underline}
297
380
  .dsm-workbuddy-usage-error{margin:0;font-size:14px;line-height:22px;color:var(--dsw-alias-state-error-primary,#ef4444)}
@@ -386,6 +469,12 @@ window.__ModuleLoader__.load({
386
469
  * Order within each group is preserved from the Host: the store reports
387
470
  * candidates in probe order, which is meaningful (the live file before its
388
471
  * timestamped backups).
472
+ *
473
+ * There is deliberately no `encrypted` flag here. There used to be, for a
474
+ * notice rendered inside the list; that notice is gone because
475
+ * {@link signedOutNotice} now answers the encrypted cause in the paragraph, so
476
+ * the flag would have had no consumer — and a derived field kept "just in
477
+ * case" is how the list and the paragraph drift apart again.
389
478
  */
390
479
  function searchedView(items) {
391
480
  const interesting = items.filter((item) => INTERESTING_REASONS.includes(item.reason));
@@ -393,7 +482,6 @@ window.__ModuleLoader__.load({
393
482
  interesting,
394
483
  missing: items.filter((item) => !INTERESTING_REASONS.includes(item.reason)),
395
484
  missingOpen: interesting.length === 0,
396
- encrypted: items.some((item) => item.reason === "encrypted"),
397
485
  total: items.length
398
486
  };
399
487
  }
@@ -440,10 +528,17 @@ window.__ModuleLoader__.load({
440
528
  *
441
529
  * `selectionLost` still wins outright: there the tokens are healthy and the
442
530
  * advice is to re-pick an account, which no path list can replace.
531
+ *
532
+ * `encrypted` also outranks the generic hint, and for the same reason
533
+ * `wrong-region` does: the generic copy tells the user to sign in again, which
534
+ * is precisely the action that cannot work when the credential exists but is
535
+ * encrypted. Leaving it in the collapsed list meant the headline contradicted
536
+ * the detail below it — and the headline is the only part most users read.
443
537
  */
444
538
  function signedOutNotice(input) {
445
539
  if (input.selectionLost) return { key: "row.selectionLostMessage" };
446
540
  if (input.searched.some((item) => item.reason === "wrong-region")) return { key: "row.signedOutWrongRegion" };
541
+ if (input.searched.some((item) => item.reason === "encrypted")) return { key: "row.signedOutEncrypted" };
447
542
  if (input.searched.length > 0) return { key: "row.signedOutHint" };
448
543
  return input.message === void 0 ? { key: "row.signedOutHint" } : {
449
544
  key: "row.signedOutHint",
@@ -521,9 +616,12 @@ window.__ModuleLoader__.load({
521
616
  * buried under a dozen "not found" lines. When nothing interesting was found
522
617
  * the absent list IS the explanation, so it opens directly.
523
618
  *
524
- * An `encrypted` failure additionally raises a notice: it is the one cause
525
- * where the user is very likely already signed in, and telling them to sign in
526
- * again sends them to an action that cannot work.
619
+ * An `encrypted` failure deliberately adds NO notice of its own. It used to,
620
+ * because the paragraph above still said "sign in once in the desktop app" —
621
+ * the one action that cannot work when the credential exists but is encrypted.
622
+ * `signedOutNotice` now answers that cause in the paragraph itself, so a second
623
+ * copy here would restate it: the paragraph states the situation, this list
624
+ * supplies the detail, and the per-entry label already says what each path is.
527
625
  */
528
626
  function SearchedPaths({ items, t }) {
529
627
  const view = searchedView(items);
@@ -542,11 +640,6 @@ window.__ModuleLoader__.load({
542
640
  className: "dsm-workbuddy-searched-hint",
543
641
  children: t("row.searchedHint")
544
642
  }),
545
- view.encrypted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
546
- className: "dsm-workbuddy-searched-notice",
547
- role: "status",
548
- children: t("row.searchedEncryptedNotice")
549
- }) : null,
550
643
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
551
644
  className: "dsm-workbuddy-searched-list",
552
645
  children: [view.interesting.map((item) => renderSearchedItem(item, t)), showMissing ? view.missing.map((item) => renderSearchedItem(item, t)) : null]
@@ -596,9 +689,9 @@ window.__ModuleLoader__.load({
596
689
  return { 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)" };
597
690
  }
598
691
  /** Render WorkBuddy sign-in state, credits, and model selection as one card. */
599
- function WorkBuddyCard({ t, settingsScope }) {
692
+ function WorkBuddyCard({ t, settingsScope, view }) {
600
693
  if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
601
- const [open, setOpen] = (0, react.useState)(false);
694
+ const [open, setOpen] = (0, react.useState)(view === "page");
602
695
  /** The region whose tab is on screen; each tab is its own provider stack. */
603
696
  const [activeRegion, setActiveRegion] = (0, react.useState)("cn");
604
697
  /**
@@ -767,12 +860,19 @@ window.__ModuleLoader__.load({
767
860
  * directory, model picks, image opt-ins and budgets survive a round trip.
768
861
  * The Host withdraws or restores the provider route on the next `onChange`,
769
862
  * which is what actually removes it from DSH's model picker.
863
+ *
864
+ * Goes through `writeRegionEnabled` rather than calling `scope.set()`
865
+ * directly: on the affected 0.1.7 deployments that scope settles without
866
+ * storing anything, so a direct write appeared to succeed and then silently
867
+ * reverted. The helper adds the landed-check and the Host-endpoint fallback
868
+ * that every other settings write already had.
770
869
  */
771
870
  const toggleRegion = async (item, enabled) => {
772
- if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
871
+ if (settingsScope === void 0) return;
773
872
  setTogglingRegion(item);
774
873
  try {
775
- await settingsScope.set("regions", nextRegionEnabled(settingsScope.getSnapshot().value, item, enabled));
874
+ const slot = nextRegionEnabled(settingsScope.getSnapshot().value, item, enabled)[item];
875
+ await writeRegionEnabled(settingsScope, item, enabled, typeof slot === "object" && slot !== null ? slot : {});
776
876
  } finally {
777
877
  if (mounted.current) setTogglingRegion(void 0);
778
878
  }
@@ -845,13 +945,22 @@ window.__ModuleLoader__.load({
845
945
  const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
846
946
  const savedImageIds = status.status === "signed-in" ? new Set(status.imageModelIds) : /* @__PURE__ */ new Set();
847
947
  const activeImageIds = draft?.imageIds ?? savedImageIds;
848
- const configured = settingsScope?.getSnapshot().value;
849
- const configuredRegion = status.status === "signed-in" ? configured?.regions?.[status.region] : void 0;
850
- const legacyContextBudgets = status.status === "signed-in" && status.region === "cn" ? configured?.contextBudgets : void 0;
851
- const savedContextBudgetsSource = configuredRegion?.contextBudgets ?? legacyContextBudgets;
852
- const savedContextBudgets = typeof savedContextBudgetsSource === "object" && savedContextBudgetsSource !== null ? savedContextBudgetsSource : {};
948
+ settingsScope?.getSnapshot().value;
949
+ const savedContextBudgets = status.status === "signed-in" ? status.contextBudgets ?? {} : {};
853
950
  const activeContextBudgets = draft?.contextBudgets ?? savedContextBudgets;
854
951
  const dirty = draft !== void 0;
952
+ /**
953
+ * Whether the card's inputs accept edits.
954
+ *
955
+ * Deliberately NOT the settings scope's own `writable` flag: on the affected
956
+ * DSH 0.1.7 deployment that flag stays false (the scope initialises it false
957
+ * and only raises it once its mirror loads as a Host-backed form), which left
958
+ * every input `disabled` — clicking them did nothing at all. Writes no longer
959
+ * depend on that flag: `writeField` tries the scope and falls back to the
960
+ * plugin's own Host endpoint, which performs the mutate in the Host process.
961
+ * A bound scope is therefore all that is required to accept an edit.
962
+ */
963
+ const canWrite = settingsScope !== void 0;
855
964
  const editDraft = (edit) => {
856
965
  setDrafts((prev) => ({
857
966
  ...prev,
@@ -982,8 +1091,7 @@ window.__ModuleLoader__.load({
982
1091
  }),
983
1092
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
984
1093
  "aria-hidden": "true",
985
- className: `dsm-plugin-card-chevron${open ? " dsm-plugin-card-chevron-open" : ""}`,
986
- children: (0, react.createElement)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 14 })
1094
+ className: `dsm-plugin-card-chevron${open ? " dsm-plugin-card-chevron-open" : ""}`
987
1095
  })
988
1096
  ]
989
1097
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1021,7 +1129,7 @@ window.__ModuleLoader__.load({
1021
1129
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1022
1130
  type: "checkbox",
1023
1131
  checked: regionOnState,
1024
- disabled: togglingRegion === region || settingsScope?.getSnapshot().writable !== true,
1132
+ disabled: togglingRegion === region || !canWrite,
1025
1133
  "aria-label": t("row.tabSwitchAria", { region: region === "cn" ? t("row.tabCn") : t("row.tabGlobal") }),
1026
1134
  onChange: (event) => {
1027
1135
  toggleRegion(region, event.target.checked);
@@ -1085,7 +1193,7 @@ window.__ModuleLoader__.load({
1085
1193
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1086
1194
  className: "dsm-workbuddy-usage-select",
1087
1195
  value: status.accounts.find((account) => account.selected)?.id ?? "",
1088
- disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
1196
+ disabled: switchingAccount || !canWrite,
1089
1197
  onChange: (event) => {
1090
1198
  switchAccount(event.currentTarget.value);
1091
1199
  },
@@ -1194,7 +1302,7 @@ window.__ModuleLoader__.load({
1194
1302
  status.recovery?.usableAccount === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1195
1303
  type: "button",
1196
1304
  className: "dsm-btn dsm-btn-outline",
1197
- disabled: switchingAccount || settingsScope?.getSnapshot().writable !== true,
1305
+ disabled: switchingAccount || !canWrite,
1198
1306
  onClick: () => {
1199
1307
  const target = status.recovery?.usableAccount;
1200
1308
  if (target !== void 0) switchAccount(target.accountId);
@@ -1241,7 +1349,7 @@ window.__ModuleLoader__.load({
1241
1349
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1242
1350
  type: "checkbox",
1243
1351
  checked: activeEnabledIds.has(model.id),
1244
- disabled: settingsScope?.getSnapshot().writable !== true || saving,
1352
+ disabled: !canWrite || saving,
1245
1353
  onChange: () => {
1246
1354
  toggleModel(model.id);
1247
1355
  }
@@ -1266,7 +1374,7 @@ window.__ModuleLoader__.load({
1266
1374
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1267
1375
  type: "checkbox",
1268
1376
  checked: activeImageIds.has(model.id),
1269
- disabled: settingsScope?.getSnapshot().writable !== true || saving,
1377
+ disabled: !canWrite || saving,
1270
1378
  onChange: () => {
1271
1379
  toggleImage(model.id);
1272
1380
  }
@@ -1279,7 +1387,7 @@ window.__ModuleLoader__.load({
1279
1387
  type: "radio",
1280
1388
  name: `context-${model.id}`,
1281
1389
  checked: (activeContextBudgets[model.id] ?? 2e5) === 2e5,
1282
- disabled: settingsScope?.getSnapshot().writable !== true || saving,
1390
+ disabled: !canWrite || saving,
1283
1391
  onChange: () => {
1284
1392
  setContextBudget(model.id, 2e5);
1285
1393
  }
@@ -1287,7 +1395,7 @@ window.__ModuleLoader__.load({
1287
1395
  type: "radio",
1288
1396
  name: `context-${model.id}`,
1289
1397
  checked: model.nativeContextWindow <= 2e5 || activeContextBudgets[model.id] === model.nativeContextWindow,
1290
- disabled: model.nativeContextWindow <= 2e5 || settingsScope?.getSnapshot().writable !== true || saving,
1398
+ disabled: model.nativeContextWindow <= 2e5 || !canWrite || saving,
1291
1399
  onChange: () => {
1292
1400
  setContextBudget(model.id, model.nativeContextWindow);
1293
1401
  }
@@ -1395,6 +1503,7 @@ window.__ModuleLoader__.load({
1395
1503
  "row.signedOut": "Not signed in",
1396
1504
  "row.signedOutHint": "Sign in once in the WorkBuddy desktop app; this plugin follows that sign-in automatically.",
1397
1505
  "row.signedOutWrongRegion": "Your WorkBuddy sign-in was found, but it belongs to the other region tab. Switch tabs above to use it — there is nothing to sign in again.",
1506
+ "row.signedOutEncrypted": "Your WorkBuddy sign-in is present, but its token fields are encrypted and the plugin could not read the key. Install the WorkBuddy desktop app, or set WORKBUDDY_APP_EXECUTABLE to its executable — signing in again will not change this.",
1398
1507
  "row.searchedTitle": "Paths checked",
1399
1508
  "row.searchedHint": "If your WorkBuddy keeps its login somewhere else, point the plugin at it with the WORKBUDDY_AUTH_FILE environment variable.",
1400
1509
  "row.reasonMissing": "not found",
@@ -1404,7 +1513,6 @@ window.__ModuleLoader__.load({
1404
1513
  "row.reasonWrongRegion": "belongs to the other region",
1405
1514
  "row.sourceDesktop": "desktop app",
1406
1515
  "row.sourceDsh": "plugin copy",
1407
- "row.searchedEncryptedNotice": "The credential file is there, but its token fields are encrypted and the key could not be read. Install the WorkBuddy desktop app, or set WORKBUDDY_APP_EXECUTABLE to its executable — signing in again on its own will not help.",
1408
1516
  "row.searchedMore": "Show the {count} path(s) that were not found",
1409
1517
  "row.signedIn": "Signed in: {accountName}",
1410
1518
  "row.tokenExpiry": "Access token expires {expiresAt} (auto-renewal)",
@@ -1470,6 +1578,7 @@ window.__ModuleLoader__.load({
1470
1578
  "row.signedOut": "未登录",
1471
1579
  "row.signedOutHint": "在 WorkBuddy 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
1472
1580
  "row.signedOutWrongRegion": "找到了你的 WorkBuddy 登录信息,但它属于另一个地区的标签页。切换到上方对应标签页即可使用——不需要重新登录。",
1581
+ "row.signedOutEncrypted": "你的 WorkBuddy 登录信息在,但其中的 token 字段是加密的,插件拿不到密钥。请安装 WorkBuddy 桌面 App,或用环境变量 WORKBUDDY_APP_EXECUTABLE 指定它的可执行文件——重新登录无法解决这个问题。",
1473
1582
  "row.searchedTitle": "已检查的路径",
1474
1583
  "row.searchedHint": "如果你的 WorkBuddy 把登录信息存在别处,可以用环境变量 WORKBUDDY_AUTH_FILE 指定。",
1475
1584
  "row.reasonMissing": "不存在",
@@ -1479,7 +1588,6 @@ window.__ModuleLoader__.load({
1479
1588
  "row.reasonWrongRegion": "属于另一个地区",
1480
1589
  "row.sourceDesktop": "桌面 App",
1481
1590
  "row.sourceDsh": "插件副本",
1482
- "row.searchedEncryptedNotice": "凭据文件在,但其中的 token 字段是加密的,插件拿不到密钥。请安装 WorkBuddy 桌面 App,或用环境变量 WORKBUDDY_APP_EXECUTABLE 指定它的可执行文件——单纯重新登录解决不了这个问题。",
1483
1591
  "row.searchedMore": "显示另外 {count} 条「不存在」的路径",
1484
1592
  "row.signedIn": "已登录:{accountName}",
1485
1593
  "row.tokenExpiry": "访问令牌 {expiresAt} 过期(自动续期)",
@@ -1535,12 +1643,17 @@ window.__ModuleLoader__.load({
1535
1643
  //#region src/client/index.tsx
1536
1644
  /** Stable browser-plugin name. */
1537
1645
  const name = "dsh-connect-workbuddy-client";
1538
- /** Client services required by the Plugin configuration contribution. */
1539
- const inject = [
1540
- "slots",
1541
- "locale",
1542
- "settingsScope"
1543
- ];
1646
+ /**
1647
+ * Client services required by the Plugin configuration contribution.
1648
+ *
1649
+ * Deliberately only the two services that exist on BOTH host lines. The
1650
+ * settings surface differs by line — 0.1.5 provides `settingsScope`, 0.1.7
1651
+ * replaces it with `configForms` — and Cordis' dependency gate is hard: any
1652
+ * inject entry the running line does not provide keeps `apply` from ever
1653
+ * running. Probing both via `ctx.get()` (which returns undefined, never
1654
+ * throws, for an absent service) is what lets one build serve both lines.
1655
+ */
1656
+ const inject = ["slots", "locale"];
1544
1657
  /** Register card copy and the WorkBuddy card under Plugin configuration. */
1545
1658
  function apply(ctx) {
1546
1659
  try {
@@ -1550,16 +1663,36 @@ window.__ModuleLoader__.load({
1550
1663
  en
1551
1664
  }), "dsh-connect-workbuddy: settings copy");
1552
1665
  const t = ctx.locale.bind(namespace);
1553
- const settingsScope = ctx.settingsScope.bind({ namespace: "workbuddy" });
1554
- ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
1555
- name: "settings.plugin.item",
1556
- key: "workbuddy",
1557
- priority: 30,
1558
- inject: () => ({
1559
- t,
1560
- settingsScope
1561
- })
1562
- }, WorkBuddyCard));
1666
+ const softGet = (name) => ctx.get(name);
1667
+ let settingsScope;
1668
+ const forms = softGet("configForms");
1669
+ const legacy = softGet("settingsScope");
1670
+ if (forms !== void 0) {
1671
+ let ns = "workbuddy";
1672
+ try {
1673
+ const served = (forms.describe().getSnapshot().view?.namespaces ?? []).find((entry) => entry.ns === "workbuddy" || /workbuddy/i.test(entry.ns));
1674
+ if (served !== void 0) ns = served.ns;
1675
+ } catch {}
1676
+ settingsScope = forms.get(ns);
1677
+ } else if (legacy !== void 0) settingsScope = legacy.bind({ namespace: "workbuddy" });
1678
+ const registerCard = (slotName, key) => {
1679
+ try {
1680
+ ctx.slots.inject(slotName, () => ctx.slots.register({
1681
+ name: slotName,
1682
+ key,
1683
+ priority: 30,
1684
+ inject: () => settingsScope === void 0 ? { t } : {
1685
+ t,
1686
+ settingsScope
1687
+ }
1688
+ }, WorkBuddyCard));
1689
+ } catch (error) {
1690
+ console.error(`[dsh-connect-workbuddy] card slot "${slotName}" failed to register (host provider unaffected):`, error);
1691
+ }
1692
+ };
1693
+ registerCard("plugins.bundle.config", "dsh-connect-workbuddy");
1694
+ registerCard("plugins.row.config", "dsh-connect-workbuddy#dsh-connect-workbuddy");
1695
+ registerCard("settings.plugin.item", "workbuddy");
1563
1696
  } catch (error) {
1564
1697
  console.error("[dsh-connect-workbuddy] client card failed to load (host provider unaffected):", error);
1565
1698
  }