Skip to content

Commit 770862f

Browse files
committed
feat(desktop): download updates in background
1 parent b297744 commit 770862f

4 files changed

Lines changed: 24 additions & 12 deletions

File tree

apps/desktop/src/updates/DesktopUpdates.test.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ describe("DesktopUpdates", () => {
274274
}).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
275275
});
276276

277-
it.effect("updates and broadcasts state from updater events", () => {
277+
it.effect("downloads and broadcasts an available update immediately", () => {
278278
const harness = makeHarness();
279279

280280
return Effect.scoped(
@@ -286,10 +286,10 @@ describe("DesktopUpdates", () => {
286286
yield* flushCallbacks;
287287

288288
const state = yield* updates.getState;
289-
assert.equal(state.status, "available");
289+
assert.equal(state.status, "downloading");
290290
assert.equal(state.availableVersion, "1.2.4");
291291
assert.isNotNull(state.checkedAt);
292-
assert.equal(harness.sentStates.at(-1)?.status, "available");
292+
assert.equal(harness.sentStates.at(-1)?.status, "downloading");
293293
}),
294294
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
295295
});
@@ -321,7 +321,7 @@ describe("DesktopUpdates", () => {
321321
yield* flushCallbacks;
322322

323323
const state = yield* updates.getState;
324-
assert.equal(state.status, "available");
324+
assert.equal(state.status, "downloading");
325325
assert.deepEqual(state.releaseNotes, [
326326
{
327327
version: "1.2.4-nightly.20260709.766",
@@ -407,7 +407,7 @@ describe("DesktopUpdates", () => {
407407
);
408408
});
409409

410-
it.effect("recovers download state after an unexpected setup failure", () => {
410+
it.effect("leaves an automatic download available to retry after setup failure", () => {
411411
let disableDifferentialCalls = 0;
412412
const harness = makeHarness({
413413
setDisableDifferentialDownload: Effect.suspend(() => {
@@ -425,10 +425,6 @@ describe("DesktopUpdates", () => {
425425
harness.emit("update-available", { version: "1.2.4" });
426426
yield* flushCallbacks;
427427

428-
const result = yield* updates.download;
429-
assert.isTrue(result.accepted);
430-
assert.isFalse(result.completed);
431-
432428
const failedState = yield* updates.getState;
433429
assert.equal(failedState.status, "available");
434430
assert.equal(failedState.errorContext, "download");
@@ -451,6 +447,9 @@ describe("DesktopUpdates", () => {
451447
return Effect.void;
452448
}
453449
if (disableDifferentialCalls === 2) {
450+
return Effect.die(new Error("automatic download setup failed"));
451+
}
452+
if (disableDifferentialCalls === 3) {
454453
return Deferred.succeed(actionStarted, undefined).pipe(Effect.andThen(Effect.never));
455454
}
456455
return Effect.void;
@@ -470,7 +469,10 @@ describe("DesktopUpdates", () => {
470469

471470
const interruptedState = yield* updates.getState;
472471
assert.equal(interruptedState.status, "available");
473-
assert.isNull(interruptedState.message);
472+
assert.equal(
473+
interruptedState.message,
474+
"Desktop update download action failed unexpectedly.",
475+
);
474476

475477
const retry = yield* updates.download;
476478
assert.isTrue(retry.accepted);

apps/desktop/src/updates/DesktopUpdates.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,14 +575,16 @@ export const make = Effect.gen(function* () {
575575

576576
const checkedAt = yield* currentIsoTimestamp;
577577
const releaseNotes = normalizeDesktopUpdateReleaseNotes(info.releaseNotes, info.version);
578-
yield* setState(
578+
yield* Ref.set(
579+
updateStateRef,
579580
reduceDesktopUpdateStateOnUpdateAvailable(state, info.version, checkedAt, releaseNotes),
580581
);
581582
yield* Ref.set(lastLoggedDownloadMilestoneRef, -1);
582583
yield* logUpdaterInfo("update available", {
583584
version: info.version,
584585
releaseNoteGroups: releaseNotes.length,
585586
});
587+
yield* downloadAvailableUpdate;
586588
}),
587589
),
588590
Effect.catchCause((cause) => {

apps/web/src/components/sidebar/SidebarUpdatePill.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ function SidebarUpdateControl() {
243243
type="button"
244244
aria-label={tooltip}
245245
aria-disabled={disabled || isActionPending || undefined}
246-
disabled={disabled || isActionPending}
246+
disabled={isActionPending || (!isUpdateState && disabled)}
247247
className={cn(
248248
"inline-flex size-8 items-center justify-center rounded-full outline-hidden ring-ring transition-colors enabled:cursor-pointer focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-60",
249249
isUpdateState
@@ -254,6 +254,8 @@ function SidebarUpdateControl() {
254254
>
255255
{action === "install" ? (
256256
<RotateCwIcon className="size-4" />
257+
) : isDownloading ? (
258+
<RefreshCwIcon className="size-4 animate-spin" />
257259
) : isUpdateState ? (
258260
<DownloadIcon className="size-4" />
259261
) : (

docs/user/updating.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ connection will disappear briefly and work that is still running may be interrup
2020

2121
The update does not remove saved threads, settings, or project files.
2222

23+
## Desktop App Updates
24+
25+
The desktop app downloads its own updates in the background. Once an update is ready, select
26+
**Restart to update** when you are ready to close T3 Code. T3 Code always asks for confirmation
27+
before restarting, and a failed background download remains available to retry.
28+
2329
## Choose the Action You See
2430

2531
| Action | What to do |

0 commit comments

Comments
 (0)