Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe("DesktopUpdates", () => {
}).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

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

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

const state = yield* updates.getState;
assert.equal(state.status, "available");
assert.equal(state.status, "downloading");
assert.equal(state.availableVersion, "1.2.4");
assert.isNotNull(state.checkedAt);
assert.equal(harness.sentStates.at(-1)?.status, "available");
assert.equal(harness.sentStates.at(-1)?.status, "downloading");
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});
Expand Down Expand Up @@ -321,7 +321,7 @@ describe("DesktopUpdates", () => {
yield* flushCallbacks;

const state = yield* updates.getState;
assert.equal(state.status, "available");
assert.equal(state.status, "downloading");
assert.deepEqual(state.releaseNotes, [
{
version: "1.2.4-nightly.20260709.766",
Expand Down Expand Up @@ -407,7 +407,7 @@ describe("DesktopUpdates", () => {
);
});

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

const result = yield* updates.download;
assert.isTrue(result.accepted);
assert.isFalse(result.completed);

const failedState = yield* updates.getState;
assert.equal(failedState.status, "available");
assert.equal(failedState.errorContext, "download");
Expand All @@ -451,6 +447,9 @@ describe("DesktopUpdates", () => {
return Effect.void;
}
if (disableDifferentialCalls === 2) {
return Effect.die(new Error("automatic download setup failed"));
}
if (disableDifferentialCalls === 3) {
return Deferred.succeed(actionStarted, undefined).pipe(Effect.andThen(Effect.never));
}
return Effect.void;
Expand All @@ -470,7 +469,10 @@ describe("DesktopUpdates", () => {

const interruptedState = yield* updates.getState;
assert.equal(interruptedState.status, "available");
assert.isNull(interruptedState.message);
assert.equal(
interruptedState.message,
"Desktop update download action failed unexpectedly.",
);

const retry = yield* updates.download;
assert.isTrue(retry.accepted);
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,14 +575,16 @@ export const make = Effect.gen(function* () {

const checkedAt = yield* currentIsoTimestamp;
const releaseNotes = normalizeDesktopUpdateReleaseNotes(info.releaseNotes, info.version);
yield* setState(
yield* Ref.set(
updateStateRef,
reduceDesktopUpdateStateOnUpdateAvailable(state, info.version, checkedAt, releaseNotes),
);
yield* Ref.set(lastLoggedDownloadMilestoneRef, -1);
yield* logUpdaterInfo("update available", {
version: info.version,
releaseNoteGroups: releaseNotes.length,
});
yield* downloadAvailableUpdate;
}),
),
Effect.catchCause((cause) => {
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/sidebar/SidebarUpdatePill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ function SidebarUpdateControl() {
type="button"
aria-label={tooltip}
aria-disabled={disabled || isActionPending || undefined}
disabled={disabled || isActionPending}
disabled={isActionPending || (!isUpdateState && disabled)}
className={cn(
"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",
isUpdateState
Expand All @@ -254,6 +254,8 @@ function SidebarUpdateControl() {
>
{action === "install" ? (
<RotateCwIcon className="size-4" />
) : isDownloading ? (
<RefreshCwIcon className="size-4 animate-spin" />
) : isUpdateState ? (
<DownloadIcon className="size-4" />
) : (
Expand Down
6 changes: 6 additions & 0 deletions docs/user/updating.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ connection will disappear briefly and work that is still running may be interrup

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

## Desktop App Updates

The desktop app downloads its own updates in the background. Once an update is ready, select
**Restart to update** when you are ready to close T3 Code. T3 Code always asks for confirmation
before restarting, and a failed background download remains available to retry.

## Choose the Action You See

| Action | What to do |
Expand Down
Loading