dsh-workbuddy-connect 0.6.2 → 0.6.4

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
@@ -30,6 +30,14 @@ 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
+ /**
34
+ * Same-origin route backing the browser's update reminder.
35
+ *
36
+ * Read-only like the status route, so the same loopback Host/Origin gate
37
+ * applies; it answers public npm/GitHub metadata only and never token
38
+ * material.
39
+ */
40
+ const WORKBUDDY_UPDATE_PATH = "/plugins/dsh-workbuddy-connect/update";
33
41
  /** Every reason code, for validation without trusting a wire value. */
34
42
  const SIGNED_OUT_REASON_CODES = [
35
43
  "no-credential",
@@ -163,7 +171,7 @@ window.__ModuleLoader__.load({
163
171
  children: t("assistantHeading")
164
172
  }),
165
173
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
166
- style: bodyStyle,
174
+ style: bodyStyle$1,
167
175
  children: t("assistantIntro")
168
176
  }),
169
177
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -173,7 +181,7 @@ window.__ModuleLoader__.load({
173
181
  children: prompt
174
182
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
175
183
  type: "button",
176
- style: buttonStyle$1,
184
+ style: buttonStyle$2,
177
185
  onClick: () => {
178
186
  (async () => {
179
187
  const ok = await copyPrompt(prompt);
@@ -195,14 +203,14 @@ window.__ModuleLoader__.load({
195
203
  children: t("assistantCopyFailed")
196
204
  }) : null,
197
205
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
198
- style: bodyStyle,
206
+ style: bodyStyle$1,
199
207
  children: t("assistantAfter")
200
208
  }),
201
209
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
202
- style: rowStyle,
210
+ style: rowStyle$1,
203
211
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
204
212
  type: "button",
205
- style: buttonStyle$1,
213
+ style: buttonStyle$2,
206
214
  disabled: busy,
207
215
  onClick: onRecheck,
208
216
  children: busy ? t("assistantRechecking") : t("assistantRecheck")
@@ -259,13 +267,13 @@ window.__ModuleLoader__.load({
259
267
  borderTop: "1px solid var(--dsw-alias-border-l2)",
260
268
  padding: "16px 14px 18px"
261
269
  };
262
- const bodyStyle = {
270
+ const bodyStyle$1 = {
263
271
  margin: 0,
264
272
  fontSize: 14,
265
273
  lineHeight: "22px",
266
274
  color: "var(--dsw-alias-label-secondary)"
267
275
  };
268
- const rowStyle = {
276
+ const rowStyle$1 = {
269
277
  display: "flex",
270
278
  alignItems: "center",
271
279
  justifyContent: "space-between",
@@ -280,7 +288,7 @@ window.__ModuleLoader__.load({
280
288
  fontWeight: 500,
281
289
  color: "var(--dsw-alias-label-primary)"
282
290
  };
283
- const buttonStyle$1 = {
291
+ const buttonStyle$2 = {
284
292
  boxSizing: "border-box",
285
293
  minHeight: 34,
286
294
  padding: "6px 14px",
@@ -293,7 +301,7 @@ window.__ModuleLoader__.load({
293
301
  cursor: "pointer"
294
302
  };
295
303
  const errorStyle = {
296
- ...bodyStyle,
304
+ ...bodyStyle$1,
297
305
  color: "var(--dsw-alias-state-error-primary)"
298
306
  };
299
307
  const quotaListStyle = {
@@ -511,8 +519,8 @@ window.__ModuleLoader__.load({
511
519
  * theme as a pair: `brand-primary` is a light accent here, so pairing it with a
512
520
  * hardcoded white would render white-on-white.
513
521
  */
514
- const primaryButtonStyle$1 = {
515
- ...buttonStyle$1,
522
+ const primaryButtonStyle$2 = {
523
+ ...buttonStyle$2,
516
524
  border: "1px solid var(--dsw-alias-button-primary-fill)",
517
525
  background: "var(--dsw-alias-button-primary-fill)",
518
526
  color: "var(--dsw-alias-label-primary-foreground)"
@@ -580,7 +588,7 @@ window.__ModuleLoader__.load({
580
588
  "aria-valuetext": quotaText
581
589
  }),
582
590
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
583
- style: bodyStyle,
591
+ style: bodyStyle$1,
584
592
  children: quotaText
585
593
  })
586
594
  ]
@@ -612,7 +620,7 @@ window.__ModuleLoader__.load({
612
620
  children: percent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: progressFillStyle(percent) })
613
621
  }),
614
622
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
615
- style: bodyStyle,
623
+ style: bodyStyle$1,
616
624
  children: detail
617
625
  })
618
626
  ]
@@ -710,7 +718,7 @@ window.__ModuleLoader__.load({
710
718
  })]
711
719
  }) : null,
712
720
  visibility === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
713
- style: bodyStyle,
721
+ style: bodyStyle$1,
714
722
  children: t("visibilityIntro")
715
723
  }),
716
724
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -822,19 +830,19 @@ window.__ModuleLoader__.load({
822
830
  children: t("probeHeading")
823
831
  }),
824
832
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
825
- style: bodyStyle,
833
+ style: bodyStyle$1,
826
834
  children: t("probeIntro")
827
835
  }),
828
836
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
829
- style: bodyStyle,
837
+ style: bodyStyle$1,
830
838
  children: t("probeConsentHint")
831
839
  }),
832
840
  probe.running ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
833
- style: bodyStyle,
841
+ style: bodyStyle$1,
834
842
  children: t("probeRunningGeneric")
835
843
  }) : null,
836
844
  probe.candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
837
- style: bodyStyle,
845
+ style: bodyStyle$1,
838
846
  children: t("probeResultEmpty")
839
847
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
840
848
  style: quotaGroupStyle,
@@ -853,7 +861,7 @@ window.__ModuleLoader__.load({
853
861
  children: result.validation === "validating" && result.efforts.length > 0 ? result.efforts.join(" / ") : t(result.validation === "non-validating" ? "probeResultNotValidating" : "probeResultUnknown")
854
862
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
855
863
  type: "button",
856
- style: buttonStyle$1,
864
+ style: buttonStyle$2,
857
865
  disabled: probe.running || busy,
858
866
  onClick: () => {
859
867
  setPending(id);
@@ -869,20 +877,20 @@ window.__ModuleLoader__.load({
869
877
  pending === id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
870
878
  style: confirmBoxStyle,
871
879
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
872
- style: bodyStyle,
880
+ style: bodyStyle$1,
873
881
  children: t("probeConfirmBody", { model: name })
874
882
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
875
883
  style: confirmRowStyle$1,
876
884
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
877
885
  type: "button",
878
- style: buttonStyle$1,
886
+ style: buttonStyle$2,
879
887
  onClick: () => {
880
888
  setPending(void 0);
881
889
  },
882
890
  children: t("cancel")
883
891
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
884
892
  type: "button",
885
- style: primaryButtonStyle$1,
893
+ style: primaryButtonStyle$2,
886
894
  disabled: probe.running || busy,
887
895
  onClick: () => {
888
896
  setRunningModel(id);
@@ -899,7 +907,7 @@ window.__ModuleLoader__.load({
899
907
  }),
900
908
  probe.results.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
901
909
  type: "button",
902
- style: buttonStyle$1,
910
+ style: buttonStyle$2,
903
911
  disabled: busy,
904
912
  onClick: () => {
905
913
  onClear();
@@ -1207,7 +1215,7 @@ window.__ModuleLoader__.load({
1207
1215
  children: t("accountHeading")
1208
1216
  }),
1209
1217
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1210
- style: rowStyle,
1218
+ style: rowStyle$1,
1211
1219
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1212
1220
  style: statusStyle,
1213
1221
  role: "status",
@@ -1218,7 +1226,7 @@ window.__ModuleLoader__.load({
1218
1226
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1219
1227
  }), assistCode === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1220
1228
  type: "button",
1221
- style: buttonStyle$1,
1229
+ style: buttonStyle$2,
1222
1230
  disabled: busy,
1223
1231
  onClick: () => {
1224
1232
  manualRefresh();
@@ -1232,17 +1240,17 @@ window.__ModuleLoader__.load({
1232
1240
  }),
1233
1241
  status?.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1234
1242
  status.expiresAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1235
- style: bodyStyle,
1243
+ style: bodyStyle$1,
1236
1244
  children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
1237
1245
  }),
1238
1246
  status.catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
- style: rowStyle,
1247
+ style: rowStyle$1,
1240
1248
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1241
- style: bodyStyle,
1249
+ style: bodyStyle$1,
1242
1250
  children: [status.catalog.source === "live" && status.catalog.fetchedAt !== void 0 ? t("catalogLive", { time: formatTime(status.catalog.fetchedAt) }) : status.catalog.source === "saved" && status.catalog.fetchedAt !== void 0 ? t("catalogSaved", { time: formatTime(status.catalog.fetchedAt) }) : t("catalogFallback"), status.catalog.appVersion === void 0 ? "" : ` · ${t("catalogAppVersion", { version: status.catalog.appVersion })}`]
1243
1251
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1244
1252
  type: "button",
1245
- style: buttonStyle$1,
1253
+ style: buttonStyle$2,
1246
1254
  disabled: busy,
1247
1255
  onClick: () => {
1248
1256
  refreshModels();
@@ -1281,12 +1289,12 @@ window.__ModuleLoader__.load({
1281
1289
  status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1282
1290
  style: quotaListStyle,
1283
1291
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1284
- style: rowStyle,
1292
+ style: rowStyle$1,
1285
1293
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1286
1294
  style: quotaTitleStyle,
1287
1295
  children: t("creditsHeading")
1288
1296
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1289
- style: bodyStyle,
1297
+ style: bodyStyle$1,
1290
1298
  children: status.credits.unlimited === true ? t("creditsTotalUnlimited") : t("creditsTotal", { total: formatNumber(status.credits.total) })
1291
1299
  })]
1292
1300
  }), status.credits.cycleResetTime === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1363,7 +1371,7 @@ window.__ModuleLoader__.load({
1363
1371
  })
1364
1372
  ] }) : null,
1365
1373
  status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1366
- style: status.reason === void 0 ? bodyStyle : errorStyle,
1374
+ style: status.reason === void 0 ? bodyStyle$1 : errorStyle,
1367
1375
  children: status.reason ?? t(variant.signedOutKey)
1368
1376
  }), assistCode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistBlock, {
1369
1377
  t,
@@ -1424,7 +1432,7 @@ window.__ModuleLoader__.load({
1424
1432
  transform: "translateY(2px)",
1425
1433
  marginRight: -8
1426
1434
  };
1427
- const buttonStyle = {
1435
+ const buttonStyle$1 = {
1428
1436
  display: "inline-flex",
1429
1437
  alignItems: "center",
1430
1438
  justifyContent: "center",
@@ -1509,7 +1517,7 @@ window.__ModuleLoader__.load({
1509
1517
  * theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
1510
1518
  * the same job.
1511
1519
  */
1512
- const primaryButtonStyle = {
1520
+ const primaryButtonStyle$1 = {
1513
1521
  ...confirmButtonStyle,
1514
1522
  border: "1px solid var(--dsw-alias-button-primary-fill)",
1515
1523
  background: "var(--dsw-alias-button-primary-fill)",
@@ -1727,7 +1735,7 @@ window.__ModuleLoader__.load({
1727
1735
  setTooltipVisible(false);
1728
1736
  },
1729
1737
  style: {
1730
- ...buttonStyle,
1738
+ ...buttonStyle$1,
1731
1739
  opacity: disabled && !confirming ? .6 : 1,
1732
1740
  cursor: disabled ? "default" : "pointer"
1733
1741
  },
@@ -1782,7 +1790,7 @@ window.__ModuleLoader__.load({
1782
1790
  children: t("cancel")
1783
1791
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1784
1792
  type: "button",
1785
- style: primaryButtonStyle,
1793
+ style: primaryButtonStyle$1,
1786
1794
  onClick: () => {
1787
1795
  detect();
1788
1796
  },
@@ -1813,6 +1821,748 @@ window.__ModuleLoader__.load({
1813
1821
  return t("probeNoteUnknown");
1814
1822
  }
1815
1823
  //#endregion
1824
+ //#region src/update.ts
1825
+ /**
1826
+ * Public update metadata and bounded version checking for WorkBuddy Connect.
1827
+ *
1828
+ * Two upstreams, strictly layered so the judgement never depends on the
1829
+ * enrichment: npm's dist-tags decide whether a newer release exists, and only
1830
+ * then is GitHub's release list consulted — one request that feeds both the
1831
+ * "versions behind" count and the per-release notes. Every upstream answer is
1832
+ * treated as untrusted input: bodies are size-capped while streaming, releases
1833
+ * are re-validated on the browser side by {@link parseWorkBuddyUpdateResult}
1834
+ * before anything renders.
1835
+ *
1836
+ * This module is host/browser shared and dependency-free by design (the client
1837
+ * bundle's runtime imports stay React + local modules only).
1838
+ *
1839
+ * @module dsh-workbuddy-connect/update
1840
+ */
1841
+ const WORKBUDDY_REPOSITORY_URL = "https://github.com/corrinehu/dsh-workbuddy-connect";
1842
+ `${WORKBUDDY_REPOSITORY_URL.replace("https://github.com", "https://api.github.com/repos")}`;
1843
+ const WORKBUDDY_RELEASE_PAGE_BASE = `${WORKBUDDY_REPOSITORY_URL}/releases/tag/`;
1844
+ /** GitHub caps one page at 100 releases; ours is far below that today. */
1845
+ const RELEASES_LIST_MAX = 100;
1846
+ const RELEASE_NAME_MAX_CHARS = 200;
1847
+ const RELEASE_NOTES_MAX_CHARS = 16e3;
1848
+ /** Parse one exact SemVer version, accepting the conventional leading `v`. */
1849
+ function parseWorkBuddyVersion(raw) {
1850
+ if (typeof raw !== "string") return void 0;
1851
+ const normalized = raw.startsWith("v") ? raw.slice(1) : raw;
1852
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(normalized);
1853
+ if (match === null) return void 0;
1854
+ const rawPrerelease = match[4] === void 0 ? [] : match[4].split(".");
1855
+ if (rawPrerelease.some((identifier) => /^\d+$/u.test(identifier) && !/^(0|[1-9]\d*)$/u.test(identifier))) return void 0;
1856
+ const prerelease = rawPrerelease.map((identifier) => /^(0|[1-9]\d*)$/u.test(identifier) ? Number(identifier) : identifier);
1857
+ if (prerelease.some((identifier) => typeof identifier === "number" && !Number.isSafeInteger(identifier))) return void 0;
1858
+ const parsed = {
1859
+ major: Number(match[1]),
1860
+ minor: Number(match[2]),
1861
+ patch: Number(match[3]),
1862
+ prerelease
1863
+ };
1864
+ return [
1865
+ parsed.major,
1866
+ parsed.minor,
1867
+ parsed.patch
1868
+ ].every(Number.isSafeInteger) ? parsed : void 0;
1869
+ }
1870
+ /**
1871
+ * One canonical spelling per SemVer value: leading `v` and build metadata
1872
+ * fold away, so `v0.6.4`, `0.6.4`, and `0.6.4+build` dedupe as one release.
1873
+ * Returns `undefined` for unparseable input; callers drop those first.
1874
+ */
1875
+ function canonicalWorkBuddyVersion(version) {
1876
+ const parsed = parseWorkBuddyVersion(version);
1877
+ if (parsed === void 0) return void 0;
1878
+ return `${String(parsed.major)}.${String(parsed.minor)}.${String(parsed.patch)}` + (parsed.prerelease.length === 0 ? "" : `-${parsed.prerelease.join(".")}`);
1879
+ }
1880
+ function compareIdentifiers(left, right) {
1881
+ if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
1882
+ if (typeof left === "number") return -1;
1883
+ if (typeof right === "number") return 1;
1884
+ return left < right ? -1 : left > right ? 1 : 0;
1885
+ }
1886
+ /** Compare two versions using SemVer precedence (build metadata ignored). */
1887
+ function compareWorkBuddyVersions(left, right) {
1888
+ const a = parseWorkBuddyVersion(left);
1889
+ const b = parseWorkBuddyVersion(right);
1890
+ if (a === void 0 || b === void 0) throw new TypeError("invalid WorkBuddy Connect version");
1891
+ for (const [aPart, bPart] of [
1892
+ [a.major, b.major],
1893
+ [a.minor, b.minor],
1894
+ [a.patch, b.patch]
1895
+ ]) if (aPart !== bPart) return aPart < bPart ? -1 : 1;
1896
+ if (a.prerelease.length === 0 && b.prerelease.length !== 0) return 1;
1897
+ if (a.prerelease.length !== 0 && b.prerelease.length === 0) return -1;
1898
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
1899
+ const aPart = a.prerelease[index];
1900
+ const bPart = b.prerelease[index];
1901
+ if (aPart === void 0) return -1;
1902
+ if (bPart === void 0) return 1;
1903
+ const comparison = compareIdentifiers(aPart, bPart);
1904
+ if (comparison !== 0) return comparison;
1905
+ }
1906
+ return 0;
1907
+ }
1908
+ /** Strip control characters, normalize newlines, and cap the length. */
1909
+ function cleanReleaseText(value, maxLength) {
1910
+ if (typeof value !== "string" || value.length === 0) return void 0;
1911
+ const clean = value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, "").replace(/\r\n?/gu, "\n").trim().slice(0, maxLength);
1912
+ return clean.length === 0 ? void 0 : clean;
1913
+ }
1914
+ function cleanPublishedAt(value) {
1915
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/iu.test(value) ? value.slice(0, 64) : void 0;
1916
+ }
1917
+ function releasePageUrl(version) {
1918
+ return `${WORKBUDDY_RELEASE_PAGE_BASE}v${version}`;
1919
+ }
1920
+ /**
1921
+ * Validate a route response before the browser renders it. Same distrust as
1922
+ * the host side: the versions must parse, `update-available` must still hold
1923
+ * under a fresh comparison, the release URL must equal the one derived from
1924
+ * the version, and every listed release must fall in (current, latest] — a
1925
+ * host that invents content fails closed here.
1926
+ */
1927
+ function parseWorkBuddyUpdateResult(value) {
1928
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1929
+ const record = value;
1930
+ const currentVersion = record["currentVersion"];
1931
+ if (typeof currentVersion !== "string" || parseWorkBuddyVersion(currentVersion) === void 0) return void 0;
1932
+ if (record["status"] === "unavailable") {
1933
+ const reason = record["reason"];
1934
+ return reason === "invalid-current-version" || reason === "registry-unavailable" || reason === "invalid-registry-response" ? {
1935
+ status: "unavailable",
1936
+ currentVersion,
1937
+ reason
1938
+ } : void 0;
1939
+ }
1940
+ const latestVersion = record["latestVersion"];
1941
+ if (typeof latestVersion !== "string" || parseWorkBuddyVersion(latestVersion) === void 0) return void 0;
1942
+ if (record["status"] === "up-to-date") return {
1943
+ status: "up-to-date",
1944
+ currentVersion,
1945
+ latestVersion
1946
+ };
1947
+ if (record["status"] !== "update-available" || compareWorkBuddyVersions(latestVersion, currentVersion) <= 0) return void 0;
1948
+ const expectedUrl = releasePageUrl(latestVersion);
1949
+ if (record["releaseUrl"] !== expectedUrl) return void 0;
1950
+ if (!Array.isArray(record["releases"]) || record["releases"].length > RELEASES_LIST_MAX) return void 0;
1951
+ const seen = /* @__PURE__ */ new Set();
1952
+ const releases = [];
1953
+ for (const raw of record["releases"]) {
1954
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
1955
+ const entry = raw;
1956
+ const version = entry["version"];
1957
+ if (typeof version !== "string" || parseWorkBuddyVersion(version) === void 0) return void 0;
1958
+ if (compareWorkBuddyVersions(version, currentVersion) <= 0 || compareWorkBuddyVersions(version, latestVersion) > 0) return void 0;
1959
+ const canonical = canonicalWorkBuddyVersion(version);
1960
+ if (canonical === void 0 || seen.has(canonical)) return void 0;
1961
+ seen.add(canonical);
1962
+ const name = cleanReleaseText(entry["name"], RELEASE_NAME_MAX_CHARS);
1963
+ const notes = cleanReleaseText(entry["notes"], RELEASE_NOTES_MAX_CHARS);
1964
+ const publishedAt = cleanPublishedAt(entry["publishedAt"]);
1965
+ releases.push({
1966
+ version,
1967
+ ...name === void 0 ? {} : { name },
1968
+ ...notes === void 0 ? {} : { notes },
1969
+ ...publishedAt === void 0 ? {} : { publishedAt }
1970
+ });
1971
+ }
1972
+ const rawVersionsBehind = record["versionsBehind"];
1973
+ if (rawVersionsBehind !== void 0 && (typeof rawVersionsBehind !== "number" || !Number.isSafeInteger(rawVersionsBehind) || rawVersionsBehind !== releases.length)) return void 0;
1974
+ return {
1975
+ status: "update-available",
1976
+ currentVersion,
1977
+ latestVersion,
1978
+ releaseUrl: expectedUrl,
1979
+ releases,
1980
+ ...rawVersionsBehind === void 0 ? {} : { versionsBehind: rawVersionsBehind }
1981
+ };
1982
+ }
1983
+ //#endregion
1984
+ //#region src/client/WorkBuddyUpdateNotice.tsx
1985
+ /**
1986
+ * The bottom-right update reminder: one floating panel, rendered only while a
1987
+ * newer release exists and has not been dismissed. Each in-range release
1988
+ * shows as a collapsed title row (our release titles are written as bilingual
1989
+ * user-facing summaries, so the row doubles as the highlight line); opening
1990
+ * one reveals its notes under the markdown whitelist below.
1991
+ */
1992
+ const DEFAULT_T = (key, _params) => String(key);
1993
+ const panelStyle = {
1994
+ display: "flex",
1995
+ flexDirection: "column",
1996
+ gap: 10,
1997
+ padding: "13px 15px",
1998
+ border: "1px solid var(--dsw-alias-border-l2)",
1999
+ borderRadius: 12,
2000
+ background: "var(--dsw-alias-bg-module-platform)",
2001
+ color: "var(--dsw-alias-label-primary)"
2002
+ };
2003
+ const overlayStyle = {
2004
+ position: "fixed",
2005
+ bottom: 16,
2006
+ right: 20,
2007
+ zIndex: 30,
2008
+ width: "min(440px, calc(100vw - 40px))",
2009
+ pointerEvents: "auto",
2010
+ maxHeight: "calc(100vh - 32px)",
2011
+ overflowY: "auto",
2012
+ boxSizing: "border-box",
2013
+ boxShadow: "0 8px 28px rgba(0, 0, 0, 0.16)"
2014
+ };
2015
+ const rowStyle = {
2016
+ display: "flex",
2017
+ alignItems: "center",
2018
+ justifyContent: "space-between",
2019
+ gap: 12,
2020
+ flexWrap: "wrap"
2021
+ };
2022
+ const titleStyle = {
2023
+ margin: 0,
2024
+ fontSize: 14,
2025
+ lineHeight: "20px",
2026
+ fontWeight: 600
2027
+ };
2028
+ const bodyStyle = {
2029
+ margin: 0,
2030
+ color: "var(--dsw-alias-label-secondary)",
2031
+ fontSize: 13,
2032
+ lineHeight: "20px"
2033
+ };
2034
+ const sectionStyle = {
2035
+ display: "flex",
2036
+ flexDirection: "column",
2037
+ gap: 6
2038
+ };
2039
+ const releaseRowStyle = {
2040
+ display: "flex",
2041
+ alignItems: "flex-start",
2042
+ gap: 8,
2043
+ padding: "7px 9px",
2044
+ borderRadius: 7,
2045
+ background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04))",
2046
+ border: 0,
2047
+ width: "100%",
2048
+ boxSizing: "border-box",
2049
+ color: "inherit",
2050
+ font: "inherit",
2051
+ fontSize: 13,
2052
+ lineHeight: "19px",
2053
+ textAlign: "left",
2054
+ cursor: "pointer",
2055
+ overflowWrap: "anywhere"
2056
+ };
2057
+ const buttonStyle = {
2058
+ display: "inline-flex",
2059
+ alignItems: "center",
2060
+ justifyContent: "center",
2061
+ boxSizing: "border-box",
2062
+ minHeight: 32,
2063
+ padding: "4px 11px",
2064
+ border: "1px solid var(--dsw-alias-border-l2)",
2065
+ borderRadius: 7,
2066
+ background: "var(--dsw-alias-bg-layer-1)",
2067
+ color: "var(--dsw-alias-label-primary)",
2068
+ font: "inherit",
2069
+ fontSize: 12,
2070
+ lineHeight: "20px",
2071
+ whiteSpace: "nowrap",
2072
+ cursor: "pointer"
2073
+ };
2074
+ const primaryButtonStyle = {
2075
+ ...buttonStyle,
2076
+ borderColor: "var(--dsw-alias-button-primary-fill)",
2077
+ background: "var(--dsw-alias-button-primary-fill)",
2078
+ color: "var(--dsw-alias-label-primary-foreground)"
2079
+ };
2080
+ const textButtonStyle = {
2081
+ border: 0,
2082
+ padding: 0,
2083
+ background: "transparent",
2084
+ color: "var(--dsw-alias-brand-primary)",
2085
+ font: "inherit",
2086
+ fontSize: 12,
2087
+ lineHeight: "20px",
2088
+ cursor: "pointer",
2089
+ textDecoration: "underline",
2090
+ textUnderlineOffset: 2
2091
+ };
2092
+ const promptRowStyle = {
2093
+ display: "flex",
2094
+ alignItems: "center",
2095
+ gap: 8,
2096
+ padding: "7px 8px 7px 10px",
2097
+ borderRadius: 7,
2098
+ background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.06))"
2099
+ };
2100
+ const promptTextStyle = {
2101
+ flex: "1 1 auto",
2102
+ minWidth: 0,
2103
+ margin: 0,
2104
+ padding: 0,
2105
+ background: "transparent",
2106
+ color: "var(--dsw-alias-label-primary)",
2107
+ fontSize: 12,
2108
+ lineHeight: "19px",
2109
+ whiteSpace: "pre-wrap",
2110
+ overflowWrap: "anywhere"
2111
+ };
2112
+ const notesStyle = {
2113
+ maxHeight: 220,
2114
+ overflowY: "auto",
2115
+ margin: "4px 0 0",
2116
+ padding: "9px 10px",
2117
+ borderRadius: 7,
2118
+ background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.06))",
2119
+ color: "var(--dsw-alias-label-secondary)",
2120
+ fontSize: 12,
2121
+ lineHeight: "19px",
2122
+ overflowWrap: "anywhere"
2123
+ };
2124
+ const notesListStyle = {
2125
+ margin: "4px 0",
2126
+ paddingLeft: 18
2127
+ };
2128
+ const notesHeadingStyle = {
2129
+ margin: "0 0 4px",
2130
+ fontSize: 12,
2131
+ lineHeight: "19px",
2132
+ fontWeight: 600,
2133
+ color: "var(--dsw-alias-label-primary)"
2134
+ };
2135
+ async function copyAgentPrompt(prompt) {
2136
+ try {
2137
+ if (navigator.clipboard?.writeText === void 0) return false;
2138
+ await navigator.clipboard.writeText(prompt);
2139
+ return true;
2140
+ } catch {
2141
+ return false;
2142
+ }
2143
+ }
2144
+ /** Only same-origin GitHub links may render as anchors; everything else is text. */
2145
+ function safeReleaseUrl(value) {
2146
+ try {
2147
+ const url = new URL(value);
2148
+ return url.protocol === "https:" && url.hostname === "github.com" ? url.href : void 0;
2149
+ } catch {
2150
+ return;
2151
+ }
2152
+ }
2153
+ function renderInlineMarkdown(text, keyPrefix) {
2154
+ const tokens = /(?:\*\*[^*]+\*\*|\[[^\]]+\]\(https:\/\/[^)\s]+\)|https:\/\/[^\s<]+)/gu;
2155
+ const children = [];
2156
+ let lastIndex = 0;
2157
+ let match;
2158
+ let tokenIndex = 0;
2159
+ while ((match = tokens.exec(text)) !== null) {
2160
+ if (match.index > lastIndex) children.push(text.slice(lastIndex, match.index));
2161
+ const token = match[0];
2162
+ const bold = /^\*\*([^*]+)\*\*$/u.exec(token);
2163
+ const markdownLink = /^\[([^\]]+)\]\((https:\/\/[^)\s]+)\)$/u.exec(token);
2164
+ const bareUrl = /^https:\/\/[^\s<]+$/u.test(token) ? token.replace(/[.,]$/u, "") : void 0;
2165
+ if (bold !== null) children.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: bold[1] ?? "" }, `${keyPrefix}-bold-${tokenIndex}`));
2166
+ else if (markdownLink !== null) {
2167
+ const label = markdownLink[1] ?? token;
2168
+ const href = markdownLink[2] === void 0 ? void 0 : safeReleaseUrl(markdownLink[2]);
2169
+ children.push(href === void 0 ? label : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2170
+ href,
2171
+ target: "_blank",
2172
+ rel: "noopener noreferrer",
2173
+ children: label
2174
+ }, `${keyPrefix}-link-${tokenIndex}`));
2175
+ } else if (bareUrl !== void 0) {
2176
+ const href = safeReleaseUrl(bareUrl);
2177
+ children.push(href === void 0 ? token : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2178
+ href,
2179
+ target: "_blank",
2180
+ rel: "noopener noreferrer",
2181
+ children: token
2182
+ }, `${keyPrefix}-url-${tokenIndex}`));
2183
+ } else children.push(token);
2184
+ lastIndex = match.index + token.length;
2185
+ tokenIndex += 1;
2186
+ }
2187
+ if (lastIndex < text.length) children.push(text.slice(lastIndex));
2188
+ return children;
2189
+ }
2190
+ function renderReleaseNotes(markdown) {
2191
+ const content = [];
2192
+ let bullets = [];
2193
+ const flushBullets = () => {
2194
+ if (bullets.length === 0) return;
2195
+ content.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
2196
+ style: notesListStyle,
2197
+ children: bullets
2198
+ }, `list-${content.length}`));
2199
+ bullets = [];
2200
+ };
2201
+ markdown.split("\n").forEach((line, index) => {
2202
+ const trimmed = line.trim();
2203
+ const bullet = /^[-*]\s+(.+)$/u.exec(trimmed);
2204
+ const heading = /^#{1,6}\s+(.+)$/u.exec(trimmed);
2205
+ if (bullet !== null) bullets.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: renderInlineMarkdown(bullet[1] ?? "", `item-${index}`) }, `item-${index}`));
2206
+ else if (heading !== null) {
2207
+ flushBullets();
2208
+ content.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
2209
+ style: notesHeadingStyle,
2210
+ children: renderInlineMarkdown(heading[1] ?? "", `heading-${index}`)
2211
+ }, `heading-${index}`));
2212
+ } else if (trimmed !== "") {
2213
+ flushBullets();
2214
+ content.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2215
+ style: {
2216
+ ...bodyStyle,
2217
+ fontSize: 12,
2218
+ lineHeight: "19px"
2219
+ },
2220
+ children: renderInlineMarkdown(trimmed, `paragraph-${index}`)
2221
+ }, `paragraph-${index}`));
2222
+ } else flushBullets();
2223
+ });
2224
+ flushBullets();
2225
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2226
+ style: notesStyle,
2227
+ children: content
2228
+ });
2229
+ }
2230
+ /** One in-range release: a collapsed title row that opens its notes. */
2231
+ function ReleaseRow({ release, t }) {
2232
+ const [open, setOpen] = (0, react.useState)(false);
2233
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2234
+ type: "button",
2235
+ style: releaseRowStyle,
2236
+ "aria-expanded": open,
2237
+ onClick: () => {
2238
+ setOpen(!open);
2239
+ },
2240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2241
+ "aria-hidden": true,
2242
+ children: open ? "▾" : "▸"
2243
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2244
+ style: {
2245
+ flex: "1 1 auto",
2246
+ minWidth: 0
2247
+ },
2248
+ children: release.name ?? `v${release.version}`
2249
+ })]
2250
+ }), open ? release.notes === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2251
+ style: {
2252
+ ...bodyStyle,
2253
+ fontSize: 12,
2254
+ padding: "4px 9px 0",
2255
+ margin: 0
2256
+ },
2257
+ children: t("releaseNotesUnavailable")
2258
+ }) : renderReleaseNotes(release.notes) : null] });
2259
+ }
2260
+ /** Bottom-right reminder registered in the shell.overlay list seat. */
2261
+ function WorkBuddyUpdateOverlay({ t = DEFAULT_T, updater }) {
2262
+ if (updater === void 0) return null;
2263
+ const snapshot = (0, react.useSyncExternalStore)(updater.subscribe, updater.getSnapshot, updater.getSnapshot);
2264
+ const latestVersion = snapshot.latestVersion;
2265
+ const [copied, setCopied] = (0, react.useState)(false);
2266
+ const [copyFailed, setCopyFailed] = (0, react.useState)(false);
2267
+ const [recheckRequested, setRecheckRequested] = (0, react.useState)(false);
2268
+ const noticeKey = latestVersion === void 0 ? void 0 : `${snapshot.currentVersion}:${latestVersion}`;
2269
+ if (!(recheckRequested && (snapshot.status === "checking" || snapshot.status === "unavailable")) && (snapshot.status !== "update-available" || noticeKey === void 0 || snapshot.dismissedNotice === noticeKey)) return null;
2270
+ const available = snapshot.status === "update-available";
2271
+ const releases = snapshot.releases ?? [];
2272
+ const agentPrompt = t("agentUpgradePrompt", { repository: WORKBUDDY_REPOSITORY_URL });
2273
+ const copy = async () => {
2274
+ setCopyFailed(false);
2275
+ const ok = await copyAgentPrompt(agentPrompt);
2276
+ setCopied(ok);
2277
+ setCopyFailed(!ok);
2278
+ };
2279
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2280
+ style: {
2281
+ ...panelStyle,
2282
+ ...overlayStyle
2283
+ },
2284
+ role: "status",
2285
+ "aria-label": t("updateNoticeLabel"),
2286
+ children: [
2287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2288
+ style: rowStyle,
2289
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
2290
+ style: titleStyle,
2291
+ children: available ? t("newVersionAvailable", { version: latestVersion }) : t("updateNoticeLabel")
2292
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2293
+ type: "button",
2294
+ style: buttonStyle,
2295
+ "aria-label": t("dismissUpdate"),
2296
+ onClick: () => {
2297
+ setRecheckRequested(false);
2298
+ if (noticeKey !== void 0) updater.dismiss(noticeKey);
2299
+ },
2300
+ children: t("dismissUpdate")
2301
+ })]
2302
+ }),
2303
+ available ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2304
+ style: bodyStyle,
2305
+ children: [t("versionSummary", {
2306
+ current: snapshot.currentVersion,
2307
+ latest: latestVersion
2308
+ }), snapshot.versionsBehind === void 0 ? ` · ${t("versionsBehindUnknown")}` : ` · ${t("versionsBehind", { count: snapshot.versionsBehind })}`]
2309
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2310
+ style: sectionStyle,
2311
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
2312
+ style: titleStyle,
2313
+ children: t("releasesHeading")
2314
+ }), releases.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2315
+ style: bodyStyle,
2316
+ children: t("releaseListUnavailable")
2317
+ }) : releases.map((release) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReleaseRow, {
2318
+ release,
2319
+ t
2320
+ }, release.version))]
2321
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2322
+ style: bodyStyle,
2323
+ children: t("updateCheckUnavailable")
2324
+ }),
2325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2326
+ style: sectionStyle,
2327
+ children: [
2328
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2329
+ style: promptRowStyle,
2330
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2331
+ style: promptTextStyle,
2332
+ children: agentPrompt
2333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2334
+ type: "button",
2335
+ style: buttonStyle,
2336
+ onClick: () => {
2337
+ copy();
2338
+ },
2339
+ children: copied ? t("agentPromptCopied") : t("copyForAgent")
2340
+ })]
2341
+ }),
2342
+ copyFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2343
+ style: {
2344
+ ...bodyStyle,
2345
+ fontSize: 12,
2346
+ margin: 0
2347
+ },
2348
+ children: t("agentPromptCopyFailed")
2349
+ }) : null,
2350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2351
+ style: rowStyle,
2352
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2353
+ type: "button",
2354
+ style: primaryButtonStyle,
2355
+ disabled: !available,
2356
+ onClick: () => {
2357
+ setRecheckRequested(true);
2358
+ updater.refresh(true);
2359
+ },
2360
+ children: snapshot.status === "checking" ? t("checkingForUpdates") : t("recheckAfterUpgrade")
2361
+ }), snapshot.releaseUrl === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2362
+ href: snapshot.releaseUrl,
2363
+ target: "_blank",
2364
+ rel: "noopener noreferrer",
2365
+ style: textButtonStyle,
2366
+ children: t("openReleasePage")
2367
+ })]
2368
+ }),
2369
+ recheckRequested && snapshot.status === "update-available" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2370
+ style: {
2371
+ ...bodyStyle,
2372
+ fontSize: 12,
2373
+ margin: 0
2374
+ },
2375
+ children: t("upgradeStillAvailable", { version: snapshot.currentVersion })
2376
+ }) : null
2377
+ ]
2378
+ })
2379
+ ]
2380
+ });
2381
+ }
2382
+ //#endregion
2383
+ //#region src/client/update-store.ts
2384
+ /**
2385
+ * Browser-owned cache and observable state for the update reminder.
2386
+ *
2387
+ * One reminder for the whole bundle — the check compares this npm package's
2388
+ * own version, so both provider cards share it. The cache is localStorage
2389
+ * with a 7-day TTL and the current version embedded, so an upgrade
2390
+ * invalidates it by itself. `unavailable` answers are never cached: the next
2391
+ * mount retries, and one in-page retry is scheduled after five minutes while
2392
+ * the page stays open.
2393
+ */
2394
+ const WORKBUDDY_UPDATE_CACHE_KEY = "dsh-workbuddy-connect:update-check";
2395
+ const WORKBUDDY_UPDATE_DISMISSED_KEY = "dsh-workbuddy-connect:update-dismissed";
2396
+ /** Retry transient update-check failures while the page remains open. */
2397
+ const WORKBUDDY_UPDATE_RECHECK_MS = 3e5;
2398
+ /** The reminder's own fetch deadline, independent of the host's upstream one. */
2399
+ const ROUTE_TIMEOUT_MS = 3e4;
2400
+ function storage() {
2401
+ try {
2402
+ return typeof localStorage === "undefined" ? void 0 : localStorage;
2403
+ } catch {
2404
+ return;
2405
+ }
2406
+ }
2407
+ function resultSnapshot(result, dismissedNotice) {
2408
+ return {
2409
+ status: result.status,
2410
+ currentVersion: result.currentVersion,
2411
+ ...result.status === "up-to-date" || result.status === "update-available" ? { latestVersion: result.latestVersion } : {},
2412
+ ...result.status === "update-available" ? {
2413
+ releaseUrl: result.releaseUrl,
2414
+ releases: result.releases,
2415
+ ...result.versionsBehind === void 0 ? {} : { versionsBehind: result.versionsBehind }
2416
+ } : {},
2417
+ ...dismissedNotice === void 0 ? {} : { dismissedNotice }
2418
+ };
2419
+ }
2420
+ /** Observable browser state behind the bottom-right reminder. */
2421
+ var WorkBuddyUpdateStore = class {
2422
+ currentVersion;
2423
+ snapshot;
2424
+ listeners = /* @__PURE__ */ new Set();
2425
+ request;
2426
+ disposed = false;
2427
+ recheckTimer;
2428
+ constructor(currentVersion) {
2429
+ this.currentVersion = currentVersion;
2430
+ this.snapshot = {
2431
+ status: "idle",
2432
+ currentVersion
2433
+ };
2434
+ }
2435
+ getSnapshot = () => this.snapshot;
2436
+ subscribe = (listener) => {
2437
+ this.listeners.add(listener);
2438
+ return () => {
2439
+ this.listeners.delete(listener);
2440
+ };
2441
+ };
2442
+ setSnapshot(next) {
2443
+ if (this.disposed) return;
2444
+ this.snapshot = next;
2445
+ for (const listener of this.listeners) listener();
2446
+ }
2447
+ dismissedNotice() {
2448
+ try {
2449
+ const value = storage()?.getItem(WORKBUDDY_UPDATE_DISMISSED_KEY);
2450
+ return value === null || value === "" ? void 0 : value;
2451
+ } catch {
2452
+ return;
2453
+ }
2454
+ }
2455
+ readCached() {
2456
+ try {
2457
+ const raw = storage()?.getItem(WORKBUDDY_UPDATE_CACHE_KEY);
2458
+ if (raw === null || raw === void 0) return void 0;
2459
+ const cached = JSON.parse(raw);
2460
+ if (!Number.isSafeInteger(cached.checkedAt) || cached.checkedAt > Date.now() || Date.now() - cached.checkedAt > 6048e5) return void 0;
2461
+ const result = parseWorkBuddyUpdateResult(cached.result);
2462
+ if (result === void 0 || result.status === "unavailable") return void 0;
2463
+ return {
2464
+ result,
2465
+ checkedAt: cached.checkedAt
2466
+ };
2467
+ } catch {
2468
+ return;
2469
+ }
2470
+ }
2471
+ writeCached(result, checkedAt) {
2472
+ try {
2473
+ if (result.status === "unavailable") storage()?.removeItem(WORKBUDDY_UPDATE_CACHE_KEY);
2474
+ else storage()?.setItem(WORKBUDDY_UPDATE_CACHE_KEY, JSON.stringify({
2475
+ checkedAt,
2476
+ result
2477
+ }));
2478
+ } catch {}
2479
+ }
2480
+ acceptResult(result) {
2481
+ if (this.disposed) return;
2482
+ const checkedAt = Date.now();
2483
+ this.writeCached(result, checkedAt);
2484
+ const next = resultSnapshot(result, this.dismissedNotice());
2485
+ if (result.status === "unavailable" && this.snapshot.latestVersion !== void 0) next.latestVersion = this.snapshot.latestVersion;
2486
+ this.setSnapshot({
2487
+ ...next,
2488
+ checkedAt
2489
+ });
2490
+ if (result.status === "unavailable") this.recheckTimer = setTimeout(() => {
2491
+ this.refresh(true);
2492
+ }, WORKBUDDY_UPDATE_RECHECK_MS);
2493
+ }
2494
+ /** Reuse a result for a week; force bypasses the cache. */
2495
+ async refresh(force = false) {
2496
+ if (this.disposed || this.request !== void 0) return;
2497
+ clearTimeout(this.recheckTimer);
2498
+ this.recheckTimer = void 0;
2499
+ const controller = new AbortController();
2500
+ this.request = controller;
2501
+ const timer = setTimeout(() => {
2502
+ controller.abort(/* @__PURE__ */ new Error("update route timed out"));
2503
+ }, ROUTE_TIMEOUT_MS);
2504
+ this.setSnapshot({
2505
+ status: "checking",
2506
+ currentVersion: this.currentVersion,
2507
+ ...this.snapshot.latestVersion === void 0 ? {} : { latestVersion: this.snapshot.latestVersion },
2508
+ ...this.snapshot.dismissedNotice === void 0 ? {} : { dismissedNotice: this.snapshot.dismissedNotice }
2509
+ });
2510
+ try {
2511
+ if (!force) {
2512
+ const cached = this.readCached();
2513
+ if (cached !== void 0 && cached.result.currentVersion === this.currentVersion) {
2514
+ this.setSnapshot({
2515
+ ...resultSnapshot(cached.result, this.dismissedNotice()),
2516
+ checkedAt: cached.checkedAt
2517
+ });
2518
+ return;
2519
+ }
2520
+ }
2521
+ const response = await fetch(WORKBUDDY_UPDATE_PATH, {
2522
+ method: "GET",
2523
+ headers: { accept: "application/json" },
2524
+ credentials: "same-origin",
2525
+ signal: controller.signal
2526
+ });
2527
+ const value = await response.json().catch(() => void 0);
2528
+ const result = response.ok ? parseWorkBuddyUpdateResult(value) : void 0;
2529
+ this.acceptResult(result ?? {
2530
+ status: "unavailable",
2531
+ currentVersion: this.currentVersion,
2532
+ reason: "registry-unavailable"
2533
+ });
2534
+ } catch {
2535
+ if (!this.disposed) this.acceptResult({
2536
+ status: "unavailable",
2537
+ currentVersion: this.currentVersion,
2538
+ reason: "registry-unavailable"
2539
+ });
2540
+ } finally {
2541
+ clearTimeout(timer);
2542
+ if (this.request === controller) this.request = void 0;
2543
+ }
2544
+ }
2545
+ dismiss(notice) {
2546
+ try {
2547
+ storage()?.setItem(WORKBUDDY_UPDATE_DISMISSED_KEY, notice);
2548
+ } catch {}
2549
+ this.setSnapshot({
2550
+ ...this.snapshot,
2551
+ dismissedNotice: notice
2552
+ });
2553
+ }
2554
+ dispose() {
2555
+ this.disposed = true;
2556
+ clearTimeout(this.recheckTimer);
2557
+ this.request?.abort();
2558
+ this.request = void 0;
2559
+ this.listeners.clear();
2560
+ }
2561
+ };
2562
+ //#endregion
2563
+ //#region src/version.ts
2564
+ const WORKBUDDY_CONNECT_VERSION = "0.6.4";
2565
+ //#endregion
1816
2566
  //#region src/client/WorkBuddyConfigPage.tsx
1817
2567
  /**
1818
2568
  * The cards' list; the page supplies no other chrome. A semantic `<ul>` —
@@ -1952,7 +2702,25 @@ window.__ModuleLoader__.load({
1952
2702
  assistIncomplete: "the automatic search could not be completed",
1953
2703
  assistPathInvalid: "the configured program path is not usable",
1954
2704
  assistUnavailableCN: "no decryption program is configured for this platform",
1955
- assistUnavailableAI: "no decryption program is configured for WorkBuddy AI"
2705
+ assistUnavailableAI: "no decryption program is configured for WorkBuddy AI",
2706
+ updateNoticeLabel: "WorkBuddy Connect update",
2707
+ newVersionAvailable: "New version available: {version}",
2708
+ versionSummary: "Current {current} · Latest {latest}",
2709
+ versionsBehind: "{count} release(s) behind",
2710
+ versionsBehindUnknown: "release gap unavailable",
2711
+ releasesHeading: "What changed",
2712
+ releaseListUnavailable: "The per-release summary is unavailable right now; see the release page for details.",
2713
+ releaseNotesUnavailable: "Release notes for this version are unavailable.",
2714
+ copyForAgent: "Copy for agent",
2715
+ agentPromptCopied: "Copied",
2716
+ agentPromptCopyFailed: "Copy failed — select and copy the prompt manually.",
2717
+ recheckAfterUpgrade: "Re-check after upgrading",
2718
+ upgradeStillAvailable: "Upgrade not detected yet (still {version}). After upgrading, fully quit and restart DSH, then check again.",
2719
+ dismissUpdate: "Don't remind me again",
2720
+ openReleasePage: "Open release page",
2721
+ checkingForUpdates: "Checking…",
2722
+ updateCheckUnavailable: "Update information is unavailable right now. Check again later.",
2723
+ agentUpgradePrompt: "Please open {repository}, check its latest version, and install or update the plugin \"dsh-workbuddy-connect\" in my current DSH profile following the project README (the install command differs by profile). After upgrading, fully quit and restart DSH."
1956
2724
  };
1957
2725
  const zh = {
1958
2726
  title: "DSH WorkBuddy Connect",
@@ -2053,7 +2821,25 @@ window.__ModuleLoader__.load({
2053
2821
  assistIncomplete: "自动定位未能完成",
2054
2822
  assistPathInvalid: "指定的程序路径不可用",
2055
2823
  assistUnavailableCN: "当前平台尚未配置解密程序",
2056
- assistUnavailableAI: "尚未配置 WorkBuddy AI 的解密程序"
2824
+ assistUnavailableAI: "尚未配置 WorkBuddy AI 的解密程序",
2825
+ updateNoticeLabel: "WorkBuddy Connect 更新",
2826
+ newVersionAvailable: "发现新版本:{version}",
2827
+ versionSummary: "当前 {current} · 最新 {latest}",
2828
+ versionsBehind: "落后 {count} 个版本",
2829
+ versionsBehindUnknown: "版本差距暂时不可知",
2830
+ releasesHeading: "更新内容",
2831
+ releaseListUnavailable: "各版本的更新说明暂时不可用,请到 Release 页面查看。",
2832
+ releaseNotesUnavailable: "该版本的发布说明不可用。",
2833
+ copyForAgent: "复制给 Agent",
2834
+ agentPromptCopied: "已复制",
2835
+ agentPromptCopyFailed: "复制失败,请手动选中复制。",
2836
+ recheckAfterUpgrade: "升级完成后,重新检查",
2837
+ upgradeStillAvailable: "暂未检测到升级(仍是 {version})。升级后请完全退出并重启 DSH 再检查。",
2838
+ dismissUpdate: "不再提醒",
2839
+ openReleasePage: "打开 Release 页面",
2840
+ checkingForUpdates: "检查中…",
2841
+ updateCheckUnavailable: "暂时无法获取更新信息,请稍后再试。",
2842
+ agentUpgradePrompt: "请打开 {repository},查看最新版本,并按项目 README 把插件 dsh-workbuddy-connect 安装或更新到我当前使用的 DSH profile(不同 profile 的安装命令不同)。升级完成后请完全退出并重启 DSH。"
2057
2843
  };
2058
2844
  //#endregion
2059
2845
  //#region src/client/index.tsx
@@ -2147,6 +2933,27 @@ window.__ModuleLoader__.load({
2147
2933
  }), "dsh-workbuddy-connect: settings copy");
2148
2934
  });
2149
2935
  const t = ctx.locale.bind(namespace);
2936
+ const updater = new WorkBuddyUpdateStore(WORKBUDDY_CONNECT_VERSION);
2937
+ guardClientContribution("update reminder lifecycle", () => {
2938
+ ctx.effect(() => {
2939
+ updater.refresh();
2940
+ return () => {
2941
+ updater.dispose();
2942
+ };
2943
+ }, "dsh-workbuddy-connect: update checker");
2944
+ });
2945
+ guardClientContribution("update reminder overlay", () => {
2946
+ ctx.slots.inject("shell.overlay", () => guardClientContribution("update reminder overlay", () => ctx.slots.register({
2947
+ name: "shell.overlay",
2948
+ id: "workbuddy-update",
2949
+ order: 40,
2950
+ locale: namespace,
2951
+ inject: () => ({
2952
+ t,
2953
+ updater
2954
+ })
2955
+ }, WorkBuddyUpdateOverlay)) ?? NOOP_DISPOSER);
2956
+ });
2150
2957
  for (const [index, variant] of CARD_VARIANTS.entries()) {
2151
2958
  const label = `settings.plugin.item card "${variant.id}"`;
2152
2959
  guardClientContribution(label, () => {