dsh-workbuddy-connect 0.5.1 → 0.5.2

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/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { E as WorkBuddyUpstreamClient, J as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-CE_2IvEG.js";
2
+ import { E as WorkBuddyUpstreamClient, J as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-CExA7lJt.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
package/lib/client.js CHANGED
@@ -31,6 +31,27 @@ window.__ModuleLoader__.load({
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
33
  //#endregion
34
+ //#region src/client/status-document.ts
35
+ /**
36
+ * Whether a parsed status response really is a status document.
37
+ *
38
+ * A 200 is not a promise about the body: it may be empty, literal `null`, a
39
+ * non-JSON page from a proxy, or an array. Both halves of the browser plugin
40
+ * read the same route, so both must agree on what is valid — storing an
41
+ * unreadable value puts something in state that the next render dereferences.
42
+ *
43
+ * The check is deliberately limited to the discriminator (plus `error`'s
44
+ * `message`, which the error paragraph renders): validating optional fields
45
+ * here would reject documents the host legitimately omits fields from.
46
+ */
47
+ function isWorkBuddyWebStatus(value) {
48
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
49
+ const wrapped = value;
50
+ const status = wrapped["status"];
51
+ if (status === "signed-out" || status === "signed-in") return true;
52
+ return status === "error" && typeof wrapped["message"] === "string";
53
+ }
54
+ //#endregion
34
55
  //#region src/client/WorkBuddyPluginCard.tsx
35
56
  /** WorkBuddy status card contributed to Harness Plugin configuration. */
36
57
  /** CN WorkBuddy; the plugin's long-standing card and default. */
@@ -291,6 +312,12 @@ window.__ModuleLoader__.load({
291
312
  background: "var(--dsw-alias-brand-primary, #1677ff)"
292
313
  };
293
314
  }
315
+ /**
316
+ * Status dot colour. Takes `'loading'` as well as the document's own states:
317
+ * before the first response the card knows nothing about the account, so it must
318
+ * not borrow the signed-out grey — that would read as "nothing is wrong, nobody
319
+ * is signed in" when the truth is "not read yet".
320
+ */
294
321
  function dotStyle(status) {
295
322
  return {
296
323
  width: 9,
@@ -309,29 +336,40 @@ window.__ModuleLoader__.load({
309
336
  timeStyle: "short"
310
337
  }).format(new Date(ms));
311
338
  }
312
- /** One billing package as a labeled progress bar. */
339
+ /**
340
+ * One billing package as a labeled progress bar.
341
+ *
342
+ * A package whose allowance the upstream never reported (`size` not positive)
343
+ * has no percentage to state. It must not fall back to 100%: the plugin would be
344
+ * claiming a full quota it knows nothing about, which is the opposite of the
345
+ * honest "remaining N" line printed below it. Unknown size therefore renders the
346
+ * percent slot as unknown copy and an unfilled, indeterminate track.
347
+ */
313
348
  function CreditBar({ label, remain, size, t }) {
314
- const detail = size > 0 ? t("exactRemaining", {
349
+ const sizeKnown = size > 0;
350
+ const detail = sizeKnown ? t("exactRemaining", {
315
351
  remain: formatNumber(remain),
316
352
  size: formatNumber(size)
317
353
  }) : t("creditPackageUnknownSize", { remain: formatNumber(remain) });
318
- const percent = size > 0 ? remain / size * 100 : 100;
319
- const display = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent);
354
+ const percent = sizeKnown ? remain / size * 100 : void 0;
355
+ const display = percent === void 0 ? t("percentUnknown") : t("percentRemaining", { percent: new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent) });
320
356
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
321
357
  style: quotaGroupStyle,
322
358
  children: [
323
359
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
324
360
  style: quotaLabelStyle,
325
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("percentRemaining", { percent: display }) })]
361
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display })]
326
362
  }),
327
363
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
328
364
  style: progressTrackStyle,
329
365
  role: "progressbar",
330
366
  "aria-label": label,
331
- "aria-valuemin": 0,
332
- "aria-valuemax": 100,
333
- "aria-valuenow": percent,
334
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: progressFillStyle(percent) })
367
+ ...percent === void 0 ? { "aria-valuetext": detail } : {
368
+ "aria-valuemin": 0,
369
+ "aria-valuemax": 100,
370
+ "aria-valuenow": percent
371
+ },
372
+ children: percent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: progressFillStyle(percent) })
335
373
  }),
336
374
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
337
375
  style: bodyStyle,
@@ -555,17 +593,72 @@ window.__ModuleLoader__.load({
555
593
  function WorkBuddyPluginCard({ t, variant = CN_CARD_VARIANT }) {
556
594
  if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
557
595
  const [open, setOpen] = (0, react.useState)(false);
558
- const [status, setStatus] = (0, react.useState)({ status: "signed-out" });
596
+ /**
597
+ * The document to render. `undefined` means *not read yet*, which is a
598
+ * distinct state from "signed out": seeding this with a signed-out document
599
+ * told an already-signed-in user they were signed out for the whole first
600
+ * round trip (and forever, if the read never settled).
601
+ */
602
+ const [status, setStatus] = (0, react.useState)();
603
+ /**
604
+ * Whether the last **successful** read found a usable credential.
605
+ *
606
+ * Kept apart from `status` because the poll's liveness must depend on what the
607
+ * account actually is, not on what the card last displayed: a failed read
608
+ * leaves this untouched, so a transient failure cannot disarm the interval,
609
+ * while a genuine signed-out answer still stops it.
610
+ *
611
+ * `undefined` therefore means "no successful read yet", which is also the
612
+ * condition that decides whether a failed read has anything to preserve.
613
+ */
614
+ const [signedIn, setSignedIn] = (0, react.useState)();
615
+ /**
616
+ * Why the most recent read failed, when it did. Rendered as a notice beside
617
+ * whatever document is still on screen, rather than replacing it.
618
+ */
619
+ const [readFailure, setReadFailure] = (0, react.useState)();
559
620
  const [busy, setBusy] = (0, react.useState)(false);
560
621
  const [tab, setTab] = (0, react.useState)("status");
561
622
  const mounted = (0, react.useRef)(true);
623
+ /**
624
+ * Identity of the newest read that may write. Assigned when a read *starts*,
625
+ * so a response is superseded by anything begun after it — "the response whose
626
+ * request started last wins". Without this, a slow poll begun before a manual
627
+ * action could settle after the action's own refresh and restore the older
628
+ * document.
629
+ */
630
+ const readSeq = (0, react.useRef)(0);
631
+ /** Manual requests in flight, so unmount can abort them like the poll's. */
632
+ const manualControllers = (0, react.useRef)(/* @__PURE__ */ new Set());
562
633
  (0, react.useEffect)(() => {
563
634
  mounted.current = true;
564
635
  return () => {
565
636
  mounted.current = false;
637
+ for (const controller of manualControllers.current) controller.abort();
638
+ manualControllers.current.clear();
566
639
  };
567
640
  }, []);
641
+ /** Register a manual request's controller so unmount aborts it. */
642
+ const trackController = (0, react.useCallback)(() => {
643
+ const controller = new AbortController();
644
+ manualControllers.current.add(controller);
645
+ return controller;
646
+ }, []);
647
+ /**
648
+ * Read the status document and apply it under the two policies the card's
649
+ * correctness rests on:
650
+ *
651
+ * - a non-document body (empty, `null`, a non-JSON page) is a failed read, not
652
+ * something to store and then dereference in the render;
653
+ * - a failed read never discards a document already on screen. It is recorded
654
+ * and shown as a notice beside that document; only when nothing has been
655
+ * read yet does the failure itself become the rendered state.
656
+ *
657
+ * Returns whether this read produced the current document.
658
+ */
568
659
  const refresh = (0, react.useCallback)(async (signal) => {
660
+ const seq = ++readSeq.current;
661
+ const current = () => mounted.current && signal?.aborted !== true && seq === readSeq.current;
569
662
  try {
570
663
  const response = await fetch(variant.statusPath, {
571
664
  headers: { accept: "application/json" },
@@ -574,12 +667,23 @@ window.__ModuleLoader__.load({
574
667
  });
575
668
  const value = await response.json().catch(() => void 0);
576
669
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
577
- if (mounted.current && signal?.aborted !== true) setStatus(value);
670
+ if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
671
+ if (!current()) return false;
672
+ setStatus(value);
673
+ if (value.status === "signed-in") setSignedIn(true);
674
+ else if (value.status === "signed-out") setSignedIn(false);
675
+ setReadFailure(void 0);
676
+ return true;
578
677
  } catch (error) {
579
- if (mounted.current && signal?.aborted !== true) setStatus({
580
- status: "error",
581
- message: error instanceof Error ? error.message : t("requestFailed")
582
- });
678
+ const message = error instanceof Error ? error.message : t("requestFailed");
679
+ if (current()) {
680
+ setReadFailure(message);
681
+ setStatus((previous) => previous === void 0 ? {
682
+ status: "error",
683
+ message
684
+ } : previous);
685
+ }
686
+ return false;
583
687
  }
584
688
  }, [t, variant.statusPath]);
585
689
  (0, react.useEffect)(() => {
@@ -591,7 +695,7 @@ window.__ModuleLoader__.load({
591
695
  };
592
696
  }, [open, refresh]);
593
697
  (0, react.useEffect)(() => {
594
- if (!open || status.status !== "signed-in") return;
698
+ if (!open || signedIn === false) return;
595
699
  const controller = new AbortController();
596
700
  const timer = window.setInterval(() => {
597
701
  refresh(controller.signal);
@@ -603,13 +707,15 @@ window.__ModuleLoader__.load({
603
707
  }, [
604
708
  open,
605
709
  refresh,
606
- status.status
710
+ signedIn
607
711
  ]);
608
712
  const manualRefresh = async () => {
609
713
  setBusy(true);
714
+ const controller = trackController();
610
715
  try {
611
- await refresh();
716
+ await refresh(controller.signal);
612
717
  } finally {
718
+ manualControllers.current.delete(controller);
613
719
  if (mounted.current) setBusy(false);
614
720
  }
615
721
  };
@@ -622,9 +728,10 @@ window.__ModuleLoader__.load({
622
728
  * than thrown away, so the reason survives the round trip.
623
729
  */
624
730
  const refreshModels = (0, react.useCallback)(async () => {
625
- const key = status.status === "signed-in" ? status.probeKey : void 0;
731
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
626
732
  if (key === void 0) return;
627
733
  setBusy(true);
734
+ const controller = trackController();
628
735
  try {
629
736
  const response = await fetch(variant.probePath, {
630
737
  method: "POST",
@@ -633,23 +740,27 @@ window.__ModuleLoader__.load({
633
740
  "X-WorkBuddy-Probe-Key": key
634
741
  },
635
742
  credentials: "same-origin",
743
+ signal: controller.signal,
636
744
  body: JSON.stringify({ action: "refresh" })
637
745
  });
638
746
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
639
747
  } catch (error) {
640
- if (mounted.current) setStatus((previous) => ({
641
- status: "error",
642
- message: error instanceof Error ? error.message : t("requestFailed")
643
- }));
748
+ if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
749
+ manualControllers.current.delete(controller);
644
750
  return;
645
751
  } finally {
646
752
  if (mounted.current) setBusy(false);
647
753
  }
648
- await refresh();
754
+ try {
755
+ await refresh(controller.signal);
756
+ } finally {
757
+ manualControllers.current.delete(controller);
758
+ }
649
759
  }, [
650
760
  refresh,
651
761
  status,
652
762
  t,
763
+ trackController,
653
764
  variant.probePath
654
765
  ]);
655
766
  /**
@@ -660,9 +771,10 @@ window.__ModuleLoader__.load({
660
771
  * catalog from here.
661
772
  */
662
773
  const control = (0, react.useCallback)(async (action) => {
663
- const key = status.status === "signed-in" ? status.probeKey : void 0;
774
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
664
775
  if (key === void 0) return;
665
776
  setBusy(true);
777
+ const controller = trackController();
666
778
  try {
667
779
  const response = await fetch(variant.probePath, {
668
780
  method: "POST",
@@ -671,6 +783,7 @@ window.__ModuleLoader__.load({
671
783
  "X-WorkBuddy-Probe-Key": key
672
784
  },
673
785
  credentials: "same-origin",
786
+ signal: controller.signal,
674
787
  body: JSON.stringify(action)
675
788
  });
676
789
  const value = await response.json().catch(() => void 0);
@@ -678,19 +791,18 @@ window.__ModuleLoader__.load({
678
791
  const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
679
792
  throw new Error(message);
680
793
  }
681
- await refresh();
794
+ await refresh(controller.signal);
682
795
  } catch (error) {
683
- if (mounted.current) setStatus((previous) => ({
684
- status: "error",
685
- message: error instanceof Error ? error.message : t("requestFailed")
686
- }));
796
+ if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
687
797
  } finally {
798
+ manualControllers.current.delete(controller);
688
799
  if (mounted.current) setBusy(false);
689
800
  }
690
801
  }, [
691
802
  refresh,
692
803
  status,
693
804
  t,
805
+ trackController,
694
806
  variant.probePath
695
807
  ]);
696
808
  /**
@@ -704,7 +816,7 @@ window.__ModuleLoader__.load({
704
816
  });
705
817
  }, [control]);
706
818
  const title = t(variant.titleKey);
707
- const label = 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");
819
+ 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");
708
820
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
709
821
  style: cardStyle,
710
822
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -744,9 +856,10 @@ window.__ModuleLoader__.load({
744
856
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
745
857
  style: statusStyle,
746
858
  role: "status",
859
+ "aria-busy": status === void 0,
747
860
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
748
861
  "aria-hidden": "true",
749
- style: dotStyle(status.status)
862
+ style: dotStyle(status === void 0 ? "loading" : status.status)
750
863
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
751
864
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
752
865
  type: "button",
@@ -758,7 +871,11 @@ window.__ModuleLoader__.load({
758
871
  children: busy ? t("refreshing") : t("refresh")
759
872
  })]
760
873
  }),
761
- status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
874
+ readFailure === void 0 || signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
875
+ style: errorStyle,
876
+ children: t("statusRefreshFailed", { message: readFailure })
877
+ }),
878
+ status?.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
762
879
  status.expiresAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
763
880
  style: bodyStyle,
764
881
  children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
@@ -864,11 +981,11 @@ window.__ModuleLoader__.load({
864
981
  })]
865
982
  })
866
983
  ] }) : null,
867
- status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
984
+ status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
868
985
  style: status.reason === void 0 ? bodyStyle : errorStyle,
869
986
  children: status.reason ?? t(variant.signedOutKey)
870
987
  }) : null,
871
- status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
988
+ status?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
872
989
  style: errorStyle,
873
990
  children: status.message
874
991
  }) : null
@@ -1064,15 +1181,24 @@ window.__ModuleLoader__.load({
1064
1181
  /**
1065
1182
  * The one-line tooltip: current state first, then what a click does — the same
1066
1183
  * two-part shape Fast Mode uses.
1184
+ *
1185
+ * A recorded result outranks a remembered failure. `failed` only means "the last
1186
+ * run from this control did not complete"; the host can record a result for the
1187
+ * same model at any time (a detection started from the settings card, another
1188
+ * conversation, or a finished sweep), and the levels the user paid for are the
1189
+ * more useful answer than the stale failure. Failure copy is what remains when
1190
+ * there is no result to report.
1067
1191
  */
1068
1192
  function tooltipText(t, model, state) {
1069
1193
  if (state.busy) return t("probeRunning", { model });
1070
- if (state.failed) return t("probeTooltipRetry");
1071
1194
  const result = state.result;
1072
- if (result === void 0) return t("probeTooltipIdle", { model });
1073
- if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
1074
- if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1075
- return t("probeTooltipRetry");
1195
+ if (result !== void 0) {
1196
+ if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
1197
+ if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1198
+ return t("probeTooltipRetry");
1199
+ }
1200
+ if (state.failed) return t("probeTooltipRetry");
1201
+ return t("probeTooltipIdle", { model });
1076
1202
  }
1077
1203
  /** Model-independent shell: resolves the selection, then delegates per model. */
1078
1204
  function WorkBuddyProbeControl({ directory, t }) {
@@ -1097,17 +1223,20 @@ window.__ModuleLoader__.load({
1097
1223
  const [note, setNote] = (0, react.useState)();
1098
1224
  const inFlight = (0, react.useRef)(false);
1099
1225
  const mounted = (0, react.useRef)(false);
1226
+ const readSeq = (0, react.useRef)(0);
1100
1227
  const tooltipId = (0, react.useId)();
1101
1228
  const refresh = (0, react.useCallback)(async (signal) => {
1229
+ const seq = ++readSeq.current;
1102
1230
  const response = await fetch(card.statusPath, {
1103
1231
  credentials: "same-origin",
1104
1232
  headers: { accept: "application/json" },
1105
1233
  ...signal === void 0 ? {} : { signal }
1106
1234
  });
1107
1235
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1108
- const value = await response.json();
1109
- if (mounted.current && !signal?.aborted) setStatus(value);
1110
- }, [card.statusPath]);
1236
+ const value = await response.json().catch(() => void 0);
1237
+ if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
1238
+ if (mounted.current && !signal?.aborted && seq === readSeq.current) setStatus(value);
1239
+ }, [card.statusPath, t]);
1111
1240
  (0, react.useEffect)(() => {
1112
1241
  mounted.current = true;
1113
1242
  const controller = new AbortController();
@@ -1128,6 +1257,9 @@ window.__ModuleLoader__.load({
1128
1257
  const key = status?.status === "signed-in" ? status.probeKey : void 0;
1129
1258
  const result = status === void 0 ? void 0 : resultFor(status, model);
1130
1259
  const visible = probe?.candidates.includes(model) === true || result !== void 0;
1260
+ (0, react.useEffect)(() => {
1261
+ if (result !== void 0) setFailed(false);
1262
+ }, [result]);
1131
1263
  (0, react.useEffect)(() => {
1132
1264
  setConfirming(false);
1133
1265
  setNote(void 0);
@@ -1314,6 +1446,7 @@ window.__ModuleLoader__.load({
1314
1446
  creditsDetailHeading: "By package",
1315
1447
  creditsTotal: "Total: {total}",
1316
1448
  percentRemaining: "{percent}% remaining",
1449
+ percentUnknown: "Remaining share unknown",
1317
1450
  exactRemaining: "{remain} / {size} remaining",
1318
1451
  creditPackageUnknownSize: "{remain} remaining",
1319
1452
  creditsError: "Credit unavailable: {message}",
@@ -1327,6 +1460,8 @@ window.__ModuleLoader__.load({
1327
1460
  catalogError: "Last update failed: {message}",
1328
1461
  catalogAppVersion: "App version {version}",
1329
1462
  requestFailed: "Request failed",
1463
+ statusRefreshFailed: "Refresh failed: {message} — showing the last known state",
1464
+ statusResponseInvalid: "WorkBuddy returned an unreadable status reply",
1330
1465
  accountHeading: "Account",
1331
1466
  modelsHeading: "Model offers",
1332
1467
  contextHeading: "Context window",
@@ -1387,6 +1522,7 @@ window.__ModuleLoader__.load({
1387
1522
  creditsDetailHeading: "按套餐",
1388
1523
  creditsTotal: "合计:{total}",
1389
1524
  percentRemaining: "剩余 {percent}%",
1525
+ percentUnknown: "剩余占比未知",
1390
1526
  exactRemaining: "剩余 {remain} / {size}",
1391
1527
  creditPackageUnknownSize: "剩余 {remain}",
1392
1528
  creditsError: "积分查询失败:{message}",
@@ -1400,6 +1536,8 @@ window.__ModuleLoader__.load({
1400
1536
  catalogError: "上次更新失败:{message}",
1401
1537
  catalogAppVersion: "App 版本 {version}",
1402
1538
  requestFailed: "请求失败",
1539
+ statusRefreshFailed: "刷新失败:{message} — 当前显示的是上次成功获取的状态",
1540
+ statusResponseInvalid: "WorkBuddy 返回的状态数据无法识别",
1403
1541
  accountHeading: "账号",
1404
1542
  modelsHeading: "模型优惠",
1405
1543
  contextHeading: "上下文窗口",
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as parseModelCatalog, B as fallbackChatIdentity, C as desktopAuthCandidatesFor, D as classifyUpstreamError, E as WorkBuddyUpstreamClient, F as probeModel, G as appUserAgent, H as resolveChatIdentity, I as randomSentinel, J as resolveAppVersion, K as installedAppVersion, L as CN_APP_VERSION_FILENAME, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as FALLBACK_CN_APP_VERSION, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as validCliVersion, V as readCliVersion, W as WORKBUDDY_APP_VERSION_FILENAME, Y as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as prepareChatBody, k as normalizeCredits, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as readBundleVersion, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as chatUserAgent } from "./variants-CE_2IvEG.js";
1
+ import { A as parseModelCatalog, B as fallbackChatIdentity, C as desktopAuthCandidatesFor, D as classifyUpstreamError, E as WorkBuddyUpstreamClient, F as probeModel, G as appUserAgent, H as resolveChatIdentity, I as randomSentinel, J as resolveAppVersion, K as installedAppVersion, L as CN_APP_VERSION_FILENAME, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as FALLBACK_CN_APP_VERSION, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as validCliVersion, V as readCliVersion, W as WORKBUDDY_APP_VERSION_FILENAME, Y as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as prepareChatBody, k as normalizeCredits, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as readBundleVersion, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as chatUserAgent } from "./variants-CExA7lJt.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
@@ -1636,7 +1636,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
1636
1636
  },
1637
1637
  {
1638
1638
  id: "hy3-x",
1639
- name: "Hy3",
1639
+ name: "Hy3-X",
1640
1640
  contextWindow: 192e3,
1641
1641
  maxTokens: 64e3,
1642
1642
  supportsImages: true,
@@ -2344,7 +2344,7 @@ const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
2344
2344
  const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
2345
2345
  //#endregion
2346
2346
  //#region src/version.ts
2347
- const WORKBUDDY_CONNECT_VERSION = "0.5.1";
2347
+ const WORKBUDDY_CONNECT_VERSION = "0.5.2";
2348
2348
  //#endregion
2349
2349
  //#region src/host-heartbeat.ts
2350
2350
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-connect",
3
3
  "displayName": "DSH WorkBuddy Connect",
4
4
  "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.5.1",
5
+ "version": "0.5.2",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",