@adea-ai/ui 0.72.3 → 0.72.5

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.
@@ -29,9 +29,13 @@ import type { Accessor } from 'solid-js';
29
29
  * the other order leaves the dialog blank for as long as the check takes, on a
30
30
  * dialog whose first question is always "what am I running?".
31
31
  *
32
- * **Errors are not always `Error`s.** An adapter can reject with a string, and a
33
- * shell can answer with an error field. Both are read, and a fallback is shown
34
- * rather than an empty alert.
32
+ * **A failed check is not a current result.** A cached `current` status can only
33
+ * be shown as up to date after the latest check succeeds; a failed check remains
34
+ * retryable and suppresses that claim.
35
+ *
36
+ * **Errors are not always `Error`s.** An adapter can reject with a string or a
37
+ * structured safe message. Unknown object fields are never serialized into the
38
+ * UI; a generic fallback is shown instead.
35
39
  *
36
40
  * The adapter's `isDesktopRuntime` gate exists because an update is a desktop
37
41
  * concern: a browser build has no installer, and the honest thing to show is that
@@ -1 +1 @@
1
- {"version":3,"file":"update-dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/update-dialog/update-dialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAmBxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,iEAAiE;AACjE,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,UAAU,GACV,SAAS,GACT,WAAW,GACX,aAAa,GACb,YAAY,GACZ,WAAW,GACX,QAAQ,CAAA;AAEZ,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,KAAK,EAAE,WAAW,CAAA;IAClB,uCAAuC;IACvC,cAAc,EAAE,MAAM,CAAA;IACtB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAC,CAAA;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,iEAAiE;IACjE,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IACjC,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACtD,sEAAsE;IACtE,gBAAgB,IAAI,OAAO,CAAA;CAC5B,CAAC,CAAA;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,aAAa,CAAA;IACtB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,uEAAuE;IACvE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC;;;;OAIG;IACH,eAAe,CAAC,EAAE,QAAQ,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;IACnD,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAoBD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,kCA6apD"}
1
+ {"version":3,"file":"update-dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/composites/update-dialog/update-dialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AAmBxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,iEAAiE;AACjE,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,UAAU,GACV,SAAS,GACT,WAAW,GACX,aAAa,GACb,YAAY,GACZ,WAAW,GACX,QAAQ,CAAA;AAEZ,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,KAAK,EAAE,WAAW,CAAA;IAClB,uCAAuC;IACvC,cAAc,EAAE,MAAM,CAAA;IACtB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,wDAAwD;IACxD,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B,CAAC,CAAA;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,iEAAiE;IACjE,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IACjC,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7B;;;;;;OAMG;IACH,OAAO,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IACtD,sEAAsE;IACtE,gBAAgB,IAAI,OAAO,CAAA;CAC5B,CAAC,CAAA;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,OAAO,EAAE,aAAa,CAAA;IACtB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,+CAA+C;IAC/C,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,uEAAuE;IACvE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC;;;;OAIG;IACH,eAAe,CAAC,EAAE,QAAQ,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;IACnD,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAgCD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,kCAmdpD"}
@@ -18,10 +18,20 @@ var _tmpl$7 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><div clas
18
18
  var _tmpl$8 = /*#__PURE__*/ template(`<div class="flex flex-col gap-2"><h2 id=update-changelog class="text-sm font-semibold">Installed changelog</h2><p class="text-xs text-muted-foreground">A plain-text history of the installed release channel.</p><div class="max-h-72 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground"role=region aria-labelledby=update-changelog tabindex=0>`);
19
19
  var _tmpl$9 = /*#__PURE__*/ template(`<div class="flex min-h-0 flex-col gap-5 overflow-y-auto p-6"><section class="rounded-xl border border-border bg-background/45 p-4"aria-label="Version status"><div class="flex flex-wrap items-start justify-between gap-4"><div class="flex flex-col gap-1"><p class="text-2xs font-semibold tracking-widest text-muted-foreground uppercase">Installed version</p><p class="text-xl font-semibold tracking-tight">v</p><p class="text-sm text-muted-foreground">`);
20
20
  var _tmpl$0 = /*#__PURE__*/ template(`<p class="text-xs text-muted-foreground">Released `);
21
+ function messageText(value) {
22
+ if (typeof value !== "string") return void 0;
23
+ const message = value.trim();
24
+ return message && message !== "[object Object]" ? message : void 0;
25
+ }
21
26
  function errorMessage(caught, fallback) {
22
- if (caught instanceof Error && caught.message) return caught.message;
23
- if (typeof caught === "string" && caught) return caught;
24
- return fallback;
27
+ if (caught instanceof Error) return messageText(caught.message) ?? fallback;
28
+ const plainMessage = messageText(caught);
29
+ if (plainMessage) return plainMessage;
30
+ if (!caught || typeof caught !== "object") return fallback;
31
+ const record = caught;
32
+ const safe = record.safe;
33
+ const nestedError = record.error;
34
+ return messageText(safe?.message) ?? messageText(nestedError?.safe?.message) ?? fallback;
25
35
  }
26
36
  /**
27
37
  * The phases during which the updater is working. `downloading` and `installing`
@@ -59,6 +69,7 @@ function UpdateDialog(props) {
59
69
  const [state, setState] = createSignal(null);
60
70
  const [busy, setBusy] = createSignal(false);
61
71
  const [error, setError] = createSignal("");
72
+ const [checkFailed, setCheckFailed] = createSignal(false);
62
73
  let lifecycleEpoch = 0;
63
74
  let activeOpenEpoch;
64
75
  let disposed = false;
@@ -71,7 +82,10 @@ function UpdateDialog(props) {
71
82
  if (!desktop() || !isCurrent()) return;
72
83
  try {
73
84
  const status = await local.adapter.getStatus();
74
- if (isCurrent()) setState(status);
85
+ const previous = state();
86
+ const hasActionableSnapshot = (candidate) => candidate?.phase === "available" || isBusy(candidate);
87
+ const preservesSnapshot = previous?.phase === "available" ? status.phase === "available" || isBusy(status) : isBusy(previous) && isBusy(status);
88
+ if (isCurrent() && !(checkFailed() && hasActionableSnapshot(previous) && !preservesSnapshot)) setState(status);
75
89
  } catch (caught) {
76
90
  if (isCurrent()) setError(errorMessage(caught, "Version status is unavailable"));
77
91
  }
@@ -82,6 +96,21 @@ function UpdateDialog(props) {
82
96
  loadStatus(() => !disposed && lifecycleEpoch === epoch);
83
97
  }
84
98
  });
99
+ const failCheck = (caught, fallback) => {
100
+ const message = errorMessage(caught, fallback);
101
+ setCheckFailed(true);
102
+ setError(message);
103
+ };
104
+ const applyCheckResult = (next) => {
105
+ if (next.phase === "failed") {
106
+ if (!state()) setState(next);
107
+ failCheck(next.error, "Could not check for updates");
108
+ return;
109
+ }
110
+ setState(next);
111
+ setCheckFailed(false);
112
+ setError("");
113
+ };
85
114
  const check = async () => {
86
115
  if (!desktop()) {
87
116
  setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`);
@@ -92,13 +121,15 @@ function UpdateDialog(props) {
92
121
  if (!isCurrent()) return;
93
122
  setBusy(true);
94
123
  setError("");
124
+ setCheckFailed(false);
95
125
  try {
96
126
  const checked = await local.adapter.check();
97
- if (isCurrent()) setState(checked);
127
+ if (isCurrent()) applyCheckResult(checked);
98
128
  } catch (caught) {
99
- if (!isCurrent()) return;
100
- setError(errorMessage(caught, "Could not check for updates"));
101
- await loadStatus(isCurrent);
129
+ if (isCurrent()) {
130
+ failCheck(caught, "Could not check for updates");
131
+ await loadStatus(isCurrent);
132
+ }
102
133
  } finally {
103
134
  if (isCurrent()) setBusy(false);
104
135
  }
@@ -128,6 +159,7 @@ function UpdateDialog(props) {
128
159
  }
129
160
  });
130
161
  setError("");
162
+ setCheckFailed(false);
131
163
  setBusy(true);
132
164
  (async () => {
133
165
  try {
@@ -135,9 +167,9 @@ function UpdateDialog(props) {
135
167
  if (!isCurrent()) return;
136
168
  setState(current);
137
169
  const checked = await local.adapter.check();
138
- if (isCurrent()) setState(checked);
170
+ if (isCurrent()) applyCheckResult(checked);
139
171
  } catch (caught) {
140
- if (isCurrent()) setError(errorMessage(caught, "Could not check for updates"));
172
+ if (isCurrent()) failCheck(caught, "Could not check for updates");
141
173
  } finally {
142
174
  if (isCurrent()) setBusy(false);
143
175
  }
@@ -155,6 +187,7 @@ function UpdateDialog(props) {
155
187
  if (!isCurrent()) return;
156
188
  setState(next);
157
189
  if (next.phase === "failed") setError(errorMessage(next.error, "Update installation failed"));
190
+ else setCheckFailed(false);
158
191
  } catch (caught) {
159
192
  if (!isCurrent()) return;
160
193
  setError(errorMessage(caught, "Update installation failed"));
@@ -169,9 +202,11 @@ function UpdateDialog(props) {
169
202
  return value ? plainTextFromMarkdown(value) : "";
170
203
  };
171
204
  const working = () => busy() || isBusy(state());
205
+ const isCurrent = () => state()?.phase === "current" && !error() && !checkFailed() && !working();
172
206
  const triggerLabel = () => {
173
207
  const current = state();
174
208
  const fallback = `v${local.fallbackVersion ?? "0.1.0"}`;
209
+ if (checkFailed()) return `Version ${current?.currentVersion || fallback} · Retry`;
175
210
  if (!current) return `${appName()} ${fallback}`;
176
211
  switch (current.phase) {
177
212
  case "checking": return "Checking for updates…";
@@ -254,7 +289,7 @@ function UpdateDialog(props) {
254
289
  insert(_el$9, () => state()?.currentVersion || local.fallbackVersion || "0.1.0", null);
255
290
  insert(_el$1, createComponent(Show, {
256
291
  get when() {
257
- return state()?.phase === "current";
292
+ return isCurrent();
258
293
  },
259
294
  get fallback() {
260
295
  return createComponent(Show, {
@@ -293,7 +328,7 @@ function UpdateDialog(props) {
293
328
  "aria-hidden": "true"
294
329
  });
295
330
  }
296
- }), "Check latest version"];
331
+ }), memo(() => checkFailed() ? "Retry update check" : "Check latest version")];
297
332
  }
298
333
  }), null);
299
334
  insert(_el$4, createComponent(Show, {
@@ -385,7 +420,7 @@ function UpdateDialog(props) {
385
420
  }), null);
386
421
  insert(_el$4, createComponent(Show, {
387
422
  get when() {
388
- return state()?.phase === "current";
423
+ return isCurrent();
389
424
  },
390
425
  get children() {
391
426
  var _el$23 = _tmpl$4(), _el$24 = _el$23.firstChild;
@@ -1 +1 @@
1
- {"version":3,"file":"update-dialog.js","names":["createEffect","createMemo","createSignal","onCleanup","onMount","Show","splitProps","Accessor","Check","Download","ExternalLink","LoaderCircle","RefreshCw","Sparkles","Badge","Button","Dialog","DialogClose","DialogContent","DialogDescription","DialogFooter","DialogHeader","DialogTitle","DialogTrigger","createDialogFocusRestoration","Progress","cn","formatBytes","formatReleaseDate","plainTextFromMarkdown","UpdatePhase","UpdateState","Readonly","phase","currentVersion","availableVersion","changelog","releaseNotes","releaseDate","downloadedBytes","totalBytes","error","releaseUrl","restartRequired","UpdateAdapter","getStatus","Promise","check","install","expectedVersion","isDesktopRuntime","UpdateDialogProps","adapter","appName","fallbackVersion","desktopOnlyMessage","open","defaultOpen","onOpenChange","restoreFocusRef","HTMLElement","class","errorMessage","caught","fallback","Error","message","isBusy","state","UpdateDialog","props","local","uncontrolledOpen","setUncontrolledOpen","setOpen","next","undefined","focusRestoration","desktop","setState","busy","setBusy","setError","lifecycleEpoch","activeOpenEpoch","disposed","loadStatus","isCurrent","status","epoch","checked","active","current","version","notes","value","working","triggerLabel","progressValue","Math","min","round","_$createComponent","children","when","as","variant","size","_$memo","onOpenAutoFocus","onCloseAutoFocus","_el$","_tmpl$","_el$2","firstChild","_el$3","nextSibling","_$insert","_el$4","_tmpl$9","_el$5","_el$6","_el$7","_el$8","_el$9","_el$0","_el$1","type","disabled","onClick","_el$10","_tmpl$2","_el$11","_el$12","_el$13","_el$14","_el$16","_el$15","_el$17","_el$18","_el$20","_el$19","released","_el$35","_tmpl$0","_el$36","_el$21","_tmpl$3","_el$22","aria-label","_el$23","_tmpl$4","_el$24","_el$25","_tmpl$5","_el$26","_tmpl$6","_el$27","_tmpl$7","_el$28","_el$29","_el$30","_el$31","_tmpl$8","_el$32","_el$33","_el$34","url","window","_$createComponent","_$memo"],"sources":["../../../../src/components/composites/update-dialog/update-dialog.tsx"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n onCleanup,\n onMount,\n Show,\n splitProps,\n} from 'solid-js'\nimport type { Accessor } from 'solid-js'\nimport { Check, Download, ExternalLink, LoaderCircle, RefreshCw, Sparkles } from 'lucide-solid'\nimport { Badge } from '../../ui/badge'\nimport { Button } from '../../ui/button'\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from '../../ui/dialog'\nimport { createDialogFocusRestoration } from '../../ui/dialog/dialog'\nimport { Progress } from '../../ui/progress'\nimport { cn } from '#lib/utils'\nimport { formatBytes, formatReleaseDate, plainTextFromMarkdown } from '#lib/version-notes'\n\n/**\n * UpdateDialog.\n *\n * A software-update surface: the installed version, whether a newer release\n * exists, a download that shows its progress, and the release notes for what is\n * about to arrive.\n *\n * The transport is an adapter, because an update is the one thing a component\n * library genuinely cannot implement: it is signed, downloaded and applied by the\n * application's own updater, and the mechanism differs per shell. What *is*\n * general is the phase machine and the way each phase is presented — and that is\n * where the hard-won detail lives:\n *\n * **A refused install answers with a normal payload, not a rejection.** adea's\n * shell returns a status whose phase is `failed` when a download or an extract\n * fails, rather than throwing. Without handling that, clicking install looked like\n * nothing happened at all — the button spun, the phase never changed, and the\n * error sat unread in a field nobody rendered. The `failed` phase after an install\n * is therefore surfaced as an error, explicitly.\n *\n * **Busy is derived from the snapshot as well as from the click.** An update can\n * be downloading while the dialog is open — started before it opened, or by the\n * shell's own poll — so a local `busy` flag alone would offer an enabled Install\n * button mid-download.\n *\n * **Opening the dialog loads status first, then checks.** Status paints the\n * installed version immediately; the check is a network round trip. Doing them in\n * the other order leaves the dialog blank for as long as the check takes, on a\n * dialog whose first question is always \"what am I running?\".\n *\n * **Errors are not always `Error`s.** An adapter can reject with a string, and a\n * shell can answer with an error field. Both are read, and a fallback is shown\n * rather than an empty alert.\n *\n * The adapter's `isDesktopRuntime` gate exists because an update is a desktop\n * concern: a browser build has no installer, and the honest thing to show is that\n * updates are available in the desktop application rather than a disabled button\n * with no reason.\n */\n\n/** The phase an update is in. The union is the state machine. */\nexport type UpdatePhase =\n | 'idle'\n | 'checking'\n | 'current'\n | 'available'\n | 'downloading'\n | 'installing'\n | 'installed'\n | 'failed'\n\n/** A snapshot of the updater. Every field the dialog renders, and nothing else. */\nexport type UpdateState = Readonly<{\n phase: UpdatePhase\n /** The version currently installed. */\n currentVersion: string\n /** The version that can be installed, when one can. */\n availableVersion?: string | null\n /** A changelog for the installed channel, as markdown. */\n changelog?: string\n /** Notes for the available release, as markdown. */\n releaseNotes?: string | null\n /** The available release's date, as an ISO string. */\n releaseDate?: string | null\n /** Bytes downloaded so far, during `downloading`. */\n downloadedBytes?: number\n /** Total bytes, when the server sent a length. */\n totalBytes?: number | null\n /** Set when `phase` is `failed`, and sometimes when it is not. */\n error?: string | null\n /** A URL for the release's page. */\n releaseUrl?: string | null\n /** True when the application must restart to finish. */\n restartRequired?: boolean\n}>\n\n/**\n * The updater, as the application exposes it. Three calls, each answering with a\n * full snapshot rather than a delta — an updater's state is small, and a snapshot\n * cannot drift from the shell's own view of it.\n */\nexport type UpdateAdapter = Readonly<{\n /** The current state, without contacting the release channel. */\n getStatus(): Promise<UpdateState>\n /** Ask the release channel, and answer with what it found. */\n check(): Promise<UpdateState>\n /**\n * Download and apply `expectedVersion`.\n *\n * The argument is the version the caller believes is available, so the updater\n * can refuse a request that raced a newer release rather than installing the\n * wrong one.\n */\n install(expectedVersion: string): Promise<UpdateState>\n /** False in a browser build, where there is no installer to drive. */\n isDesktopRuntime(): boolean\n}>\n\nexport type UpdateDialogProps = {\n adapter: UpdateAdapter\n /** The application's name, used in the title and the copy. */\n appName?: string\n /** Shown before the first status arrives, so the dialog is never blank. */\n fallbackVersion?: string\n /** Copy for the adapter's non-desktop case. */\n desktopOnlyMessage?: string\n /** A controlled open state. Omit for the trigger to manage its own. */\n open?: boolean\n /**\n * Start open, uncontrolled — for a caller that opens the dialog because it found\n * something (\"an update is available\") rather than because the user asked. The\n * trigger is still rendered, so the dialog is reachable again after closing.\n */\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /**\n * Supplies a stable external control to focus when a controlled dialog closes.\n * Useful when a menu or other layer closes before this dialog opens; when omitted,\n * the element focused immediately before the dialog opens is used when available.\n */\n restoreFocusRef?: Accessor<HTMLElement | undefined>\n /** Render only the panel, for a caller that provides its own dialog. */\n class?: string\n}\n\nfunction errorMessage(caught: unknown, fallback: string): string {\n if (caught instanceof Error && caught.message) return caught.message\n if (typeof caught === 'string' && caught) return caught\n return fallback\n}\n\n/**\n * The phases during which the updater is working. `downloading` and `installing`\n * are included because the shell can be in either while the dialog is open, and\n * offering an enabled Install button during a download is how a double-install\n * happens.\n */\nfunction isBusy(state: UpdateState | null): boolean {\n return (\n state?.phase === 'checking' || state?.phase === 'downloading' || state?.phase === 'installing'\n )\n}\n\nexport function UpdateDialog(props: UpdateDialogProps) {\n const [local] = splitProps(props, [\n 'adapter',\n 'appName',\n 'fallbackVersion',\n 'desktopOnlyMessage',\n 'open',\n 'defaultOpen',\n 'onOpenChange',\n 'restoreFocusRef',\n 'class',\n ])\n\n const [uncontrolledOpen, setUncontrolledOpen] = createSignal(local.defaultOpen ?? false)\n const open = () => local.open ?? uncontrolledOpen()\n const setOpen = (next: boolean) => {\n if (local.open === undefined) setUncontrolledOpen(next)\n local.onOpenChange?.(next)\n }\n\n const focusRestoration = createDialogFocusRestoration({\n open,\n restoreFocusRef: local.restoreFocusRef,\n })\n\n const appName = () => local.appName ?? 'the application'\n const desktop = () => local.adapter.isDesktopRuntime()\n const [state, setState] = createSignal<UpdateState | null>(null)\n const [busy, setBusy] = createSignal(false)\n const [error, setError] = createSignal('')\n let lifecycleEpoch = 0\n let activeOpenEpoch: number | undefined\n let disposed = false\n\n onCleanup(() => {\n disposed = true\n activeOpenEpoch = undefined\n lifecycleEpoch += 1\n })\n\n const loadStatus = async (isCurrent: () => boolean = () => !disposed) => {\n if (!desktop() || !isCurrent()) return\n try {\n const status = await local.adapter.getStatus()\n if (isCurrent()) setState(status)\n } catch (caught) {\n if (isCurrent()) setError(errorMessage(caught, 'Version status is unavailable'))\n }\n }\n\n onMount(() => {\n if (desktop() && !open()) {\n const epoch = lifecycleEpoch\n void loadStatus(() => !disposed && lifecycleEpoch === epoch)\n }\n })\n\n const check = async () => {\n if (!desktop()) {\n setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`)\n return\n }\n const epoch = activeOpenEpoch\n const isCurrent = () =>\n !disposed &&\n epoch !== undefined &&\n activeOpenEpoch === epoch &&\n lifecycleEpoch === epoch &&\n open()\n if (!isCurrent()) return\n setBusy(true)\n setError('')\n try {\n const checked = await local.adapter.check()\n if (isCurrent()) setState(checked)\n } catch (caught) {\n if (!isCurrent()) return\n setError(errorMessage(caught, 'Could not check for updates'))\n await loadStatus(isCurrent)\n } finally {\n if (isCurrent()) setBusy(false)\n }\n }\n\n /**\n * Status first, then the check — and an `active` flag, because the dialog can\n * close while the check is in flight and a late `setState` would write into a\n * disposed scope.\n */\n createEffect(() => {\n if (!open() || !desktop()) {\n if (activeOpenEpoch !== undefined) {\n activeOpenEpoch = undefined\n lifecycleEpoch += 1\n }\n return\n }\n const epoch = ++lifecycleEpoch\n activeOpenEpoch = epoch\n let active = true\n const isCurrent = () =>\n active && !disposed && activeOpenEpoch === epoch && lifecycleEpoch === epoch && open()\n onCleanup(() => {\n active = false\n if (activeOpenEpoch === epoch) {\n activeOpenEpoch = undefined\n lifecycleEpoch += 1\n }\n })\n setError('')\n setBusy(true)\n void (async () => {\n try {\n const current = await local.adapter.getStatus()\n if (!isCurrent()) return\n setState(current)\n const checked = await local.adapter.check()\n if (isCurrent()) setState(checked)\n } catch (caught) {\n if (isCurrent()) setError(errorMessage(caught, 'Could not check for updates'))\n } finally {\n if (isCurrent()) setBusy(false)\n }\n })()\n })\n\n const install = async () => {\n const version = state()?.availableVersion\n const epoch = activeOpenEpoch\n const isCurrent = () =>\n !disposed &&\n epoch !== undefined &&\n activeOpenEpoch === epoch &&\n lifecycleEpoch === epoch &&\n open()\n if (!version || !isCurrent()) return\n setBusy(true)\n setError('')\n try {\n const next = await local.adapter.install(version)\n if (!isCurrent()) return\n setState(next)\n // A refused install answers with a normal payload whose phase is `failed`.\n // Without this the click looked like nothing happened.\n if (next.phase === 'failed') {\n setError(errorMessage(next.error, 'Update installation failed'))\n }\n } catch (caught) {\n if (!isCurrent()) return\n setError(errorMessage(caught, 'Update installation failed'))\n await loadStatus(isCurrent)\n } finally {\n if (isCurrent()) setBusy(false)\n }\n }\n\n const changelog = createMemo(() => plainTextFromMarkdown(state()?.changelog ?? ''))\n const notes = () => {\n const value = state()?.releaseNotes\n return value ? plainTextFromMarkdown(value) : ''\n }\n const working = () => busy() || isBusy(state())\n\n const triggerLabel = () => {\n const current = state()\n const fallback = `v${local.fallbackVersion ?? '0.1.0'}`\n if (!current) return `${appName()} ${fallback}`\n switch (current.phase) {\n case 'checking':\n return 'Checking for updates…'\n case 'available':\n return current.availableVersion\n ? `Update v${current.availableVersion} available`\n : 'Update available'\n case 'downloading':\n case 'installing':\n return 'Installing update…'\n case 'failed':\n return `Version ${current.currentVersion || fallback} · Retry`\n default:\n return `${appName()} v${current.currentVersion || fallback}`\n }\n }\n\n const progressValue = () => {\n const current = state()\n if (!current?.totalBytes || !current.downloadedBytes) return null\n return Math.min(100, Math.round((current.downloadedBytes / current.totalBytes) * 100))\n }\n\n return (\n <Dialog open={open()} onOpenChange={setOpen}>\n <Show when={local.open === undefined}>\n <DialogTrigger\n as={Button}\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Open version and updates\"\n aria-haspopup=\"dialog\"\n >\n <Show when={state()?.phase === 'available'} fallback={<RefreshCw aria-hidden=\"true\" />}>\n <Sparkles aria-hidden=\"true\" />\n </Show>\n {triggerLabel()}\n </DialogTrigger>\n </Show>\n\n <DialogContent\n class={cn('max-w-3xl', local.class)}\n onOpenAutoFocus={focusRestoration.onOpenAutoFocus}\n onCloseAutoFocus={focusRestoration.onCloseAutoFocus}\n >\n <DialogHeader>\n <div class=\"flex items-center gap-3\">\n <span class=\"flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground\">\n <Sparkles class=\"size-5\" aria-hidden=\"true\" />\n </span>\n <div>\n <DialogTitle>Version &amp; updates</DialogTitle>\n <DialogDescription>\n Keep {appName()} current and review what changed in each release.\n </DialogDescription>\n </div>\n </div>\n </DialogHeader>\n\n <div class=\"flex min-h-0 flex-col gap-5 overflow-y-auto p-6\">\n <section\n class=\"rounded-xl border border-border bg-background/45 p-4\"\n aria-label=\"Version status\"\n >\n <div class=\"flex flex-wrap items-start justify-between gap-4\">\n <div class=\"flex flex-col gap-1\">\n <p class=\"text-2xs font-semibold tracking-widest text-muted-foreground uppercase\">\n Installed version\n </p>\n <p class=\"text-xl font-semibold tracking-tight\">\n v{state()?.currentVersion || local.fallbackVersion || '0.1.0'}\n </p>\n <p class=\"text-sm text-muted-foreground\">\n <Show\n when={state()?.phase === 'current'}\n fallback={\n <Show\n when={state()?.phase === 'available' && state()?.availableVersion}\n fallback={\n desktop()\n ? 'Check the release channel for the latest signed build.'\n : (local.desktopOnlyMessage ??\n `Open this dialog inside the desktop app to check for updates.`)\n }\n >\n {`A newer release, v${state()?.availableVersion}, is ready.`}\n </Show>\n }\n >\n You are running the latest release.\n </Show>\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={!desktop() || working()}\n onClick={() => void check()}\n >\n <Show when={working()} fallback={<RefreshCw aria-hidden=\"true\" />}>\n <LoaderCircle class=\"animate-spin\" aria-hidden=\"true\" />\n </Show>\n Check latest version\n </Button>\n </div>\n </section>\n\n <Show when={state()?.phase === 'available' && state()?.availableVersion}>\n <section\n class=\"rounded-xl border border-primary/35 bg-primary-subtle p-4\"\n aria-label=\"Available update\"\n >\n <div class=\"flex flex-wrap items-start justify-between gap-4\">\n <div class=\"flex flex-col gap-1\">\n <p class=\"flex items-center gap-2 text-sm font-semibold\">\n <Sparkles class=\"size-4 text-primary\" aria-hidden=\"true\" />\n Version {state()?.availableVersion} is ready\n </p>\n <p class=\"text-sm text-muted-foreground\">\n The signed installer is verified before {appName()} restarts.\n </p>\n <Show when={formatReleaseDate(state()?.releaseDate ?? null)}>\n {(released) => (\n <p class=\"text-xs text-muted-foreground\">Released {released()}</p>\n )}\n </Show>\n </div>\n <Button type=\"button\" size=\"sm\" disabled={working()} onClick={() => void install()}>\n <Show when={working()} fallback={<Download aria-hidden=\"true\" />}>\n <LoaderCircle class=\"animate-spin\" aria-hidden=\"true\" />\n </Show>\n Install and restart\n </Button>\n </div>\n </section>\n </Show>\n\n {/*\n Progress with both numbers: a bar alone does not distinguish a stalled\n download from a slow one, and \"12.4 MB of 48.1 MB\" does.\n */}\n <Show when={state()?.phase === 'downloading' || state()?.phase === 'installing'}>\n <section class=\"grid gap-2\" aria-label=\"Update progress\">\n <Progress\n value={progressValue() ?? undefined}\n aria-label={\n state()?.phase === 'installing' ? 'Installing update' : 'Downloading update'\n }\n />\n <p class=\"text-xs text-muted-foreground\" role=\"status\">\n <Show\n when={state()?.phase === 'installing'}\n fallback={\n <Show when={progressValue() !== null} fallback=\"Downloading…\">\n {`${formatBytes(state()?.downloadedBytes ?? 0)} of ${formatBytes(\n state()?.totalBytes ?? 0\n )} · ${progressValue()}%`}\n </Show>\n }\n >\n Applying the update…\n </Show>\n </p>\n </section>\n </Show>\n\n <Show when={state()?.phase === 'current'}>\n <p class=\"text-foreground flex items-center gap-2 text-sm\" role=\"status\">\n <Check class=\"size-4 text-success\" aria-hidden=\"true\" />\n {appName()} is up to date.\n </p>\n </Show>\n\n <Show when={state()?.phase === 'installed'}>\n <p class=\"text-foreground flex items-center gap-2 text-sm\" role=\"status\">\n <Check class=\"size-4 text-success\" aria-hidden=\"true\" />\n <Show when={state()?.restartRequired} fallback=\"The update is applied.\">\n Restart {appName()} to finish the update.\n </Show>\n </p>\n </Show>\n\n <Show when={error()}>\n <p\n class=\"text-foreground rounded-lg border border-destructive/35 bg-destructive-subtle px-3 py-2 text-sm\"\n role=\"alert\"\n >\n {error()}\n </p>\n </Show>\n\n <Show when={notes()}>\n <div class=\"flex flex-col gap-2\">\n <div class=\"flex items-center gap-2\">\n <h2 id=\"update-release-notes\" class=\"text-sm font-semibold\">\n What changed in this release\n </h2>\n <Badge variant=\"subtle\" size=\"sm\">\n v{state()?.availableVersion}\n </Badge>\n </div>\n {/* A long text block with no focusable descendant: it needs its own tab\n stop, named by the heading above it, or a keyboard user cannot\n scroll it (`scrollable-region-focusable`). */}\n <div\n class=\"max-h-52 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground\"\n role=\"region\"\n aria-labelledby=\"update-release-notes\"\n tabindex=\"0\"\n >\n {notes()}\n </div>\n </div>\n </Show>\n\n <Show when={changelog()}>\n <div class=\"flex flex-col gap-2\">\n <h2 id=\"update-changelog\" class=\"text-sm font-semibold\">\n Installed changelog\n </h2>\n <p class=\"text-xs text-muted-foreground\">\n A plain-text history of the installed release channel.\n </p>\n {/* A long text block with no focusable descendant: it needs its own tab\n stop, named by the heading above it, or a keyboard user cannot\n scroll it (`scrollable-region-focusable`). */}\n <div\n class=\"max-h-72 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground\"\n role=\"region\"\n aria-labelledby=\"update-changelog\"\n tabindex=\"0\"\n >\n {changelog()}\n </div>\n </div>\n </Show>\n </div>\n\n <DialogFooter>\n <Show when={state()?.releaseUrl}>\n {(url) => (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n // `noopener,noreferrer` because the release page is a different\n // origin and must not get a handle on this window.\n onClick={() => window.open(url(), '_blank', 'noopener,noreferrer')}\n >\n <ExternalLink aria-hidden=\"true\" />\n View releases\n </Button>\n )}\n </Show>\n <DialogClose as={Button} variant=\"outline\" size=\"sm\">\n Close\n </DialogClose>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyJA,SAAS8D,aAAaC,QAAiBC,UAA0B;CAC/D,IAAID,kBAAkBE,SAASF,OAAOG,SAAS,OAAOH,OAAOG;CAC7D,IAAI,OAAOH,WAAW,YAAYA,QAAQ,OAAOA;CACjD,OAAOC;AACT;;;;;;;AAQA,SAASG,OAAOC,OAAoC;CAClD,OACEA,OAAOnC,UAAU,cAAcmC,OAAOnC,UAAU,iBAAiBmC,OAAOnC,UAAU;AAEtF;AAEA,SAAgBoC,aAAaC,OAA0B;CACrD,MAAM,CAACC,SAASjE,WAAWgE,OAAO;EAChC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAO,CACR;CAED,MAAM,CAACE,kBAAkBC,uBAAuBvE,aAAaqE,MAAMd,eAAe,KAAK;CACvF,MAAMD,aAAae,MAAMf,QAAQgB,iBAAiB;CAClD,MAAME,WAAWC,SAAkB;EACjC,IAAIJ,MAAMf,SAASoB,KAAAA,GAAWH,oBAAoBE,IAAI;EACtDJ,MAAMb,eAAeiB,IAAI;CAC3B;CAEA,MAAME,mBAAmBrD,6BAA6B;EACpDgC;EACAG,iBAAiBY,MAAMZ;CACzB,CAAC;CAED,MAAMN,gBAAgBkB,MAAMlB,WAAW;CACvC,MAAMyB,gBAAgBP,MAAMnB,QAAQF,iBAAiB;CACrD,MAAM,CAACkB,OAAOW,YAAY7E,aAAiC,IAAI;CAC/D,MAAM,CAAC8E,MAAMC,WAAW/E,aAAa,KAAK;CAC1C,MAAM,CAACuC,OAAOyC,YAAYhF,aAAa,EAAE;CACzC,IAAIiF,iBAAiB;CACrB,IAAIC;CACJ,IAAIC,WAAW;CAEflF,gBAAgB;EACdkF,WAAW;EACXD,kBAAkBR,KAAAA;EAClBO,kBAAkB;CACpB,CAAC;CAED,MAAMG,aAAa,OAAOC,kBAAiC,CAACF,aAAa;EACvE,IAAI,CAACP,QAAQ,KAAK,CAACS,UAAU,GAAG;EAChC,IAAI;GACF,MAAMC,SAAS,MAAMjB,MAAMnB,QAAQP,UAAU;GAC7C,IAAI0C,UAAU,GAAGR,SAASS,MAAM;EAClC,SAASzB,QAAQ;GACf,IAAIwB,UAAU,GAAGL,SAASpB,aAAaC,QAAQ,+BAA+B,CAAC;EACjF;CACF;CAEA3D,cAAc;EACZ,IAAI0E,QAAQ,KAAK,CAACtB,KAAK,GAAG;GACxB,MAAMiC,QAAQN;GACd,iBAAsB,CAACE,YAAYF,mBAAmBM,KAAK;EAC7D;CACF,CAAC;CAED,MAAM1C,QAAQ,YAAY;EACxB,IAAI,CAAC+B,QAAQ,GAAG;GACdI,SAASX,MAAMhB,sBAAsB,mDAAmD;GACxF;EACF;EACA,MAAMkC,QAAQL;EACd,MAAMG,kBACJ,CAACF,YACDI,UAAUb,KAAAA,KACVQ,oBAAoBK,SACpBN,mBAAmBM,SACnBjC,KAAK;EACP,IAAI,CAAC+B,UAAU,GAAG;EAClBN,QAAQ,IAAI;EACZC,SAAS,EAAE;EACX,IAAI;GACF,MAAMQ,UAAU,MAAMnB,MAAMnB,QAAQL,MAAM;GAC1C,IAAIwC,UAAU,GAAGR,SAASW,OAAO;EACnC,SAAS3B,QAAQ;GACf,IAAI,CAACwB,UAAU,GAAG;GAClBL,SAASpB,aAAaC,QAAQ,6BAA6B,CAAC;GAC5D,MAAMuB,WAAWC,SAAS;EAC5B,UAAU;GACR,IAAIA,UAAU,GAAGN,QAAQ,KAAK;EAChC;CACF;;;;;;CAOAjF,mBAAmB;EACjB,IAAI,CAACwD,KAAK,KAAK,CAACsB,QAAQ,GAAG;GACzB,IAAIM,oBAAoBR,KAAAA,GAAW;IACjCQ,kBAAkBR,KAAAA;IAClBO,kBAAkB;GACpB;GACA;EACF;EACA,MAAMM,QAAQ,EAAEN;EAChBC,kBAAkBK;EAClB,IAAIE,SAAS;EACb,MAAMJ,kBACJI,UAAU,CAACN,YAAYD,oBAAoBK,SAASN,mBAAmBM,SAASjC,KAAK;EACvFrD,gBAAgB;GACdwF,SAAS;GACT,IAAIP,oBAAoBK,OAAO;IAC7BL,kBAAkBR,KAAAA;IAClBO,kBAAkB;GACpB;EACF,CAAC;EACDD,SAAS,EAAE;EACXD,QAAQ,IAAI;EACZ,CAAM,YAAY;GAChB,IAAI;IACF,MAAMW,UAAU,MAAMrB,MAAMnB,QAAQP,UAAU;IAC9C,IAAI,CAAC0C,UAAU,GAAG;IAClBR,SAASa,OAAO;IAChB,MAAMF,UAAU,MAAMnB,MAAMnB,QAAQL,MAAM;IAC1C,IAAIwC,UAAU,GAAGR,SAASW,OAAO;GACnC,SAAS3B,QAAQ;IACf,IAAIwB,UAAU,GAAGL,SAASpB,aAAaC,QAAQ,6BAA6B,CAAC;GAC/E,UAAU;IACR,IAAIwB,UAAU,GAAGN,QAAQ,KAAK;GAChC;EACF,EAAA,CAAG;CACL,CAAC;CAED,MAAMjC,UAAU,YAAY;EAC1B,MAAM6C,UAAUzB,MAAM,CAAC,EAAEjC;EACzB,MAAMsD,QAAQL;EACd,MAAMG,kBACJ,CAACF,YACDI,UAAUb,KAAAA,KACVQ,oBAAoBK,SACpBN,mBAAmBM,SACnBjC,KAAK;EACP,IAAI,CAACqC,WAAW,CAACN,UAAU,GAAG;EAC9BN,QAAQ,IAAI;EACZC,SAAS,EAAE;EACX,IAAI;GACF,MAAMP,OAAO,MAAMJ,MAAMnB,QAAQJ,QAAQ6C,OAAO;GAChD,IAAI,CAACN,UAAU,GAAG;GAClBR,SAASJ,IAAI;GAGb,IAAIA,KAAK1C,UAAU,UACjBiD,SAASpB,aAAaa,KAAKlC,OAAO,4BAA4B,CAAC;EAEnE,SAASsB,QAAQ;GACf,IAAI,CAACwB,UAAU,GAAG;GAClBL,SAASpB,aAAaC,QAAQ,4BAA4B,CAAC;GAC3D,MAAMuB,WAAWC,SAAS;EAC5B,UAAU;GACR,IAAIA,UAAU,GAAGN,QAAQ,KAAK;EAChC;CACF;CAEA,MAAM7C,YAAYnC,iBAAiB4B,sBAAsBuC,MAAM,CAAC,EAAEhC,aAAa,EAAE,CAAC;CAClF,MAAM0D,cAAc;EAClB,MAAMC,QAAQ3B,MAAM,CAAC,EAAE/B;EACvB,OAAO0D,QAAQlE,sBAAsBkE,KAAK,IAAI;CAChD;CACA,MAAMC,gBAAgBhB,KAAK,KAAKb,OAAOC,MAAM,CAAC;CAE9C,MAAM6B,qBAAqB;EACzB,MAAML,UAAUxB,MAAM;EACtB,MAAMJ,WAAW,IAAIO,MAAMjB,mBAAmB;EAC9C,IAAI,CAACsC,SAAS,OAAO,GAAGvC,QAAQ,EAAC,GAAIW;EACrC,QAAQ4B,QAAQ3D,OAAhB;GACE,KAAK,YACH,OAAO;GACT,KAAK,aACH,OAAO2D,QAAQzD,mBACX,WAAWyD,QAAQzD,iBAAgB,cACnC;GACN,KAAK;GACL,KAAK,cACH,OAAO;GACT,KAAK,UACH,OAAO,WAAWyD,QAAQ1D,kBAAkB8B,SAAQ;GACtD,SACE,OAAO,GAAGX,QAAQ,EAAC,IAAKuC,QAAQ1D,kBAAkB8B;EACtD;CACF;CAEA,MAAMkC,sBAAsB;EAC1B,MAAMN,UAAUxB,MAAM;EACtB,IAAI,CAACwB,SAASpD,cAAc,CAACoD,QAAQrD,iBAAiB,OAAO;EAC7D,OAAO4D,KAAKC,IAAI,KAAKD,KAAKE,MAAOT,QAAQrD,kBAAkBqD,QAAQpD,aAAc,GAAG,CAAC;CACvF;CAEA,OAAA8D,gBACGtF,QAAM;EAAA,IAACwC,OAAI;GAAA,OAAEA,KAAK;EAAC;EAAEE,cAAcgB;EAAO,IAAA6B,WAAA;GAAA,OAAA,CAAAD,gBACxCjG,MAAI;IAAA,IAACmG,OAAI;KAAA,OAAEjC,MAAMf,SAASoB,KAAAA;IAAS;IAAA,IAAA2B,WAAA;KAAA,OAAAD,gBACjC/E,eAAa;MACZkF,IAAI1F;MACJ2F,SAAO;MACPC,MAAI;MAAA,cAAA;MAAA,iBAAA;MAAA,IAAAJ,WAAA;OAAA,OAAA,CAAAD,gBAIHjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEpC,MAAM,CAAC,EAAEnC,UAAU;QAAW;QAAA,IAAE+B,WAAQ;SAAA,OAAAsC,gBAAG1F,WAAS,EAAA,eAAA,OAAA,CAAA;QAAA;QAAA,IAAA2F,WAAA;SAAA,OAAAD,gBAC7DzF,UAAQ,EAAA,eAAA,OAAA,CAAA;QAAA;OAAA,CAAA,GAAA+F,WAEVX,aAAa,CAAC,CAAA;MAAA;KAAA,CAAA;IAAA;GAAA,CAAA,GAAAK,gBAIlBpF,eAAa;IAAA,KAAA,WAAA;KAAA,OACLQ,GAAG,aAAa6C,MAAMV,KAAK;IAAC;IAAA,IACnCgD,kBAAe;KAAA,OAAEhC,iBAAiBgC;IAAe;IAAA,IACjDC,mBAAgB;KAAA,OAAEjC,iBAAiBiC;IAAgB;IAAA,IAAAP,WAAA;KAAA,OAAA;MAAAD,gBAElDjF,cAAY,EAAA,IAAAkF,WAAA;OAAA,IAAAQ,OAAAC,OAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAG;OAAAC,OAAAJ,OAAAX,gBAGNzF,UAAQ;QAAA,SAAA;QAAA,eAAA;OAAA,CAAA,CAAA;OAAAwG,OAAAF,OAAAb,gBAGRhF,aAAW,EAAAiF,UAAA,oBAAA,CAAA,GAAA,IAAA;OAAAc,OAAAF,OAAAb,gBACXnF,mBAAiB,EAAA,IAAAoF,WAAA;QAAA,OAAA;SAAA;SAAAK,WACVvD,QAAQ,CAAC;SAAA;QAAA;OAAA,EAAA,CAAA,GAAA,IAAA;OAAA,OAAA0D;MAAA,EAAA,CAAA;aAAA;OAAA,IAAAO,QAAAC,QAAA,GAAAE,QAAAH,MAAAJ,WAAAA,YAAAU,QAAAH,MAAAP,WAAAA,WAAAE;OAAAQ,MAAAV;OAAA,IAAAY,QAAAF,MAAAR;OAAAC,OAAAO,aAiBXxD,MAAM,CAAC,EAAElC,kBAAkBqC,MAAMjB,mBAAmB,SAAO,IAAA;OAAA+D,OAAAS,OAAAxB,gBAG5DjG,MAAI;QAAA,IACHmG,OAAI;SAAA,OAAEpC,MAAM,CAAC,EAAEnC,UAAU;QAAS;QAAA,IAClC+B,WAAQ;SAAA,OAAAsC,gBACLjG,MAAI;UAAA,IACHmG,OAAI;WAAA,OAAEI,WAAAxC,MAAM,CAAC,EAAEnC,UAAU,WAAW,CAAA,CAAA,KAAImC,MAAM,CAAC,EAAEjC;UAAgB;UAAA,IACjE6B,WAAQ;WAAA,OACN4C,WAAA,CAAA,CAAA9B,QAAQ,CAAC,CAAA,CAAA,IACL,2DACCP,MAAMhB,sBACP;UAAgE;UAAA,IAAAgD,WAAA;WAAA,OAGrE,qBAAqBnC,MAAM,CAAC,EAAEjC,iBAAgB;UAAa;SAAA,CAAA;QAAA;QAAAoE,UAAA;OAAA,CAAA,CAAA;OAAAc,OAAAI,OAAAnB,gBAQrEvF,QAAM;QACLgH,MAAI;QACJrB,SAAO;QACPC,MAAI;QAAA,IACJqB,WAAQ;SAAA,OAAE,CAAClD,QAAQ,KAAKkB,QAAQ;QAAC;QACjCiC,eAAe,KAAKlF,MAAM;QAAC,IAAAwD,WAAA;SAAA,OAAA,CAAAD,gBAE1BjG,MAAI;UAAA,IAACmG,OAAI;WAAA,OAAER,QAAQ;UAAC;UAAA,IAAEhC,WAAQ;WAAA,OAAAsC,gBAAG1F,WAAS,EAAA,eAAA,OAAA,CAAA;UAAA;UAAA,IAAA2F,WAAA;WAAA,OAAAD,gBACxC3F,cAAY;YAAA,SAAA;YAAA,eAAA;WAAA,CAAA;UAAA;SAAA,CAAA,GAAA,sBAAA;QAAA;OAAA,CAAA,GAAA,IAAA;OAAA0G,OAAAC,OAAAhB,gBAOpBjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEI,WAAAxC,MAAM,CAAC,EAAEnC,UAAU,WAAW,CAAA,CAAA,KAAImC,MAAM,CAAC,EAAEjC;QAAgB;QAAA,IAAAoE,WAAA;SAAA,IAAA2B,SAAAC,QAAA,GAAAC,SAAAF,OAAAhB,YAAAmB,SAAAD,OAAAlB,YAAAoB,SAAAD,OAAAnB,YAAAqB,SAAAD,OAAApB,YAAAsB,SAAAD,OAAAnB;SAAAoB,OAAApB;SAAA,IAAAsB,SAAAJ,OAAAlB,aAAAwB,SAAAF,OAAAxB,WAAAE;SAAAwB,OAAAxB;SAAAC,OAAAiB,QAAAhC,gBAQ5DzF,UAAQ;UAAA,SAAA;UAAA,eAAA;SAAA,CAAA,GAAA0H,MAAA;SAAAlB,OAAAiB,cACAlE,MAAM,CAAC,EAAEjC,kBAAgBqG,MAAA;SAAAnB,OAAAqB,QAGOrF,SAAOuF,MAAA;SAAAvB,OAAAgB,QAAA/B,gBAEjDjG,MAAI;UAAA,IAACmG,OAAI;WAAA,OAAE5E,kBAAkBwC,MAAM,CAAC,EAAE9B,eAAe,IAAI;UAAC;UAAAiE,WACvDuC,oBAAQ;WAAA,IAAAC,SAAAC,QAAA;WAAAD,OAAA7B;WAAAG,OAAA0B,QAC2CD,UAAQ,IAAA;WAAA,OAAAC;UAAA,EAAA,CAAA;SAC5D,CAAA,GAAA,IAAA;SAAA1B,OAAAe,QAAA9B,gBAGJvF,QAAM;UAACgH,MAAI;UAAUpB,MAAI;UAAA,IAAMqB,WAAQ;WAAA,OAAEhC,QAAQ;UAAC;UAAEiC,eAAe,KAAKjF,QAAQ;UAAC,IAAAuD,WAAA;WAAA,OAAA,CAAAD,gBAC/EjG,MAAI;YAAA,IAACmG,OAAI;aAAA,OAAER,QAAQ;YAAC;YAAA,IAAEhC,WAAQ;aAAA,OAAAsC,gBAAG7F,UAAQ,EAAA,eAAA,OAAA,CAAA;YAAA;YAAA,IAAA8F,WAAA;aAAA,OAAAD,gBACvC3F,cAAY;cAAA,SAAA;cAAA,eAAA;aAAA,CAAA;YAAA;WAAA,CAAA,GAAA,qBAAA;UAAA;SAAA,CAAA,GAAA,IAAA;SAAA,OAAAuH;QAAA;OAAA,CAAA,GAAA,IAAA;OAAAb,OAAAC,OAAAhB,gBAYtBjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEpC,MAAM,CAAC,EAAEnC,UAAU,iBAAiBmC,MAAM,CAAC,EAAEnC,UAAU;QAAY;QAAA,IAAAsE,WAAA;SAAA,IAAA2C,SAAAC,QAAA,GAAAC,SAAAF,OAAAhC;SAAAG,OAAA6B,QAAA5C,gBAE1E7E,UAAQ;UAAA,IACPsE,QAAK;WAAA,OAAEG,cAAc,KAAKtB,KAAAA;UAAS;UAAA,KAAA,gBAAA;WAAA,OAEjCR,MAAM,CAAC,EAAEnC,UAAU,eAAe,sBAAsB;UAAoB;SAAA,CAAA,GAAAmH,MAAA;SAAA/B,OAAA+B,QAAA9C,gBAI7EjG,MAAI;UAAA,IACHmG,OAAI;WAAA,OAAEpC,MAAM,CAAC,EAAEnC,UAAU;UAAY;UAAA,IACrC+B,WAAQ;WAAA,OAAAsC,gBACLjG,MAAI;YAAA,IAACmG,OAAI;aAAA,OAAEN,cAAc,MAAM;YAAI;YAAElC,UAAQ;YAAA,IAAAuC,WAAA;aAAA,OAC3C,GAAG5E,YAAYyC,MAAM,CAAC,EAAE7B,mBAAmB,CAAC,EAAC,MAAOZ,YACnDyC,MAAM,CAAC,EAAE5B,cAAc,CACzB,EAAC,KAAM0D,cAAc,EAAC;YAAG;WAAA,CAAA;UAAA;UAAAK,UAAA;SAAA,CAAA,CAAA;SAAA,OAAA2C;QAAA;OAAA,CAAA,GAAA,IAAA;OAAA7B,OAAAC,OAAAhB,gBAUpCjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEpC,MAAM,CAAC,EAAEnC,UAAU;QAAS;QAAA,IAAAsE,WAAA;SAAA,IAAA+C,SAAAC,QAAA,GAAAC,SAAAF,OAAApC;SAAAG,OAAAiC,QAAAhD,gBAEnC9F,OAAK;UAAA,SAAA;UAAA,eAAA;SAAA,CAAA,GAAAgJ,MAAA;SAAAnC,OAAAiC,QACLjG,SAAOmG,MAAA;SAAA,OAAAF;QAAA;OAAA,CAAA,GAAA,IAAA;OAAAjC,OAAAC,OAAAhB,gBAIXjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEpC,MAAM,CAAC,EAAEnC,UAAU;QAAW;QAAA,IAAAsE,WAAA;SAAA,IAAAkD,SAAAC,QAAA;SAAArC,OAAAoC,QAAAnD,gBAErC9F,OAAK;UAAA,SAAA;UAAA,eAAA;SAAA,CAAA,GAAA,IAAA;SAAA6G,OAAAoC,QAAAnD,gBACLjG,MAAI;UAAA,IAACmG,OAAI;WAAA,OAAEpC,MAAM,CAAC,EAAEzB;UAAe;UAAEqB,UAAQ;UAAA,IAAAuC,WAAA;WAAA,OAAA;YAAA;YAAAK,WACnCvD,QAAQ,CAAC;YAAA;WAAA;UAAA;SAAA,CAAA,GAAA,IAAA;SAAA,OAAAoG;QAAA;OAAA,CAAA,GAAA,IAAA;OAAApC,OAAAC,OAAAhB,gBAKvBjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAE/D,MAAM;QAAC;QAAA,IAAA8D,WAAA;SAAA,IAAAoD,SAAAC,QAAA;SAAAvC,OAAAsC,QAKdlH,KAAK;SAAA,OAAAkH;QAAA;OAAA,CAAA,GAAA,IAAA;OAAAtC,OAAAC,OAAAhB,gBAITjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEV,MAAM;QAAC;QAAA,IAAAS,WAAA;SAAA,IAAAsD,SAAAC,QAAA,GAAAC,SAAAF,OAAA3C;SAAA6C,OAAA7C;SAAA,IAAA+C,SAAAF,OAAA3C;SAAAC,OAAA0C,QAAAzD,gBAMZxF,OAAK;UAAC4F,SAAO;UAAUC,MAAI;UAAA,IAAAJ,WAAA;WAAA,OAAA,CAAA,KAAAK,WACxBxC,MAAM,CAAC,EAAEjC,gBAAgB,CAAA;UAAA;SAAA,CAAA,GAAA,IAAA;SAAAkF,OAAA4C,QAY5BnE,KAAK;SAAA,OAAA+D;QAAA;OAAA,CAAA,GAAA,IAAA;OAAAxC,OAAAC,OAAAhB,gBAKXjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEpE,UAAU;QAAC;QAAA,IAAAmE,WAAA;SAAA,IAAA2D,SAAAC,QAAA,GAAAG,SAAAJ,OAAAhD,WAAAE,YAAAA;SAAAC,OAAAiD,QAiBhBlI,SAAS;SAAA,OAAA8H;QAAA;OAAA,CAAA,GAAA,IAAA;OAAA,OAAA5C;MAAA,EAAA,CAAA;MAAAhB,gBAMjBlF,cAAY,EAAA,IAAAmF,WAAA;OAAA,OAAA,CAAAD,gBACVjG,MAAI;QAAA,IAACmG,OAAI;SAAA,OAAEpC,MAAM,CAAC,EAAE1B;QAAU;QAAA6D,WAC3BgE,QAAGjE,gBACFvF,QAAM;SACLgH,MAAI;SACJrB,SAAO;SACPC,MAAI;SAGJsB,eAAeuC,OAAOhH,KAAK+G,IAAI,GAAG,UAAU,qBAAqB;SAAC,IAAAhE,WAAA;UAAA,OAAA,CAAAD,gBAEjE5F,cAAY,EAAA,eAAA,OAAA,CAAA,GAAA,eAAA;SAAA;QAAA,CAAA;OAGhB,CAAA,GAAA4F,gBAEFrF,aAAW;QAACwF,IAAI1F;QAAQ2F,SAAO;QAAWC,MAAI;QAAAJ,UAAA;OAAA,CAAA,CAAA;MAAA,EAAA,CAAA;KAAA;IAAA;GAAA,CAAA,CAAA;EAAA;CAAA,CAAA;AAOzD"}
1
+ {"version":3,"file":"update-dialog.js","names":["createEffect","createMemo","createSignal","onCleanup","onMount","Show","splitProps","Accessor","Check","Download","ExternalLink","LoaderCircle","RefreshCw","Sparkles","Badge","Button","Dialog","DialogClose","DialogContent","DialogDescription","DialogFooter","DialogHeader","DialogTitle","DialogTrigger","createDialogFocusRestoration","Progress","cn","formatBytes","formatReleaseDate","plainTextFromMarkdown","UpdatePhase","UpdateState","Readonly","phase","currentVersion","availableVersion","changelog","releaseNotes","releaseDate","downloadedBytes","totalBytes","error","releaseUrl","restartRequired","UpdateAdapter","getStatus","Promise","check","install","expectedVersion","isDesktopRuntime","UpdateDialogProps","adapter","appName","fallbackVersion","desktopOnlyMessage","open","defaultOpen","onOpenChange","restoreFocusRef","HTMLElement","class","messageText","value","undefined","message","trim","errorMessage","caught","fallback","Error","plainMessage","record","safe","nestedError","isBusy","state","UpdateDialog","props","local","uncontrolledOpen","setUncontrolledOpen","setOpen","next","focusRestoration","desktop","setState","busy","setBusy","setError","checkFailed","setCheckFailed","lifecycleEpoch","activeOpenEpoch","disposed","loadStatus","isCurrent","status","previous","hasActionableSnapshot","candidate","preservesSnapshot","epoch","failCheck","applyCheckResult","current","checked","active","version","notes","working","triggerLabel","progressValue","Math","min","round","_$createComponent","children","when","as","variant","size","_$memo","onOpenAutoFocus","onCloseAutoFocus","_el$","_tmpl$","_el$2","firstChild","_el$3","nextSibling","_$insert","_el$4","_tmpl$9","_el$5","_el$6","_el$7","_el$8","_el$9","_el$0","_el$1","type","disabled","onClick","_el$10","_tmpl$2","_el$11","_el$12","_el$13","_el$14","_el$16","_el$15","_el$17","_el$18","_el$20","_el$19","released","_el$35","_tmpl$0","_el$36","_el$21","_tmpl$3","_el$22","aria-label","_el$23","_tmpl$4","_el$24","_el$25","_tmpl$5","_el$26","_tmpl$6","_el$27","_tmpl$7","_el$28","_el$29","_el$30","_el$31","_tmpl$8","_el$32","_el$33","_el$34","url","window","_$createComponent","_$memo"],"sources":["../../../../src/components/composites/update-dialog/update-dialog.tsx"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n onCleanup,\n onMount,\n Show,\n splitProps,\n} from 'solid-js'\nimport type { Accessor } from 'solid-js'\nimport { Check, Download, ExternalLink, LoaderCircle, RefreshCw, Sparkles } from 'lucide-solid'\nimport { Badge } from '../../ui/badge'\nimport { Button } from '../../ui/button'\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from '../../ui/dialog'\nimport { createDialogFocusRestoration } from '../../ui/dialog/dialog'\nimport { Progress } from '../../ui/progress'\nimport { cn } from '#lib/utils'\nimport { formatBytes, formatReleaseDate, plainTextFromMarkdown } from '#lib/version-notes'\n\n/**\n * UpdateDialog.\n *\n * A software-update surface: the installed version, whether a newer release\n * exists, a download that shows its progress, and the release notes for what is\n * about to arrive.\n *\n * The transport is an adapter, because an update is the one thing a component\n * library genuinely cannot implement: it is signed, downloaded and applied by the\n * application's own updater, and the mechanism differs per shell. What *is*\n * general is the phase machine and the way each phase is presented — and that is\n * where the hard-won detail lives:\n *\n * **A refused install answers with a normal payload, not a rejection.** adea's\n * shell returns a status whose phase is `failed` when a download or an extract\n * fails, rather than throwing. Without handling that, clicking install looked like\n * nothing happened at all — the button spun, the phase never changed, and the\n * error sat unread in a field nobody rendered. The `failed` phase after an install\n * is therefore surfaced as an error, explicitly.\n *\n * **Busy is derived from the snapshot as well as from the click.** An update can\n * be downloading while the dialog is open — started before it opened, or by the\n * shell's own poll — so a local `busy` flag alone would offer an enabled Install\n * button mid-download.\n *\n * **Opening the dialog loads status first, then checks.** Status paints the\n * installed version immediately; the check is a network round trip. Doing them in\n * the other order leaves the dialog blank for as long as the check takes, on a\n * dialog whose first question is always \"what am I running?\".\n *\n * **A failed check is not a current result.** A cached `current` status can only\n * be shown as up to date after the latest check succeeds; a failed check remains\n * retryable and suppresses that claim.\n *\n * **Errors are not always `Error`s.** An adapter can reject with a string or a\n * structured safe message. Unknown object fields are never serialized into the\n * UI; a generic fallback is shown instead.\n *\n * The adapter's `isDesktopRuntime` gate exists because an update is a desktop\n * concern: a browser build has no installer, and the honest thing to show is that\n * updates are available in the desktop application rather than a disabled button\n * with no reason.\n */\n\n/** The phase an update is in. The union is the state machine. */\nexport type UpdatePhase =\n | 'idle'\n | 'checking'\n | 'current'\n | 'available'\n | 'downloading'\n | 'installing'\n | 'installed'\n | 'failed'\n\n/** A snapshot of the updater. Every field the dialog renders, and nothing else. */\nexport type UpdateState = Readonly<{\n phase: UpdatePhase\n /** The version currently installed. */\n currentVersion: string\n /** The version that can be installed, when one can. */\n availableVersion?: string | null\n /** A changelog for the installed channel, as markdown. */\n changelog?: string\n /** Notes for the available release, as markdown. */\n releaseNotes?: string | null\n /** The available release's date, as an ISO string. */\n releaseDate?: string | null\n /** Bytes downloaded so far, during `downloading`. */\n downloadedBytes?: number\n /** Total bytes, when the server sent a length. */\n totalBytes?: number | null\n /** Set when `phase` is `failed`, and sometimes when it is not. */\n error?: string | null\n /** A URL for the release's page. */\n releaseUrl?: string | null\n /** True when the application must restart to finish. */\n restartRequired?: boolean\n}>\n\n/**\n * The updater, as the application exposes it. Three calls, each answering with a\n * full snapshot rather than a delta — an updater's state is small, and a snapshot\n * cannot drift from the shell's own view of it.\n */\nexport type UpdateAdapter = Readonly<{\n /** The current state, without contacting the release channel. */\n getStatus(): Promise<UpdateState>\n /** Ask the release channel, and answer with what it found. */\n check(): Promise<UpdateState>\n /**\n * Download and apply `expectedVersion`.\n *\n * The argument is the version the caller believes is available, so the updater\n * can refuse a request that raced a newer release rather than installing the\n * wrong one.\n */\n install(expectedVersion: string): Promise<UpdateState>\n /** False in a browser build, where there is no installer to drive. */\n isDesktopRuntime(): boolean\n}>\n\nexport type UpdateDialogProps = {\n adapter: UpdateAdapter\n /** The application's name, used in the title and the copy. */\n appName?: string\n /** Shown before the first status arrives, so the dialog is never blank. */\n fallbackVersion?: string\n /** Copy for the adapter's non-desktop case. */\n desktopOnlyMessage?: string\n /** A controlled open state. Omit for the trigger to manage its own. */\n open?: boolean\n /**\n * Start open, uncontrolled — for a caller that opens the dialog because it found\n * something (\"an update is available\") rather than because the user asked. The\n * trigger is still rendered, so the dialog is reachable again after closing.\n */\n defaultOpen?: boolean\n onOpenChange?: (open: boolean) => void\n /**\n * Supplies a stable external control to focus when a controlled dialog closes.\n * Useful when a menu or other layer closes before this dialog opens; when omitted,\n * the element focused immediately before the dialog opens is used when available.\n */\n restoreFocusRef?: Accessor<HTMLElement | undefined>\n /** Render only the panel, for a caller that provides its own dialog. */\n class?: string\n}\n\nfunction messageText(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const message = value.trim()\n return message && message !== '[object Object]' ? message : undefined\n}\n\nfunction errorMessage(caught: unknown, fallback: string): string {\n if (caught instanceof Error) return messageText(caught.message) ?? fallback\n const plainMessage = messageText(caught)\n if (plainMessage) return plainMessage\n if (!caught || typeof caught !== 'object') return fallback\n\n const record = caught as { error?: unknown; safe?: unknown }\n const safe = record.safe as { message?: unknown } | null | undefined\n const nestedError = record.error as { safe?: { message?: unknown } | null } | null | undefined\n return messageText(safe?.message) ?? messageText(nestedError?.safe?.message) ?? fallback\n}\n\n/**\n * The phases during which the updater is working. `downloading` and `installing`\n * are included because the shell can be in either while the dialog is open, and\n * offering an enabled Install button during a download is how a double-install\n * happens.\n */\nfunction isBusy(state: UpdateState | null): boolean {\n return (\n state?.phase === 'checking' || state?.phase === 'downloading' || state?.phase === 'installing'\n )\n}\n\nexport function UpdateDialog(props: UpdateDialogProps) {\n const [local] = splitProps(props, [\n 'adapter',\n 'appName',\n 'fallbackVersion',\n 'desktopOnlyMessage',\n 'open',\n 'defaultOpen',\n 'onOpenChange',\n 'restoreFocusRef',\n 'class',\n ])\n\n const [uncontrolledOpen, setUncontrolledOpen] = createSignal(local.defaultOpen ?? false)\n const open = () => local.open ?? uncontrolledOpen()\n const setOpen = (next: boolean) => {\n if (local.open === undefined) setUncontrolledOpen(next)\n local.onOpenChange?.(next)\n }\n\n const focusRestoration = createDialogFocusRestoration({\n open,\n restoreFocusRef: local.restoreFocusRef,\n })\n\n const appName = () => local.appName ?? 'the application'\n const desktop = () => local.adapter.isDesktopRuntime()\n const [state, setState] = createSignal<UpdateState | null>(null)\n const [busy, setBusy] = createSignal(false)\n const [error, setError] = createSignal('')\n const [checkFailed, setCheckFailed] = createSignal(false)\n let lifecycleEpoch = 0\n let activeOpenEpoch: number | undefined\n let disposed = false\n\n onCleanup(() => {\n disposed = true\n activeOpenEpoch = undefined\n lifecycleEpoch += 1\n })\n\n const loadStatus = async (isCurrent: () => boolean = () => !disposed) => {\n if (!desktop() || !isCurrent()) return\n try {\n const status = await local.adapter.getStatus()\n const previous = state()\n const hasActionableSnapshot = (candidate: UpdateState | null) =>\n candidate?.phase === 'available' || isBusy(candidate)\n const preservesSnapshot =\n previous?.phase === 'available'\n ? status.phase === 'available' || isBusy(status)\n : isBusy(previous) && isBusy(status)\n if (\n isCurrent() &&\n !(checkFailed() && hasActionableSnapshot(previous) && !preservesSnapshot)\n ) {\n setState(status)\n }\n } catch (caught) {\n if (isCurrent()) setError(errorMessage(caught, 'Version status is unavailable'))\n }\n }\n\n onMount(() => {\n if (desktop() && !open()) {\n const epoch = lifecycleEpoch\n void loadStatus(() => !disposed && lifecycleEpoch === epoch)\n }\n })\n\n const failCheck = (caught: unknown, fallback: string) => {\n const message = errorMessage(caught, fallback)\n setCheckFailed(true)\n setError(message)\n }\n\n const applyCheckResult = (next: UpdateState) => {\n if (next.phase === 'failed') {\n const current = state()\n if (!current) setState(next)\n failCheck(next.error, 'Could not check for updates')\n return\n }\n setState(next)\n setCheckFailed(false)\n setError('')\n }\n\n const check = async () => {\n if (!desktop()) {\n setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`)\n return\n }\n const epoch = activeOpenEpoch\n const isCurrent = () =>\n !disposed &&\n epoch !== undefined &&\n activeOpenEpoch === epoch &&\n lifecycleEpoch === epoch &&\n open()\n if (!isCurrent()) return\n setBusy(true)\n setError('')\n setCheckFailed(false)\n try {\n const checked = await local.adapter.check()\n if (isCurrent()) applyCheckResult(checked)\n } catch (caught) {\n if (isCurrent()) {\n failCheck(caught, 'Could not check for updates')\n await loadStatus(isCurrent)\n }\n } finally {\n if (isCurrent()) setBusy(false)\n }\n }\n\n /**\n * Status first, then the check — and an `active` flag, because the dialog can\n * close while the check is in flight and a late `setState` would write into a\n * disposed scope.\n */\n createEffect(() => {\n if (!open() || !desktop()) {\n if (activeOpenEpoch !== undefined) {\n activeOpenEpoch = undefined\n lifecycleEpoch += 1\n }\n return\n }\n const epoch = ++lifecycleEpoch\n activeOpenEpoch = epoch\n let active = true\n const isCurrent = () =>\n active && !disposed && activeOpenEpoch === epoch && lifecycleEpoch === epoch && open()\n onCleanup(() => {\n active = false\n if (activeOpenEpoch === epoch) {\n activeOpenEpoch = undefined\n lifecycleEpoch += 1\n }\n })\n setError('')\n setCheckFailed(false)\n setBusy(true)\n void (async () => {\n try {\n const current = await local.adapter.getStatus()\n if (!isCurrent()) return\n setState(current)\n const checked = await local.adapter.check()\n if (isCurrent()) applyCheckResult(checked)\n } catch (caught) {\n if (isCurrent()) failCheck(caught, 'Could not check for updates')\n } finally {\n if (isCurrent()) setBusy(false)\n }\n })()\n })\n\n const install = async () => {\n const version = state()?.availableVersion\n const epoch = activeOpenEpoch\n const isCurrent = () =>\n !disposed &&\n epoch !== undefined &&\n activeOpenEpoch === epoch &&\n lifecycleEpoch === epoch &&\n open()\n if (!version || !isCurrent()) return\n setBusy(true)\n setError('')\n try {\n const next = await local.adapter.install(version)\n if (!isCurrent()) return\n setState(next)\n // A refused install answers with a normal payload whose phase is `failed`.\n // Without this the click looked like nothing happened.\n if (next.phase === 'failed') {\n setError(errorMessage(next.error, 'Update installation failed'))\n } else {\n setCheckFailed(false)\n }\n } catch (caught) {\n if (!isCurrent()) return\n setError(errorMessage(caught, 'Update installation failed'))\n await loadStatus(isCurrent)\n } finally {\n if (isCurrent()) setBusy(false)\n }\n }\n\n const changelog = createMemo(() => plainTextFromMarkdown(state()?.changelog ?? ''))\n const notes = () => {\n const value = state()?.releaseNotes\n return value ? plainTextFromMarkdown(value) : ''\n }\n const working = () => busy() || isBusy(state())\n const isCurrent = () => state()?.phase === 'current' && !error() && !checkFailed() && !working()\n\n const triggerLabel = () => {\n const current = state()\n const fallback = `v${local.fallbackVersion ?? '0.1.0'}`\n if (checkFailed()) return `Version ${current?.currentVersion || fallback} · Retry`\n if (!current) return `${appName()} ${fallback}`\n switch (current.phase) {\n case 'checking':\n return 'Checking for updates…'\n case 'available':\n return current.availableVersion\n ? `Update v${current.availableVersion} available`\n : 'Update available'\n case 'downloading':\n case 'installing':\n return 'Installing update…'\n case 'failed':\n return `Version ${current.currentVersion || fallback} · Retry`\n default:\n return `${appName()} v${current.currentVersion || fallback}`\n }\n }\n\n const progressValue = () => {\n const current = state()\n if (!current?.totalBytes || !current.downloadedBytes) return null\n return Math.min(100, Math.round((current.downloadedBytes / current.totalBytes) * 100))\n }\n\n return (\n <Dialog open={open()} onOpenChange={setOpen}>\n <Show when={local.open === undefined}>\n <DialogTrigger\n as={Button}\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"Open version and updates\"\n aria-haspopup=\"dialog\"\n >\n <Show when={state()?.phase === 'available'} fallback={<RefreshCw aria-hidden=\"true\" />}>\n <Sparkles aria-hidden=\"true\" />\n </Show>\n {triggerLabel()}\n </DialogTrigger>\n </Show>\n\n <DialogContent\n class={cn('max-w-3xl', local.class)}\n onOpenAutoFocus={focusRestoration.onOpenAutoFocus}\n onCloseAutoFocus={focusRestoration.onCloseAutoFocus}\n >\n <DialogHeader>\n <div class=\"flex items-center gap-3\">\n <span class=\"flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground\">\n <Sparkles class=\"size-5\" aria-hidden=\"true\" />\n </span>\n <div>\n <DialogTitle>Version &amp; updates</DialogTitle>\n <DialogDescription>\n Keep {appName()} current and review what changed in each release.\n </DialogDescription>\n </div>\n </div>\n </DialogHeader>\n\n <div class=\"flex min-h-0 flex-col gap-5 overflow-y-auto p-6\">\n <section\n class=\"rounded-xl border border-border bg-background/45 p-4\"\n aria-label=\"Version status\"\n >\n <div class=\"flex flex-wrap items-start justify-between gap-4\">\n <div class=\"flex flex-col gap-1\">\n <p class=\"text-2xs font-semibold tracking-widest text-muted-foreground uppercase\">\n Installed version\n </p>\n <p class=\"text-xl font-semibold tracking-tight\">\n v{state()?.currentVersion || local.fallbackVersion || '0.1.0'}\n </p>\n <p class=\"text-sm text-muted-foreground\">\n <Show\n when={isCurrent()}\n fallback={\n <Show\n when={state()?.phase === 'available' && state()?.availableVersion}\n fallback={\n desktop()\n ? 'Check the release channel for the latest signed build.'\n : (local.desktopOnlyMessage ??\n `Open this dialog inside the desktop app to check for updates.`)\n }\n >\n {`A newer release, v${state()?.availableVersion}, is ready.`}\n </Show>\n }\n >\n You are running the latest release.\n </Show>\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={!desktop() || working()}\n onClick={() => void check()}\n >\n <Show when={working()} fallback={<RefreshCw aria-hidden=\"true\" />}>\n <LoaderCircle class=\"animate-spin\" aria-hidden=\"true\" />\n </Show>\n {checkFailed() ? 'Retry update check' : 'Check latest version'}\n </Button>\n </div>\n </section>\n\n <Show when={state()?.phase === 'available' && state()?.availableVersion}>\n <section\n class=\"rounded-xl border border-primary/35 bg-primary-subtle p-4\"\n aria-label=\"Available update\"\n >\n <div class=\"flex flex-wrap items-start justify-between gap-4\">\n <div class=\"flex flex-col gap-1\">\n <p class=\"flex items-center gap-2 text-sm font-semibold\">\n <Sparkles class=\"size-4 text-primary\" aria-hidden=\"true\" />\n Version {state()?.availableVersion} is ready\n </p>\n <p class=\"text-sm text-muted-foreground\">\n The signed installer is verified before {appName()} restarts.\n </p>\n <Show when={formatReleaseDate(state()?.releaseDate ?? null)}>\n {(released) => (\n <p class=\"text-xs text-muted-foreground\">Released {released()}</p>\n )}\n </Show>\n </div>\n <Button type=\"button\" size=\"sm\" disabled={working()} onClick={() => void install()}>\n <Show when={working()} fallback={<Download aria-hidden=\"true\" />}>\n <LoaderCircle class=\"animate-spin\" aria-hidden=\"true\" />\n </Show>\n Install and restart\n </Button>\n </div>\n </section>\n </Show>\n\n {/*\n Progress with both numbers: a bar alone does not distinguish a stalled\n download from a slow one, and \"12.4 MB of 48.1 MB\" does.\n */}\n <Show when={state()?.phase === 'downloading' || state()?.phase === 'installing'}>\n <section class=\"grid gap-2\" aria-label=\"Update progress\">\n <Progress\n value={progressValue() ?? undefined}\n aria-label={\n state()?.phase === 'installing' ? 'Installing update' : 'Downloading update'\n }\n />\n <p class=\"text-xs text-muted-foreground\" role=\"status\">\n <Show\n when={state()?.phase === 'installing'}\n fallback={\n <Show when={progressValue() !== null} fallback=\"Downloading…\">\n {`${formatBytes(state()?.downloadedBytes ?? 0)} of ${formatBytes(\n state()?.totalBytes ?? 0\n )} · ${progressValue()}%`}\n </Show>\n }\n >\n Applying the update…\n </Show>\n </p>\n </section>\n </Show>\n\n <Show when={isCurrent()}>\n <p class=\"text-foreground flex items-center gap-2 text-sm\" role=\"status\">\n <Check class=\"size-4 text-success\" aria-hidden=\"true\" />\n {appName()} is up to date.\n </p>\n </Show>\n\n <Show when={state()?.phase === 'installed'}>\n <p class=\"text-foreground flex items-center gap-2 text-sm\" role=\"status\">\n <Check class=\"size-4 text-success\" aria-hidden=\"true\" />\n <Show when={state()?.restartRequired} fallback=\"The update is applied.\">\n Restart {appName()} to finish the update.\n </Show>\n </p>\n </Show>\n\n <Show when={error()}>\n <p\n class=\"text-foreground rounded-lg border border-destructive/35 bg-destructive-subtle px-3 py-2 text-sm\"\n role=\"alert\"\n >\n {error()}\n </p>\n </Show>\n\n <Show when={notes()}>\n <div class=\"flex flex-col gap-2\">\n <div class=\"flex items-center gap-2\">\n <h2 id=\"update-release-notes\" class=\"text-sm font-semibold\">\n What changed in this release\n </h2>\n <Badge variant=\"subtle\" size=\"sm\">\n v{state()?.availableVersion}\n </Badge>\n </div>\n {/* A long text block with no focusable descendant: it needs its own tab\n stop, named by the heading above it, or a keyboard user cannot\n scroll it (`scrollable-region-focusable`). */}\n <div\n class=\"max-h-52 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground\"\n role=\"region\"\n aria-labelledby=\"update-release-notes\"\n tabindex=\"0\"\n >\n {notes()}\n </div>\n </div>\n </Show>\n\n <Show when={changelog()}>\n <div class=\"flex flex-col gap-2\">\n <h2 id=\"update-changelog\" class=\"text-sm font-semibold\">\n Installed changelog\n </h2>\n <p class=\"text-xs text-muted-foreground\">\n A plain-text history of the installed release channel.\n </p>\n {/* A long text block with no focusable descendant: it needs its own tab\n stop, named by the heading above it, or a keyboard user cannot\n scroll it (`scrollable-region-focusable`). */}\n <div\n class=\"max-h-72 overflow-y-auto rounded-xl border border-border bg-background/45 p-4 font-mono text-xs leading-5 whitespace-pre-wrap text-muted-foreground\"\n role=\"region\"\n aria-labelledby=\"update-changelog\"\n tabindex=\"0\"\n >\n {changelog()}\n </div>\n </div>\n </Show>\n </div>\n\n <DialogFooter>\n <Show when={state()?.releaseUrl}>\n {(url) => (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n // `noopener,noreferrer` because the release page is a different\n // origin and must not get a handle on this window.\n onClick={() => window.open(url(), '_blank', 'noopener,noreferrer')}\n >\n <ExternalLink aria-hidden=\"true\" />\n View releases\n </Button>\n )}\n </Show>\n <DialogClose as={Button} variant=\"outline\" size=\"sm\">\n Close\n </DialogClose>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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"}
@@ -56,9 +56,13 @@ import { formatBytes, formatReleaseDate, plainTextFromMarkdown } from '../../../
56
56
  * the other order leaves the dialog blank for as long as the check takes, on a
57
57
  * dialog whose first question is always "what am I running?".
58
58
  *
59
- * **Errors are not always `Error`s.** An adapter can reject with a string, and a
60
- * shell can answer with an error field. Both are read, and a fallback is shown
61
- * rather than an empty alert.
59
+ * **A failed check is not a current result.** A cached `current` status can only
60
+ * be shown as up to date after the latest check succeeds; a failed check remains
61
+ * retryable and suppresses that claim.
62
+ *
63
+ * **Errors are not always `Error`s.** An adapter can reject with a string or a
64
+ * structured safe message. Unknown object fields are never serialized into the
65
+ * UI; a generic fallback is shown instead.
62
66
  *
63
67
  * The adapter's `isDesktopRuntime` gate exists because an update is a desktop
64
68
  * concern: a browser build has no installer, and the honest thing to show is that
@@ -151,10 +155,22 @@ export type UpdateDialogProps = {
151
155
  class?: string
152
156
  }
153
157
 
158
+ function messageText(value: unknown): string | undefined {
159
+ if (typeof value !== 'string') return undefined
160
+ const message = value.trim()
161
+ return message && message !== '[object Object]' ? message : undefined
162
+ }
163
+
154
164
  function errorMessage(caught: unknown, fallback: string): string {
155
- if (caught instanceof Error && caught.message) return caught.message
156
- if (typeof caught === 'string' && caught) return caught
157
- return fallback
165
+ if (caught instanceof Error) return messageText(caught.message) ?? fallback
166
+ const plainMessage = messageText(caught)
167
+ if (plainMessage) return plainMessage
168
+ if (!caught || typeof caught !== 'object') return fallback
169
+
170
+ const record = caught as { error?: unknown; safe?: unknown }
171
+ const safe = record.safe as { message?: unknown } | null | undefined
172
+ const nestedError = record.error as { safe?: { message?: unknown } | null } | null | undefined
173
+ return messageText(safe?.message) ?? messageText(nestedError?.safe?.message) ?? fallback
158
174
  }
159
175
 
160
176
  /**
@@ -199,6 +215,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
199
215
  const [state, setState] = createSignal<UpdateState | null>(null)
200
216
  const [busy, setBusy] = createSignal(false)
201
217
  const [error, setError] = createSignal('')
218
+ const [checkFailed, setCheckFailed] = createSignal(false)
202
219
  let lifecycleEpoch = 0
203
220
  let activeOpenEpoch: number | undefined
204
221
  let disposed = false
@@ -213,7 +230,19 @@ export function UpdateDialog(props: UpdateDialogProps) {
213
230
  if (!desktop() || !isCurrent()) return
214
231
  try {
215
232
  const status = await local.adapter.getStatus()
216
- if (isCurrent()) setState(status)
233
+ const previous = state()
234
+ const hasActionableSnapshot = (candidate: UpdateState | null) =>
235
+ candidate?.phase === 'available' || isBusy(candidate)
236
+ const preservesSnapshot =
237
+ previous?.phase === 'available'
238
+ ? status.phase === 'available' || isBusy(status)
239
+ : isBusy(previous) && isBusy(status)
240
+ if (
241
+ isCurrent() &&
242
+ !(checkFailed() && hasActionableSnapshot(previous) && !preservesSnapshot)
243
+ ) {
244
+ setState(status)
245
+ }
217
246
  } catch (caught) {
218
247
  if (isCurrent()) setError(errorMessage(caught, 'Version status is unavailable'))
219
248
  }
@@ -226,6 +255,24 @@ export function UpdateDialog(props: UpdateDialogProps) {
226
255
  }
227
256
  })
228
257
 
258
+ const failCheck = (caught: unknown, fallback: string) => {
259
+ const message = errorMessage(caught, fallback)
260
+ setCheckFailed(true)
261
+ setError(message)
262
+ }
263
+
264
+ const applyCheckResult = (next: UpdateState) => {
265
+ if (next.phase === 'failed') {
266
+ const current = state()
267
+ if (!current) setState(next)
268
+ failCheck(next.error, 'Could not check for updates')
269
+ return
270
+ }
271
+ setState(next)
272
+ setCheckFailed(false)
273
+ setError('')
274
+ }
275
+
229
276
  const check = async () => {
230
277
  if (!desktop()) {
231
278
  setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`)
@@ -241,13 +288,15 @@ export function UpdateDialog(props: UpdateDialogProps) {
241
288
  if (!isCurrent()) return
242
289
  setBusy(true)
243
290
  setError('')
291
+ setCheckFailed(false)
244
292
  try {
245
293
  const checked = await local.adapter.check()
246
- if (isCurrent()) setState(checked)
294
+ if (isCurrent()) applyCheckResult(checked)
247
295
  } catch (caught) {
248
- if (!isCurrent()) return
249
- setError(errorMessage(caught, 'Could not check for updates'))
250
- await loadStatus(isCurrent)
296
+ if (isCurrent()) {
297
+ failCheck(caught, 'Could not check for updates')
298
+ await loadStatus(isCurrent)
299
+ }
251
300
  } finally {
252
301
  if (isCurrent()) setBusy(false)
253
302
  }
@@ -279,6 +328,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
279
328
  }
280
329
  })
281
330
  setError('')
331
+ setCheckFailed(false)
282
332
  setBusy(true)
283
333
  void (async () => {
284
334
  try {
@@ -286,9 +336,9 @@ export function UpdateDialog(props: UpdateDialogProps) {
286
336
  if (!isCurrent()) return
287
337
  setState(current)
288
338
  const checked = await local.adapter.check()
289
- if (isCurrent()) setState(checked)
339
+ if (isCurrent()) applyCheckResult(checked)
290
340
  } catch (caught) {
291
- if (isCurrent()) setError(errorMessage(caught, 'Could not check for updates'))
341
+ if (isCurrent()) failCheck(caught, 'Could not check for updates')
292
342
  } finally {
293
343
  if (isCurrent()) setBusy(false)
294
344
  }
@@ -315,6 +365,8 @@ export function UpdateDialog(props: UpdateDialogProps) {
315
365
  // Without this the click looked like nothing happened.
316
366
  if (next.phase === 'failed') {
317
367
  setError(errorMessage(next.error, 'Update installation failed'))
368
+ } else {
369
+ setCheckFailed(false)
318
370
  }
319
371
  } catch (caught) {
320
372
  if (!isCurrent()) return
@@ -331,10 +383,12 @@ export function UpdateDialog(props: UpdateDialogProps) {
331
383
  return value ? plainTextFromMarkdown(value) : ''
332
384
  }
333
385
  const working = () => busy() || isBusy(state())
386
+ const isCurrent = () => state()?.phase === 'current' && !error() && !checkFailed() && !working()
334
387
 
335
388
  const triggerLabel = () => {
336
389
  const current = state()
337
390
  const fallback = `v${local.fallbackVersion ?? '0.1.0'}`
391
+ if (checkFailed()) return `Version ${current?.currentVersion || fallback} · Retry`
338
392
  if (!current) return `${appName()} ${fallback}`
339
393
  switch (current.phase) {
340
394
  case 'checking':
@@ -410,7 +464,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
410
464
  </p>
411
465
  <p class="text-sm text-muted-foreground">
412
466
  <Show
413
- when={state()?.phase === 'current'}
467
+ when={isCurrent()}
414
468
  fallback={
415
469
  <Show
416
470
  when={state()?.phase === 'available' && state()?.availableVersion}
@@ -439,7 +493,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
439
493
  <Show when={working()} fallback={<RefreshCw aria-hidden="true" />}>
440
494
  <LoaderCircle class="animate-spin" aria-hidden="true" />
441
495
  </Show>
442
- Check latest version
496
+ {checkFailed() ? 'Retry update check' : 'Check latest version'}
443
497
  </Button>
444
498
  </div>
445
499
  </section>
@@ -503,7 +557,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
503
557
  </section>
504
558
  </Show>
505
559
 
506
- <Show when={state()?.phase === 'current'}>
560
+ <Show when={isCurrent()}>
507
561
  <p class="text-foreground flex items-center gap-2 text-sm" role="status">
508
562
  <Check class="size-4 text-success" aria-hidden="true" />
509
563
  {appName()} is up to date.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.72.3",
3
+ "version": "0.72.5",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -56,9 +56,13 @@ import { formatBytes, formatReleaseDate, plainTextFromMarkdown } from '#lib/vers
56
56
  * the other order leaves the dialog blank for as long as the check takes, on a
57
57
  * dialog whose first question is always "what am I running?".
58
58
  *
59
- * **Errors are not always `Error`s.** An adapter can reject with a string, and a
60
- * shell can answer with an error field. Both are read, and a fallback is shown
61
- * rather than an empty alert.
59
+ * **A failed check is not a current result.** A cached `current` status can only
60
+ * be shown as up to date after the latest check succeeds; a failed check remains
61
+ * retryable and suppresses that claim.
62
+ *
63
+ * **Errors are not always `Error`s.** An adapter can reject with a string or a
64
+ * structured safe message. Unknown object fields are never serialized into the
65
+ * UI; a generic fallback is shown instead.
62
66
  *
63
67
  * The adapter's `isDesktopRuntime` gate exists because an update is a desktop
64
68
  * concern: a browser build has no installer, and the honest thing to show is that
@@ -151,10 +155,22 @@ export type UpdateDialogProps = {
151
155
  class?: string
152
156
  }
153
157
 
158
+ function messageText(value: unknown): string | undefined {
159
+ if (typeof value !== 'string') return undefined
160
+ const message = value.trim()
161
+ return message && message !== '[object Object]' ? message : undefined
162
+ }
163
+
154
164
  function errorMessage(caught: unknown, fallback: string): string {
155
- if (caught instanceof Error && caught.message) return caught.message
156
- if (typeof caught === 'string' && caught) return caught
157
- return fallback
165
+ if (caught instanceof Error) return messageText(caught.message) ?? fallback
166
+ const plainMessage = messageText(caught)
167
+ if (plainMessage) return plainMessage
168
+ if (!caught || typeof caught !== 'object') return fallback
169
+
170
+ const record = caught as { error?: unknown; safe?: unknown }
171
+ const safe = record.safe as { message?: unknown } | null | undefined
172
+ const nestedError = record.error as { safe?: { message?: unknown } | null } | null | undefined
173
+ return messageText(safe?.message) ?? messageText(nestedError?.safe?.message) ?? fallback
158
174
  }
159
175
 
160
176
  /**
@@ -199,6 +215,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
199
215
  const [state, setState] = createSignal<UpdateState | null>(null)
200
216
  const [busy, setBusy] = createSignal(false)
201
217
  const [error, setError] = createSignal('')
218
+ const [checkFailed, setCheckFailed] = createSignal(false)
202
219
  let lifecycleEpoch = 0
203
220
  let activeOpenEpoch: number | undefined
204
221
  let disposed = false
@@ -213,7 +230,19 @@ export function UpdateDialog(props: UpdateDialogProps) {
213
230
  if (!desktop() || !isCurrent()) return
214
231
  try {
215
232
  const status = await local.adapter.getStatus()
216
- if (isCurrent()) setState(status)
233
+ const previous = state()
234
+ const hasActionableSnapshot = (candidate: UpdateState | null) =>
235
+ candidate?.phase === 'available' || isBusy(candidate)
236
+ const preservesSnapshot =
237
+ previous?.phase === 'available'
238
+ ? status.phase === 'available' || isBusy(status)
239
+ : isBusy(previous) && isBusy(status)
240
+ if (
241
+ isCurrent() &&
242
+ !(checkFailed() && hasActionableSnapshot(previous) && !preservesSnapshot)
243
+ ) {
244
+ setState(status)
245
+ }
217
246
  } catch (caught) {
218
247
  if (isCurrent()) setError(errorMessage(caught, 'Version status is unavailable'))
219
248
  }
@@ -226,6 +255,24 @@ export function UpdateDialog(props: UpdateDialogProps) {
226
255
  }
227
256
  })
228
257
 
258
+ const failCheck = (caught: unknown, fallback: string) => {
259
+ const message = errorMessage(caught, fallback)
260
+ setCheckFailed(true)
261
+ setError(message)
262
+ }
263
+
264
+ const applyCheckResult = (next: UpdateState) => {
265
+ if (next.phase === 'failed') {
266
+ const current = state()
267
+ if (!current) setState(next)
268
+ failCheck(next.error, 'Could not check for updates')
269
+ return
270
+ }
271
+ setState(next)
272
+ setCheckFailed(false)
273
+ setError('')
274
+ }
275
+
229
276
  const check = async () => {
230
277
  if (!desktop()) {
231
278
  setError(local.desktopOnlyMessage ?? `Update checks are available from the desktop app.`)
@@ -241,13 +288,15 @@ export function UpdateDialog(props: UpdateDialogProps) {
241
288
  if (!isCurrent()) return
242
289
  setBusy(true)
243
290
  setError('')
291
+ setCheckFailed(false)
244
292
  try {
245
293
  const checked = await local.adapter.check()
246
- if (isCurrent()) setState(checked)
294
+ if (isCurrent()) applyCheckResult(checked)
247
295
  } catch (caught) {
248
- if (!isCurrent()) return
249
- setError(errorMessage(caught, 'Could not check for updates'))
250
- await loadStatus(isCurrent)
296
+ if (isCurrent()) {
297
+ failCheck(caught, 'Could not check for updates')
298
+ await loadStatus(isCurrent)
299
+ }
251
300
  } finally {
252
301
  if (isCurrent()) setBusy(false)
253
302
  }
@@ -279,6 +328,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
279
328
  }
280
329
  })
281
330
  setError('')
331
+ setCheckFailed(false)
282
332
  setBusy(true)
283
333
  void (async () => {
284
334
  try {
@@ -286,9 +336,9 @@ export function UpdateDialog(props: UpdateDialogProps) {
286
336
  if (!isCurrent()) return
287
337
  setState(current)
288
338
  const checked = await local.adapter.check()
289
- if (isCurrent()) setState(checked)
339
+ if (isCurrent()) applyCheckResult(checked)
290
340
  } catch (caught) {
291
- if (isCurrent()) setError(errorMessage(caught, 'Could not check for updates'))
341
+ if (isCurrent()) failCheck(caught, 'Could not check for updates')
292
342
  } finally {
293
343
  if (isCurrent()) setBusy(false)
294
344
  }
@@ -315,6 +365,8 @@ export function UpdateDialog(props: UpdateDialogProps) {
315
365
  // Without this the click looked like nothing happened.
316
366
  if (next.phase === 'failed') {
317
367
  setError(errorMessage(next.error, 'Update installation failed'))
368
+ } else {
369
+ setCheckFailed(false)
318
370
  }
319
371
  } catch (caught) {
320
372
  if (!isCurrent()) return
@@ -331,10 +383,12 @@ export function UpdateDialog(props: UpdateDialogProps) {
331
383
  return value ? plainTextFromMarkdown(value) : ''
332
384
  }
333
385
  const working = () => busy() || isBusy(state())
386
+ const isCurrent = () => state()?.phase === 'current' && !error() && !checkFailed() && !working()
334
387
 
335
388
  const triggerLabel = () => {
336
389
  const current = state()
337
390
  const fallback = `v${local.fallbackVersion ?? '0.1.0'}`
391
+ if (checkFailed()) return `Version ${current?.currentVersion || fallback} · Retry`
338
392
  if (!current) return `${appName()} ${fallback}`
339
393
  switch (current.phase) {
340
394
  case 'checking':
@@ -410,7 +464,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
410
464
  </p>
411
465
  <p class="text-sm text-muted-foreground">
412
466
  <Show
413
- when={state()?.phase === 'current'}
467
+ when={isCurrent()}
414
468
  fallback={
415
469
  <Show
416
470
  when={state()?.phase === 'available' && state()?.availableVersion}
@@ -439,7 +493,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
439
493
  <Show when={working()} fallback={<RefreshCw aria-hidden="true" />}>
440
494
  <LoaderCircle class="animate-spin" aria-hidden="true" />
441
495
  </Show>
442
- Check latest version
496
+ {checkFailed() ? 'Retry update check' : 'Check latest version'}
443
497
  </Button>
444
498
  </div>
445
499
  </section>
@@ -503,7 +557,7 @@ export function UpdateDialog(props: UpdateDialogProps) {
503
557
  </section>
504
558
  </Show>
505
559
 
506
- <Show when={state()?.phase === 'current'}>
560
+ <Show when={isCurrent()}>
507
561
  <p class="text-foreground flex items-center gap-2 text-sm" role="status">
508
562
  <Check class="size-4 text-success" aria-hidden="true" />
509
563
  {appName()} is up to date.