From 9733703311c7222e0a93f0d74b48dfdcb2648f9a Mon Sep 17 00:00:00 2001
From: "akashgundapuneni@gmail.com"
Date: Tue, 10 Nov 2026 12:10:50 +0100
Subject: [PATCH 1/2] fix(web): let users choose machines to update
---
.../src/components/ProviderUpdatesAction.tsx | 36 ++++++---
.../components/sidebar/SidebarUpdatePill.tsx | 12 ++-
.../UpdateEverythingDialog.browser.test.tsx | 49 ++++++++++++
.../sidebar/UpdateEverythingDialog.tsx | 75 ++++++++++++++++---
.../sidebar/updateEverything.logic.test.ts | 11 +++
.../sidebar/updateEverything.logic.ts | 14 ++++
.../components/sidebar/useUpdateEverything.ts | 23 ++++--
docs/user/updating.md | 14 ++--
8 files changed, 197 insertions(+), 37 deletions(-)
create mode 100644 apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx
diff --git a/apps/web/src/components/ProviderUpdatesAction.tsx b/apps/web/src/components/ProviderUpdatesAction.tsx
index 4834487d55e6..4bb332abbfe3 100644
--- a/apps/web/src/components/ProviderUpdatesAction.tsx
+++ b/apps/web/src/components/ProviderUpdatesAction.tsx
@@ -1,4 +1,8 @@
-import { AuthProvidersManageScope, PROVIDER_DISPLAY_NAMES } from "@t3tools/contracts";
+import {
+ AuthProvidersManageScope,
+ type EnvironmentId,
+ PROVIDER_DISPLAY_NAMES,
+} from "@t3tools/contracts";
import {
isAtomCommandInterrupted,
settlePromise,
@@ -16,6 +20,7 @@ import {
getProviderUpdateRunToastView,
type ProviderUpdateRun,
} from "./ProviderUpdateLaunchNotification.logic";
+import { selectUpdateMachines } from "./sidebar/updateEverything.logic";
import { Button } from "./ui/button";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
@@ -90,19 +95,26 @@ export function useProviderUpdates() {
: [];
});
- const updateAll = async ({ quiet = false }: { readonly quiet?: boolean } = {}) => {
+ const updateAll = async ({
+ quiet = false,
+ environmentIds,
+ }: {
+ readonly quiet?: boolean;
+ readonly environmentIds?: ReadonlyArray;
+ } = {}) => {
const runs = await Promise.all(
- machines.flatMap(({ environmentId, label, candidates }) =>
- candidates.map(async (candidate): Promise => ({
- environmentId,
- machineLabel: label,
- driver: candidate.driver,
- instanceId: candidate.instanceId,
- result: await updateProvider({
+ selectUpdateMachines(machines, environmentIds).flatMap(
+ ({ environmentId, label, candidates }) =>
+ candidates.map(async (candidate): Promise => ({
environmentId,
- input: { provider: candidate.driver, instanceId: candidate.instanceId },
- }),
- })),
+ machineLabel: label,
+ driver: candidate.driver,
+ instanceId: candidate.instanceId,
+ result: await updateProvider({
+ environmentId,
+ input: { provider: candidate.driver, instanceId: candidate.instanceId },
+ }),
+ })),
),
);
const view = quiet ? null : getProviderUpdateRunToastView(runs);
diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx
index 3c18b2c7a927..4732e4517add 100644
--- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx
+++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx
@@ -42,7 +42,11 @@ import {
import { SidebarUpdateReleaseNotes } from "./SidebarUpdateReleaseNotes";
import { SidebarUpdateCapsule } from "./SidebarUpdateCapsule";
import { UpdateEverythingDialog } from "./UpdateEverythingDialog";
-import type { UpdateEverythingRun, UpdateRunLocalState } from "./updateEverything.logic";
+import type {
+ UpdateEverythingRun,
+ UpdateRunLocalState,
+ UpdateMachineSelection,
+} from "./updateEverything.logic";
import {
clearSavedUpdateEverythingResult,
saveUpdateEverythingResult,
@@ -361,12 +365,13 @@ function SidebarUpdateControl() {
void Promise.allSettled([appCheck, providersChecked]).then(() => setIsActionPending(false));
};
- const runEverything = async () => {
+ const runEverything = async (selection?: UpdateMachineSelection) => {
const bridge = window.desktopBridge;
setIsConfirmingEverything(false);
setIsActionPending(true);
const result = await settlePromise(() =>
everything.run({
+ ...(selection ? { selection } : {}),
...(bridge && action === "download"
? {
downloadLocal: (reportError) =>
@@ -420,7 +425,8 @@ function SidebarUpdateControl() {
rows={everything.rows}
restarts={everything.restarts}
checking={providerCheck === "dialog"}
- onConfirm={() => void runEverything()}
+ selectable
+ onConfirm={(selection) => void runEverything(selection)}
/>
);
diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx
new file mode 100644
index 000000000000..6e400557607e
--- /dev/null
+++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx
@@ -0,0 +1,49 @@
+import { EnvironmentId } from "@t3tools/contracts";
+import { expect, it, vi } from "vite-plus/test";
+import { page, userEvent } from "vite-plus/test/browser";
+
+import "~/index.css";
+import { render } from "../../../test/render";
+import { UpdateEverythingDialog } from "./UpdateEverythingDialog";
+
+it("lets a remote user update the laptop without updating the PC host", async () => {
+ const pc = EnvironmentId.make("pc");
+ const laptop = EnvironmentId.make("laptop");
+ const onConfirm = vi.fn();
+ await using _screen = await render(
+ {}}
+ selectable
+ version="0.2.0"
+ providerCount={2}
+ rows={[
+ {
+ environmentId: pc,
+ label: "PC host",
+ app: "this-app",
+ providers: ["Codex"],
+ skipped: null,
+ },
+ {
+ environmentId: laptop,
+ label: "Laptop",
+ app: "desktop-app",
+ providers: ["Claude"],
+ skipped: null,
+ },
+ ]}
+ restarts={[]}
+ onConfirm={onConfirm}
+ />,
+ );
+ await expect.element(page.getByRole("heading", { name: "Update 2 machines?" })).toBeVisible();
+ await userEvent.click(page.getByRole("checkbox", { name: "PC host" }));
+ await expect.element(page.getByRole("heading", { name: "Update 1 machine?" })).toBeVisible();
+ await expect.element(page.getByText(/1 provider update$/)).toBeVisible();
+ await userEvent.click(page.getByRole("checkbox", { name: "Laptop" }));
+ await expect.element(page.getByRole("button", { name: "Update", exact: true })).toBeDisabled();
+ await userEvent.click(page.getByRole("checkbox", { name: "Laptop" }));
+ await userEvent.click(page.getByRole("button", { name: "Update", exact: true }));
+ expect(onConfirm).toHaveBeenCalledWith({ environmentIds: [laptop], updateLocal: false });
+});
diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx
index 86c1fd2841c7..01f4ec45d141 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, useRef, useState } from "react";
import { environmentThreadShells } from "~/state/threads";
import {
@@ -14,11 +14,16 @@ import {
AlertDialogPopup,
AlertDialogTitle,
} from "../ui/alert-dialog";
+import { Checkbox } from "../ui/checkbox";
import { Button } from "../ui/button";
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";
import { MiddleTruncate } from "../ui/middle-truncate";
import { ScrollArea } from "../ui/scroll-area";
-import type { RestartingMachine, UpdateEverythingRow } from "./updateEverything.logic";
+import type {
+ RestartingMachine,
+ UpdateEverythingRow,
+ UpdateMachineSelection,
+} from "./updateEverything.logic";
const isRunning = (status: string) =>
status === "preparing" || status === "starting" || status === "running";
@@ -46,7 +51,8 @@ interface UpdateEverythingDialogProps {
readonly providerCount: number;
readonly rows: ReadonlyArray;
readonly restarts: ReadonlyArray;
- readonly onConfirm: () => void;
+ readonly onConfirm: (selection?: UpdateMachineSelection) => void;
+ readonly selectable?: boolean;
/** Overrides the "Update N machines?" title, for an update-track switch. */
readonly title?: string;
readonly confirmLabel?: string;
@@ -96,8 +102,9 @@ function UpdateEverythingDialogContent({
version,
providerCount,
rows,
- restarts,
+ restarts: allRestarts,
onConfirm,
+ selectable = false,
cancelRef,
title,
confirmLabel = "Update",
@@ -106,6 +113,20 @@ function UpdateEverythingDialogContent({
busy = false,
checking = false,
}: UpdateEverythingDialogContentProps) {
+ const [excluded, setExcluded] = useState>(() => new Set());
+ const rowKey = (row: UpdateEverythingRow) =>
+ row.app === "this-app" ? "this-app" : (row.environmentId ?? "this-app");
+ const updating = rows.filter((row) => row.app !== null || row.providers.length > 0);
+ const selected = updating.filter((row) => !selectable || !excluded.has(rowKey(row)));
+ const selectedIds = new Set(
+ selected.flatMap((row) => (row.environmentId === null ? [] : [row.environmentId])),
+ );
+ const restarts = allRestarts.filter(
+ (machine) => !selectable || selectedIds.has(machine.environmentId),
+ );
+ const selectedProviderCount = selectable
+ ? selected.reduce((count, row) => count + row.providers.length, 0)
+ : providerCount;
const stopped = useAtomValue(
runningThreadCountAtom(
restarts
@@ -125,14 +146,13 @@ function UpdateEverythingDialogContent({
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);
return (
<>
- {title ?? `Update ${updating.length} ${updating.length === 1 ? "machine" : "machines"}?`}
+ {title ?? `Update ${selected.length} ${selected.length === 1 ? "machine" : "machines"}?`}
{version !== null ? (
@@ -140,9 +160,9 @@ function UpdateEverythingDialogContent({
T3 Code {version}
>
) : null}
- {version !== null && providerCount > 0 ? " · " : null}
- {providerCount > 0
- ? `${providerCount} provider ${providerCount === 1 ? "update" : "updates"}`
+ {version !== null && selectedProviderCount > 0 ? " · " : null}
+ {selectedProviderCount > 0
+ ? `${selectedProviderCount} provider ${selectedProviderCount === 1 ? "update" : "updates"}`
: null}
{checking
? `${version !== null || providerCount > 0 ? " · " : ""}Checking for more…`
@@ -156,6 +176,11 @@ function UpdateEverythingDialogContent({
{note}
))}
+ {selectable ? (
+
+ Choose which machines to update. Only selected machines will be updated or restarted.
+
+ ) : null}
{/* Machine names truncate first. Provider lists wrap between names, never inside one. */}
@@ -169,9 +194,23 @@ function UpdateEverythingDialogContent({
key={row.environmentId ?? "this-app"}
className="col-span-full grid grid-cols-subgrid border-b border-border/50 py-1.5 last:border-b-0"
>
-
+
+
{row.app !== null ? APP_LABELS[row.app] : null}
{row.app === null && row.skipped !== null ? (
@@ -236,7 +275,19 @@ function UpdateEverythingDialogContent({
}>
Cancel
-