From 9df6983179c188bbe731575f4ae3f414d5c763ea Mon Sep 17 00:00:00 2001
From: PR Batch Tester
Date: Tue, 27 Oct 2026 14:25:36 +0000
Subject: [PATCH 1/6] fix(web): remote desktop updates warn about interrupted
tasks and need a confirmation
Updating a desktop app from another client closed and relaunched it after a
dialog that never mentioned running tasks, and went ahead without asking when
no confirmation dialog was mounted. Both the per-server and the batch actions
now say running tasks and tool calls will be interrupted (and that thread
continuation cannot resume tool calls in progress), and only dispatch after
an explicit confirmation.
Co-Authored-By: Claude Opus 5.5
---
.../ServerUpdateAction.browser.test.tsx | 89 +++++++++++++++++++
.../web/src/components/ServerUpdateAction.tsx | 16 ++--
2 files changed, 97 insertions(+), 8 deletions(-)
diff --git a/apps/web/src/components/ServerUpdateAction.browser.test.tsx b/apps/web/src/components/ServerUpdateAction.browser.test.tsx
index ed3a723652a56..438264923d494 100644
--- a/apps/web/src/components/ServerUpdateAction.browser.test.tsx
+++ b/apps/web/src/components/ServerUpdateAction.browser.test.tsx
@@ -5,7 +5,9 @@ import * as Schema from "effect/Schema";
import { AsyncResult, Atom } from "effect/reactivity";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vite-plus/test";
+import { page, userEvent } from "vite-plus/test/browser";
+import "~/index.css";
import { act, render, type Screen } from "../../test/render";
type SessionResult = AsyncResult.AsyncResult;
@@ -58,6 +60,7 @@ vi.mock(import("./ui/toast"), async (original) => {
import { resetConfirmDialogForTests } from "~/confirmDialog";
import { appAtomRegistry } from "~/rpc/atomRegistry";
+import { ConfirmDialogHost } from "./ConfirmDialogHost";
import {
ServerUpdateAction,
ServerUpdatesAction,
@@ -108,6 +111,23 @@ function renderUpdate(element: ReactNode) {
);
}
+function renderWithConfirmHost(element: ReactNode) {
+ return renderUpdate(
+ <>
+ {element}
+
+ >,
+ );
+}
+
+async function answerDesktopUpdateDialog(choice: "Cancel" | "Confirm") {
+ await expect
+ .element(page.getByText(/Running tasks and tool calls will be interrupted/))
+ .toBeVisible();
+ await userEvent.click(page.getByRole("button", { name: choice }));
+ await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
+}
+
function button(screen: Screen, name?: string) {
const buttons = [...screen.container.querySelectorAll("button")];
return name === undefined
@@ -171,6 +191,38 @@ describe("ServerUpdateAction", () => {
},
);
+ const desktopProps = { selfUpdate: "desktop-managed", desktopAppUpdate: true } as const;
+
+ it("does not update a desktop app when no confirmation dialog can be shown", async () => {
+ using _state = setup();
+ testState.updateServer.mockResolvedValue(success("desktop-app"));
+ await using screen = await renderAction(desktopProps);
+ await click(screen, "Update");
+ expect(testState.updateServer).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ { choice: "Cancel", updates: 0 },
+ { choice: "Confirm", updates: 1 },
+ ] as const)(
+ "updates a desktop app only when confirmed ($choice)",
+ async ({ choice, updates }) => {
+ using _state = setup();
+ testState.updateServer.mockResolvedValue(success("desktop-app"));
+ await using screen = await renderWithConfirmHost(
+ ,
+ );
+ await click(screen, "Update");
+ await answerDesktopUpdateDialog(choice);
+ await vi.waitFor(() => expect(testState.updateServer).toHaveBeenCalledTimes(updates));
+ },
+ );
+
it("disables the action once maintenance access is removed", async () => {
using _state = setup();
await using screen = await renderAction();
@@ -237,4 +289,41 @@ describe("ServerUpdatesAction", () => {
expect.objectContaining({ type: "success", title: "Office updated" }),
);
});
+
+ const withDesktop: ReadonlyArray = [
+ targets[0]!,
+ {
+ environmentId: "batch-desktop" as EnvironmentId,
+ serverLabel: "Studio",
+ selfUpdate: "desktop-managed",
+ desktopAppUpdate: true,
+ targetVersion: "0.0.31",
+ },
+ ];
+
+ it("does not update any machine when a desktop app needs a confirmation that cannot be shown", async () => {
+ using _state = setup();
+ testState.updateServer.mockResolvedValue(success());
+ await using screen = await mount(withDesktop);
+ await click(screen, "Update all");
+ expect(testState.updateServer).not.toHaveBeenCalled();
+ expect(button(screen, "Update all")!.disabled).toBe(false);
+ });
+
+ it.each([
+ { choice: "Cancel", updates: 0 },
+ { choice: "Confirm", updates: 2 },
+ ] as const)(
+ "updates desktop apps in a batch only when confirmed ($choice)",
+ async ({ choice, updates }) => {
+ using _state = setup();
+ testState.updateServer.mockResolvedValue(success());
+ await using screen = await renderWithConfirmHost(
+ ,
+ );
+ await click(screen, "Update all");
+ await answerDesktopUpdateDialog(choice);
+ await vi.waitFor(() => expect(testState.updateServer).toHaveBeenCalledTimes(updates));
+ },
+ );
});
diff --git a/apps/web/src/components/ServerUpdateAction.tsx b/apps/web/src/components/ServerUpdateAction.tsx
index 141f139aefd7b..becebdf09ffe7 100644
--- a/apps/web/src/components/ServerUpdateAction.tsx
+++ b/apps/web/src/components/ServerUpdateAction.tsx
@@ -39,6 +39,8 @@ const UPDATE_STAGE_LABELS: Record = {
resuming: "Restarting…",
};
const pendingUpdateEnvironmentIds = new Set();
+const DESKTOP_UPDATE_INTERRUPTION_WARNING =
+ "Running tasks and tool calls will be interrupted. If thread continuation is on, threads resume afterwards, but tool calls in progress do not.";
export function serverUpdateStageLabel(stage: ServerUpdateStage): string {
return UPDATE_STAGE_LABELS[stage];
@@ -175,10 +177,9 @@ export function ServerUpdatesAction({
);
const desktopTargets = available.filter((target) => target.selfUpdate === "desktop-managed");
if (desktopTargets.length > 0) {
- const confirmed =
- (await requestConfirmDialog(
- `Update the T3 Code desktop apps on ${desktopTargets.map((target) => target.serverLabel).join(", ")}? They will close and relaunch on those machines.`,
- )) ?? true;
+ const confirmed = await requestConfirmDialog(
+ `Update the T3 Code desktop apps on ${desktopTargets.map((target) => target.serverLabel).join(", ")}?\n\nThey will close and relaunch on those machines. ${DESKTOP_UPDATE_INTERRUPTION_WARNING}`,
+ );
if (!confirmed) return;
}
await Promise.all(available.map((target) => update(target)));
@@ -280,10 +281,9 @@ export function ServerUpdateAction({
return;
}
if (isDesktopAppUpdate) {
- const confirmed =
- (await requestConfirmDialog(
- `Update the T3 Code desktop app that runs the ${serverLabel}? It will close and relaunch on that machine.`,
- )) ?? true;
+ const confirmed = await requestConfirmDialog(
+ `Update the T3 Code desktop app that runs the ${serverLabel}?\n\nIt will close and relaunch on that machine. ${DESKTOP_UPDATE_INTERRUPTION_WARNING}`,
+ );
if (!confirmed) {
return;
}
From 93167f68f155de4841a44f34e7a665c75178e9da Mon Sep 17 00:00:00 2001
From: PR Batch Tester
Date: Wed, 28 Oct 2026 10:44:44 +0000
Subject: [PATCH 2/6] fix(web): Update everything always warns that restarts
interrupt running work
The Update everything dialog only mentioned interrupted threads when the
client had loaded a running count for the restarting machines, so a remote
desktop with no loaded thread snapshot relaunched with no warning. It now says
running tasks and tool calls will be interrupted whenever any machine restarts,
and keeps the thread counts as extra detail. Provider-only updates stay quiet.
Co-Authored-By: Claude Opus 5.5
---
.../SidebarUpdatePill.browser.test.tsx | 70 +++++++++++++++++--
.../sidebar/UpdateEverythingDialog.tsx | 17 +++--
2 files changed, 77 insertions(+), 10 deletions(-)
diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
index 18ebd70c6162c..df4d83699b621 100644
--- a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
+++ b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
@@ -1,16 +1,20 @@
-import type { DesktopUpdateState } from "@t3tools/contracts";
+import type { DesktopUpdateState, EnvironmentId } from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
import { page, userEvent } from "vite-plus/test/browser";
import "~/index.css";
import { act, render } from "../../../test/render";
import { SidebarUpdatePill } from "./SidebarUpdatePill";
+import type { RestartingMachine, UpdateEverythingRow } from "./updateEverything.logic";
const mocks = vi.hoisted(() => {
const listeners = new Set<() => void>();
return {
update: null as DesktopUpdateState | null,
hasUpdates: false,
+ rows: [] as ReadonlyArray,
+ restarts: [] as ReadonlyArray,
+ run: vi.fn(async () => {}),
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
@@ -39,8 +43,9 @@ vi.mock("./useUpdateEverything", () => ({
summary: "Update everything",
version: null,
providerCount: 0,
- rows: [],
- restarts: [],
+ rows: mocks.rows,
+ restarts: mocks.restarts,
+ run: mocks.run,
}),
}));
@@ -69,9 +74,19 @@ const RELEASE_NOTES: DesktopUpdateState["releaseNotes"] = [
{ version: "0.0.50-nightly.2", items: ["Faster sidebar."], totalItems: 1 },
];
-function setup(update: DesktopUpdateState | null, { hasUpdates = false } = {}) {
+function setup(
+ update: DesktopUpdateState | null,
+ {
+ hasUpdates = false,
+ rows = [] as ReadonlyArray,
+ restarts = [] as ReadonlyArray,
+ } = {},
+) {
mocks.update = update;
mocks.hasUpdates = hasUpdates;
+ mocks.rows = rows;
+ mocks.restarts = restarts;
+ mocks.run.mockClear();
}
function publish(update: DesktopUpdateState | null) {
@@ -129,4 +144,51 @@ describe("SidebarUpdatePill update everything", () => {
await userEvent.keyboard("{Escape}");
await vi.waitFor(() => expect(document.activeElement).toBe(button.element()));
});
+
+ const studio = "studio" as EnvironmentId;
+ const interruption = /Running tasks and tool calls will be interrupted/;
+
+ it.each([
+ { choice: "Cancel", runs: 0 },
+ { choice: "Update", runs: 1 },
+ ] as const)(
+ "warns that a restarting machine with no loaded threads interrupts running work ($choice)",
+ async ({ choice, runs }) => {
+ setup(null, {
+ hasUpdates: true,
+ rows: [
+ {
+ environmentId: studio,
+ label: "Studio",
+ app: "desktop-app",
+ skipped: null,
+ providers: [],
+ },
+ ],
+ restarts: [{ environmentId: studio, continuesThreads: true }],
+ });
+ await using screen = await render();
+ await userEvent.click(screen.getByRole("button", { name: "Update everything" }));
+ const dialog = page.getByRole("alertdialog");
+ await expect.element(dialog.getByText(interruption)).toBeVisible();
+
+ await userEvent.click(dialog.getByRole("button", { name: choice, exact: true }));
+ await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
+ await vi.waitFor(() => expect(mocks.run).toHaveBeenCalledTimes(runs));
+ },
+ );
+
+ it("does not warn about interruptions when only providers update", async () => {
+ setup(null, {
+ hasUpdates: true,
+ rows: [
+ { environmentId: studio, label: "Studio", app: null, skipped: null, providers: ["Codex"] },
+ ],
+ });
+ await using screen = await render();
+ await userEvent.click(screen.getByRole("button", { name: "Update everything" }));
+ const dialog = page.getByRole("alertdialog");
+ await expect.element(dialog.getByText("Codex")).toBeVisible();
+ expect(dialog.getByText(interruption).query()).toBeNull();
+ });
});
diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
index 9bf101b6fa4e7..1a848bbba8873 100644
--- a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
+++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
@@ -214,17 +214,22 @@ function UpdateEverythingDialogContent({
+ {/* Thread counts come from client snapshots that can still be loading, so the warning
+ cannot depend on them. */}
{disabledReason !== null ? (
{disabledReason}
- ) : running > 0 ? (
+ ) : updating.some((row) => row.app !== null) ? (
- {stopped === 0
- ? `${threads} will be interrupted and can resume after the restart.`
- : continuing === 0
- ? `${threads} will be interrupted.`
- : `${threads} will be interrupted. ${continuing} can resume after the restart.`}
+ Running tasks and tool calls will be interrupted.
+ {running === 0
+ ? null
+ : stopped === 0
+ ? ` ${threads} can resume after the restart.`
+ : continuing === 0
+ ? ` ${threads} will not resume automatically.`
+ : ` ${continuing} of ${threads} can resume after the restart.`}
) : null}
}>
From e599e967e637e003c75b49256981cf40de704ea9 Mon Sep 17 00:00:00 2001
From: PR Batch Tester
Date: Wed, 28 Oct 2026 11:08:21 +0000
Subject: [PATCH 3/6] fix(web): make update interruption warnings accessible
---
.../SidebarUpdatePill.browser.test.tsx | 4 +++
.../sidebar/UpdateEverythingDialog.tsx | 26 +++++++++++++++----
2 files changed, 25 insertions(+), 5 deletions(-)
diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
index df4d83699b621..c0940a72f7e44 100644
--- a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
+++ b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
@@ -171,6 +171,10 @@ describe("SidebarUpdatePill update everything", () => {
await userEvent.click(screen.getByRole("button", { name: "Update everything" }));
const dialog = page.getByRole("alertdialog");
await expect.element(dialog.getByText(interruption)).toBeVisible();
+ await expect.element(dialog).toHaveAccessibleDescription(interruption);
+ await expect
+ .element(dialog.getByText(/Tool calls in progress will not resume/))
+ .toBeVisible();
await userEvent.click(dialog.getByRole("button", { name: choice, exact: true }));
await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
index 1a848bbba8873..8baf6e8449a8c 100644
--- a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
+++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
@@ -2,7 +2,7 @@ import { useAtomValue } from "@effect/atom-react";
import { EnvironmentId } from "@t3tools/contracts";
import { Atom } from "effect/reactivity";
import { ChevronRightIcon } from "lucide-react";
-import { Fragment, type RefObject, useRef } from "react";
+import { Fragment, type RefObject, useId, useRef } from "react";
import { environmentThreadShells } from "~/state/threads";
import {
@@ -62,6 +62,7 @@ type UpdateEverythingDialogContentProps = Omit<
"open" | "onOpenChange"
> & {
readonly cancelRef: RefObject;
+ readonly descriptionId: string;
};
export function UpdateEverythingDialog({
@@ -70,15 +71,25 @@ export function UpdateEverythingDialog({
...content
}: UpdateEverythingDialogProps) {
const cancelRef = useRef(null);
+ const descriptionId = useId();
return (
{/* Refocusing the trigger after a pointer close shows its focus ring and opens its tooltip. */}
row.app !== null)
+ ? `${descriptionId} ${descriptionId}-interruption`
+ : descriptionId
+ }
initialFocus={cancelRef}
finalFocus={(closeType) => closeType === "keyboard"}
>
-
+
);
@@ -102,6 +113,7 @@ function UpdateEverythingDialogContent({
notes = [],
disabledReason = null,
busy = false,
+ descriptionId,
}: UpdateEverythingDialogContentProps) {
const stopped = useAtomValue(
runningThreadCountAtom(
@@ -131,7 +143,7 @@ function UpdateEverythingDialogContent({
{title ?? `Update ${updating.length} ${updating.length === 1 ? "machine" : "machines"}?`}
-
+
{version !== null ? (
<>
T3 Code {version}
@@ -221,8 +233,12 @@ function UpdateEverythingDialogContent({
{disabledReason}
) : updating.some((row) => row.app !== null) ? (
-
- Running tasks and tool calls will be interrupted.
+
+ Running tasks and tool calls will be interrupted. Tool calls in progress will not
+ resume.
{running === 0
? null
: stopped === 0
From a712e074461690181c46f85afdc576db93b34ab6 Mon Sep 17 00:00:00 2001
From: PR Batch Tester
Date: Thu, 29 Oct 2026 02:40:59 +0000
Subject: [PATCH 4/6] fix(web): keep update warnings to one short line
---
.../ServerUpdateAction.browser.test.tsx | 4 +--
.../web/src/components/ServerUpdateAction.tsx | 7 ++---
.../SidebarUpdatePill.browser.test.tsx | 5 +---
.../sidebar/UpdateEverythingDialog.tsx | 30 ++-----------------
4 files changed, 7 insertions(+), 39 deletions(-)
diff --git a/apps/web/src/components/ServerUpdateAction.browser.test.tsx b/apps/web/src/components/ServerUpdateAction.browser.test.tsx
index 438264923d494..9cb89774313b8 100644
--- a/apps/web/src/components/ServerUpdateAction.browser.test.tsx
+++ b/apps/web/src/components/ServerUpdateAction.browser.test.tsx
@@ -121,9 +121,7 @@ function renderWithConfirmHost(element: ReactNode) {
}
async function answerDesktopUpdateDialog(choice: "Cancel" | "Confirm") {
- await expect
- .element(page.getByText(/Running tasks and tool calls will be interrupted/))
- .toBeVisible();
+ await expect.element(page.getByRole("alertdialog")).toHaveAccessibleDescription(/interrupt/i);
await userEvent.click(page.getByRole("button", { name: choice }));
await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
}
diff --git a/apps/web/src/components/ServerUpdateAction.tsx b/apps/web/src/components/ServerUpdateAction.tsx
index becebdf09ffe7..0e7f1fe4bdb62 100644
--- a/apps/web/src/components/ServerUpdateAction.tsx
+++ b/apps/web/src/components/ServerUpdateAction.tsx
@@ -39,8 +39,7 @@ const UPDATE_STAGE_LABELS: Record = {
resuming: "Restarting…",
};
const pendingUpdateEnvironmentIds = new Set();
-const DESKTOP_UPDATE_INTERRUPTION_WARNING =
- "Running tasks and tool calls will be interrupted. If thread continuation is on, threads resume afterwards, but tool calls in progress do not.";
+const DESKTOP_UPDATE_INTERRUPTION_WARNING = "Updates interrupt tasks and tool calls.";
export function serverUpdateStageLabel(stage: ServerUpdateStage): string {
return UPDATE_STAGE_LABELS[stage];
@@ -178,7 +177,7 @@ export function ServerUpdatesAction({
const desktopTargets = available.filter((target) => target.selfUpdate === "desktop-managed");
if (desktopTargets.length > 0) {
const confirmed = await requestConfirmDialog(
- `Update the T3 Code desktop apps on ${desktopTargets.map((target) => target.serverLabel).join(", ")}?\n\nThey will close and relaunch on those machines. ${DESKTOP_UPDATE_INTERRUPTION_WARNING}`,
+ `Update the T3 Code desktop apps on ${desktopTargets.map((target) => target.serverLabel).join(", ")}?\n\n${DESKTOP_UPDATE_INTERRUPTION_WARNING}`,
);
if (!confirmed) return;
}
@@ -282,7 +281,7 @@ export function ServerUpdateAction({
}
if (isDesktopAppUpdate) {
const confirmed = await requestConfirmDialog(
- `Update the T3 Code desktop app that runs the ${serverLabel}?\n\nIt will close and relaunch on that machine. ${DESKTOP_UPDATE_INTERRUPTION_WARNING}`,
+ `Update the T3 Code desktop app that runs the ${serverLabel}?\n\n${DESKTOP_UPDATE_INTERRUPTION_WARNING}`,
);
if (!confirmed) {
return;
diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
index c0940a72f7e44..cf5a8f17719d3 100644
--- a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
+++ b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
@@ -146,7 +146,7 @@ describe("SidebarUpdatePill update everything", () => {
});
const studio = "studio" as EnvironmentId;
- const interruption = /Running tasks and tool calls will be interrupted/;
+ const interruption = /interrupt/i;
it.each([
{ choice: "Cancel", runs: 0 },
@@ -172,9 +172,6 @@ describe("SidebarUpdatePill update everything", () => {
const dialog = page.getByRole("alertdialog");
await expect.element(dialog.getByText(interruption)).toBeVisible();
await expect.element(dialog).toHaveAccessibleDescription(interruption);
- await expect
- .element(dialog.getByText(/Tool calls in progress will not resume/))
- .toBeVisible();
await userEvent.click(dialog.getByRole("button", { name: choice, exact: true }));
await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
index 8baf6e8449a8c..56edd9010dceb 100644
--- a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
+++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
@@ -51,7 +51,7 @@ interface UpdateEverythingDialogProps {
readonly title?: string;
readonly confirmLabel?: string;
readonly notes?: ReadonlyArray;
- /** Shown in place of running-thread counts; disables the confirm button. */
+ /** Shown in place of the interruption warning; disables the confirm button. */
readonly disabledReason?: string | null;
/** The confirmed work is starting; it cannot be cancelled from here. */
readonly busy?: boolean;
@@ -115,24 +115,6 @@ function UpdateEverythingDialogContent({
busy = false,
descriptionId,
}: UpdateEverythingDialogContentProps) {
- const stopped = useAtomValue(
- runningThreadCountAtom(
- restarts
- .filter((machine) => !machine.continuesThreads)
- .map((machine) => machine.environmentId)
- .join("\n"),
- ),
- );
- const continuing = useAtomValue(
- runningThreadCountAtom(
- restarts
- .filter((machine) => machine.continuesThreads)
- .map((machine) => machine.environmentId)
- .join("\n"),
- ),
- );
- const running = stopped + continuing;
- const threads = `${running} running ${running === 1 ? "thread" : "threads"}`;
const restarting = new Set(restarts.map((machine) => machine.environmentId));
const updating = rows.filter((row) => row.app !== null || row.providers.length > 0);
const skipped = rows.filter((row) => row.skipped !== null);
@@ -237,15 +219,7 @@ function UpdateEverythingDialogContent({
id={`${descriptionId}-interruption`}
className="self-center text-muted-foreground text-sm max-sm:order-1 max-sm:text-center sm:mr-auto"
>
- Running tasks and tool calls will be interrupted. Tool calls in progress will not
- resume.
- {running === 0
- ? null
- : stopped === 0
- ? ` ${threads} can resume after the restart.`
- : continuing === 0
- ? ` ${threads} will not resume automatically.`
- : ` ${continuing} of ${threads} can resume after the restart.`}
+ Updates interrupt tasks and tool calls.
) : null}
}>
From 6e8988b665f042b8bbb2d248f3e2c6084b6d85db Mon Sep 17 00:00:00 2001
From: PR Batch Tester
Date: Thu, 29 Oct 2026 11:03:59 +0000
Subject: [PATCH 5/6] test(web): cover blocked update dialog after rebase
---
.../SidebarUpdatePill.browser.test.tsx | 36 +++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
index cf5a8f17719d3..7ff42ca600488 100644
--- a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
+++ b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
@@ -5,6 +5,7 @@ import { page, userEvent } from "vite-plus/test/browser";
import "~/index.css";
import { act, render } from "../../../test/render";
import { SidebarUpdatePill } from "./SidebarUpdatePill";
+import { UpdateEverythingDialog } from "./UpdateEverythingDialog";
import type { RestartingMachine, UpdateEverythingRow } from "./updateEverything.logic";
const mocks = vi.hoisted(() => {
@@ -192,4 +193,39 @@ describe("SidebarUpdatePill update everything", () => {
await expect.element(dialog.getByText("Codex")).toBeVisible();
expect(dialog.getByText(interruption).query()).toBeNull();
});
+
+ it("keeps a blocked app update's description valid and its confirmation disabled", async () => {
+ setup(null);
+ await using _screen = await render(
+ {}}
+ version={null}
+ providerCount={0}
+ rows={[
+ {
+ environmentId: studio,
+ label: "Studio",
+ app: "desktop-app",
+ skipped: null,
+ providers: [],
+ },
+ ]}
+ restarts={[]}
+ disabledReason="An environment is not connected."
+ onConfirm={() => {}}
+ />,
+ );
+ const dialog = page.getByRole("alertdialog");
+ await expect.element(dialog.getByText("An environment is not connected.")).toBeVisible();
+ await expect
+ .element(dialog.getByRole("button", { name: "Update", exact: true }))
+ .toBeDisabled();
+ expect(dialog.getByText(interruption).query()).toBeNull();
+ const descriptionIds = dialog.element().getAttribute("aria-describedby")?.split(/\s+/);
+ expect(descriptionIds?.length).toBeGreaterThan(0);
+ for (const id of descriptionIds ?? []) {
+ expect(document.getElementById(id)).not.toBeNull();
+ }
+ });
});
From 0b7ab6d62f1fcd8cb414c9e89b325f23f875751d Mon Sep 17 00:00:00 2001
From: shivam <91240327+shivamhwp@users.noreply.github.com>
Date: Sat, 7 Nov 2026 14:23:33 +0000
Subject: [PATCH 6/6] test(web): keep only the update confirmation cases that
fail on main
Co-Authored-By: Claude Opus 5.5
---
.../ServerUpdateAction.browser.test.tsx | 64 +-------------
.../SidebarUpdatePill.browser.test.tsx | 87 ++-----------------
2 files changed, 12 insertions(+), 139 deletions(-)
diff --git a/apps/web/src/components/ServerUpdateAction.browser.test.tsx b/apps/web/src/components/ServerUpdateAction.browser.test.tsx
index 9cb89774313b8..ace7f45976eff 100644
--- a/apps/web/src/components/ServerUpdateAction.browser.test.tsx
+++ b/apps/web/src/components/ServerUpdateAction.browser.test.tsx
@@ -5,9 +5,7 @@ import * as Schema from "effect/Schema";
import { AsyncResult, Atom } from "effect/reactivity";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vite-plus/test";
-import { page, userEvent } from "vite-plus/test/browser";
-import "~/index.css";
import { act, render, type Screen } from "../../test/render";
type SessionResult = AsyncResult.AsyncResult;
@@ -60,7 +58,6 @@ vi.mock(import("./ui/toast"), async (original) => {
import { resetConfirmDialogForTests } from "~/confirmDialog";
import { appAtomRegistry } from "~/rpc/atomRegistry";
-import { ConfirmDialogHost } from "./ConfirmDialogHost";
import {
ServerUpdateAction,
ServerUpdatesAction,
@@ -111,21 +108,6 @@ function renderUpdate(element: ReactNode) {
);
}
-function renderWithConfirmHost(element: ReactNode) {
- return renderUpdate(
- <>
- {element}
-
- >,
- );
-}
-
-async function answerDesktopUpdateDialog(choice: "Cancel" | "Confirm") {
- await expect.element(page.getByRole("alertdialog")).toHaveAccessibleDescription(/interrupt/i);
- await userEvent.click(page.getByRole("button", { name: choice }));
- await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
-}
-
function button(screen: Screen, name?: string) {
const buttons = [...screen.container.querySelectorAll("button")];
return name === undefined
@@ -189,38 +171,17 @@ describe("ServerUpdateAction", () => {
},
);
- const desktopProps = { selfUpdate: "desktop-managed", desktopAppUpdate: true } as const;
-
it("does not update a desktop app when no confirmation dialog can be shown", async () => {
using _state = setup();
testState.updateServer.mockResolvedValue(success("desktop-app"));
- await using screen = await renderAction(desktopProps);
+ await using screen = await renderAction({
+ selfUpdate: "desktop-managed",
+ desktopAppUpdate: true,
+ });
await click(screen, "Update");
expect(testState.updateServer).not.toHaveBeenCalled();
});
- it.each([
- { choice: "Cancel", updates: 0 },
- { choice: "Confirm", updates: 1 },
- ] as const)(
- "updates a desktop app only when confirmed ($choice)",
- async ({ choice, updates }) => {
- using _state = setup();
- testState.updateServer.mockResolvedValue(success("desktop-app"));
- await using screen = await renderWithConfirmHost(
- ,
- );
- await click(screen, "Update");
- await answerDesktopUpdateDialog(choice);
- await vi.waitFor(() => expect(testState.updateServer).toHaveBeenCalledTimes(updates));
- },
- );
-
it("disables the action once maintenance access is removed", async () => {
using _state = setup();
await using screen = await renderAction();
@@ -307,21 +268,4 @@ describe("ServerUpdatesAction", () => {
expect(testState.updateServer).not.toHaveBeenCalled();
expect(button(screen, "Update all")!.disabled).toBe(false);
});
-
- it.each([
- { choice: "Cancel", updates: 0 },
- { choice: "Confirm", updates: 2 },
- ] as const)(
- "updates desktop apps in a batch only when confirmed ($choice)",
- async ({ choice, updates }) => {
- using _state = setup();
- testState.updateServer.mockResolvedValue(success());
- await using screen = await renderWithConfirmHost(
- ,
- );
- await click(screen, "Update all");
- await answerDesktopUpdateDialog(choice);
- await vi.waitFor(() => expect(testState.updateServer).toHaveBeenCalledTimes(updates));
- },
- );
});
diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
index 832d74efc61a9..93a6f38c4de3c 100644
--- a/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
+++ b/apps/web/src/components/sidebar/SidebarUpdatePill.browser.test.tsx
@@ -6,16 +6,12 @@ import "~/index.css";
import { act, render } from "../../../test/render";
import { SidebarUpdatePill } from "./SidebarUpdatePill";
import { UpdateEverythingDialog } from "./UpdateEverythingDialog";
-import type { RestartingMachine, UpdateEverythingRow } from "./updateEverything.logic";
const mocks = vi.hoisted(() => {
const listeners = new Set<() => void>();
return {
update: null as DesktopUpdateState | null,
hasUpdates: false,
- rows: [] as ReadonlyArray,
- restarts: [] as ReadonlyArray,
- run: vi.fn(async () => {}),
subscribe: (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
@@ -44,9 +40,8 @@ vi.mock("./useUpdateEverything", () => ({
summary: "Update everything",
version: null,
providerCount: 0,
- rows: mocks.rows,
- restarts: mocks.restarts,
- run: mocks.run,
+ rows: [],
+ restarts: [],
}),
}));
@@ -75,19 +70,9 @@ const RELEASE_NOTES: DesktopUpdateState["releaseNotes"] = [
{ version: "0.0.50-nightly.2", items: ["Faster sidebar."], totalItems: 1 },
];
-function setup(
- update: DesktopUpdateState | null,
- {
- hasUpdates = false,
- rows = [] as ReadonlyArray,
- restarts = [] as ReadonlyArray,
- } = {},
-) {
+function setup(update: DesktopUpdateState | null, { hasUpdates = false } = {}) {
mocks.update = update;
mocks.hasUpdates = hasUpdates;
- mocks.rows = rows;
- mocks.restarts = restarts;
- mocks.run.mockClear();
}
function publish(update: DesktopUpdateState | null) {
@@ -157,56 +142,9 @@ describe("SidebarUpdatePill update everything", () => {
await vi.waitFor(() => expect(document.activeElement).toBe(button.element()));
});
- const studio = "studio" as EnvironmentId;
- const interruption = /interrupt/i;
-
- it.each([
- { choice: "Cancel", runs: 0 },
- { choice: "Update", runs: 1 },
- ] as const)(
- "warns that a restarting machine with no loaded threads interrupts running work ($choice)",
- async ({ choice, runs }) => {
- setup(null, {
- hasUpdates: true,
- rows: [
- {
- environmentId: studio,
- label: "Studio",
- app: "desktop-app",
- skipped: null,
- providers: [],
- },
- ],
- restarts: [{ environmentId: studio, continuesThreads: true }],
- });
- await using screen = await render();
- await userEvent.click(screen.getByRole("button", { name: "Update everything" }));
- const dialog = page.getByRole("alertdialog");
- await expect.element(dialog.getByText(interruption)).toBeVisible();
- await expect.element(dialog).toHaveAccessibleDescription(interruption);
-
- await userEvent.click(dialog.getByRole("button", { name: choice, exact: true }));
- await vi.waitFor(() => expect(document.querySelector('[role="alertdialog"]')).toBeNull());
- await vi.waitFor(() => expect(mocks.run).toHaveBeenCalledTimes(runs));
- },
- );
-
- it("does not warn about interruptions when only providers update", async () => {
- setup(null, {
- hasUpdates: true,
- rows: [
- { environmentId: studio, label: "Studio", app: null, skipped: null, providers: ["Codex"] },
- ],
- });
- await using screen = await render();
- await userEvent.click(screen.getByRole("button", { name: "Update everything" }));
- const dialog = page.getByRole("alertdialog");
- await expect.element(dialog.getByText("Codex")).toBeVisible();
- expect(dialog.getByText(interruption).query()).toBeNull();
- });
-
- it("keeps a blocked app update's description valid and its confirmation disabled", async () => {
+ it("warns that an app restart interrupts work before any threads have loaded", async () => {
setup(null);
+ const studio = "studio" as EnvironmentId;
await using _screen = await render(
{
providers: [],
},
]}
- restarts={[]}
- disabledReason="An environment is not connected."
+ restarts={[{ environmentId: studio, continuesThreads: true }]}
onConfirm={() => {}}
/>,
);
const dialog = page.getByRole("alertdialog");
- await expect.element(dialog.getByText("An environment is not connected.")).toBeVisible();
- await expect
- .element(dialog.getByRole("button", { name: "Update", exact: true }))
- .toBeDisabled();
- expect(dialog.getByText(interruption).query()).toBeNull();
- const descriptionIds = dialog.element().getAttribute("aria-describedby")?.split(/\s+/);
- expect(descriptionIds?.length).toBeGreaterThan(0);
- for (const id of descriptionIds ?? []) {
- expect(document.getElementById(id)).not.toBeNull();
- }
+ await expect.element(dialog.getByText(/interrupt/i)).toBeVisible();
+ await expect.element(dialog).toHaveAccessibleDescription(/interrupt/i);
});
});