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 - diff --git a/apps/web/src/components/sidebar/updateEverything.logic.test.ts b/apps/web/src/components/sidebar/updateEverything.logic.test.ts index 58328dbc1eee..2a90d45113fb 100644 --- a/apps/web/src/components/sidebar/updateEverything.logic.test.ts +++ b/apps/web/src/components/sidebar/updateEverything.logic.test.ts @@ -9,6 +9,7 @@ import { describeUpdateCapsule, describeUpdateEverything, runUpdateEverything, + selectUpdateMachines, type UpdateEverythingMachine, } from "./updateEverything.logic"; @@ -402,3 +403,13 @@ describe("describeUpdateCapsule", () => { ); }); }); + +describe("selectUpdateMachines", () => { + it("updates only selected machines and preserves the update-all default", () => { + const pc = { environmentId: EnvironmentId.make("pc") }; + const laptop = { environmentId: EnvironmentId.make("laptop") }; + expect(selectUpdateMachines([pc, laptop], [laptop.environmentId])).toEqual([laptop]); + expect(selectUpdateMachines([pc, laptop], [])).toEqual([]); + expect(selectUpdateMachines([pc, laptop])).toEqual([pc, laptop]); + }); +}); diff --git a/apps/web/src/components/sidebar/updateEverything.logic.ts b/apps/web/src/components/sidebar/updateEverything.logic.ts index cbbd3baa7c77..8c92bf1ccb43 100644 --- a/apps/web/src/components/sidebar/updateEverything.logic.ts +++ b/apps/web/src/components/sidebar/updateEverything.logic.ts @@ -678,3 +678,17 @@ export function describeUpdateEverything(plan: UpdateEverythingPlan): { rows: Array.from(rows.values()), }; } + +export interface UpdateMachineSelection { + readonly environmentIds: ReadonlyArray; + readonly updateLocal: boolean; +} + +export function selectUpdateMachines( + machines: ReadonlyArray, + environmentIds?: ReadonlyArray, +): ReadonlyArray { + return environmentIds === undefined + ? machines + : machines.filter((machine) => environmentIds.includes(machine.environmentId)); +} diff --git a/apps/web/src/components/sidebar/useUpdateEverything.ts b/apps/web/src/components/sidebar/useUpdateEverything.ts index 05127d988576..b81767e7d1d9 100644 --- a/apps/web/src/components/sidebar/useUpdateEverything.ts +++ b/apps/web/src/components/sidebar/useUpdateEverything.ts @@ -20,6 +20,8 @@ import { collectUpdateEverythingResult, describeUpdateEverything, runUpdateEverything, + selectUpdateMachines, + type UpdateMachineSelection, type UpdateRunMachine, } from "./updateEverything.logic"; import { @@ -98,17 +100,25 @@ export function useUpdateEverything( run: async ({ downloadLocal, installLocal, + selection, }: { + selection?: UpdateMachineSelection; downloadLocal?: (reportError: ReportLocalUpdateError) => Promise; installLocal?: (reportError: ReportLocalUpdateError) => Promise; }) => { + const selectedServers = selectUpdateMachines(servers, selection?.environmentIds); + const selectedProviders = selectUpdateMachines(providers.machines, selection?.environmentIds); + if (selection?.updateLocal === false) { + downloadLocal = undefined; + installLocal = undefined; + } const localFailures: Array = []; const reportLocalError: ReportLocalUpdateError = (title, description) => { localFailures.push(`${title}: ${description}`); }; const startedAt = Date.now(); const machinesById = new Map(); - for (const machine of providers.machines) { + for (const machine of selectedProviders) { machinesById.set(machine.environmentId, { environmentId: machine.environmentId, label: machine.label, @@ -121,7 +131,7 @@ export function useUpdateEverything( providerFailure: null, }); } - for (const server of servers) { + for (const server of selectedServers) { const existing = machinesById.get(server.environmentId); machinesById.set(server.environmentId, { environmentId: server.environmentId, @@ -149,7 +159,10 @@ export function useUpdateEverything( const outcome = await runUpdateEverything({ ...(downloadLocal ? { downloadLocal: () => downloadLocal(reportLocalError) } : {}), updateProviders: async () => { - const runs = await providers.updateAll({ quiet: true }); + const runs = await providers.updateAll({ + quiet: true, + ...(selection ? { environmentIds: selection.environmentIds } : {}), + }); const outcomes = runs.map((run) => { const runOutcome = getProviderUpdateRunOutcome(run); if (runOutcome.status !== "updated") { @@ -182,7 +195,7 @@ export function useUpdateEverything( return outcomes; }, updateServers: async () => { - const runs = servers.map(async (server) => { + const runs = selectedServers.map(async (server) => { const serverOutcome = await updateServer(server, { quiet: true }); settleUpdateRunMachine( server.environmentId, @@ -199,7 +212,7 @@ export function useUpdateEverything( return finished; }); await Promise.all( - servers.map((server, index) => + selectedServers.map((server, index) => waitForServerUpdateHandoff(server.environmentId, runs[index]!), ), ); diff --git a/docs/user/updating.md b/docs/user/updating.md index 7e62c7bf580f..589d758fd694 100644 --- a/docs/user/updating.md +++ b/docs/user/updating.md @@ -4,11 +4,15 @@ The app you use and the server running your agents can be on different machines. When a server is behind your web or desktop app, the update button at the bottom of the sidebar and **Settings → Environments** offer to update it. -## Update everything at once +## Choose which machines to update -The update button at the bottom of the sidebar updates every connected machine -in one step. It updates outdated providers, then servers that are behind, then -the desktop app you are using. Servers move to the +The update button at the bottom of the sidebar lets you choose which connected +machines to update. All machines start selected. Deselect a machine to leave its +app, server, and providers running as they are. For example, update your laptop +while keeping your PC host available over Tailscale. + +For the selected machines, it updates outdated providers, then servers that are +behind, then the desktop app you are using if selected. Servers move to the version your desktop app is installing, or to your web app's version. A background service stays on its current release channel. A remote desktop app installs the latest release from its own channel. Hover the button to see what @@ -32,7 +36,7 @@ If a desktop app restarts on its old version, T3 Code tells you the update did not apply. It does not install that version again on its own, even when another device asks. Use the update button on that machine to retry. -The confirmation counts the running threads that the restarts will interrupt, +The confirmation counts the running threads that the selected restarts will interrupt, and how many can resume afterwards. See **Continue threads after restarts** below. From 25705d85ef841e4f5aa8ba187c71fbe3bb2bd96e Mon Sep 17 00:00:00 2001 From: "akashgundapuneni@gmail.com" Date: Tue, 10 Nov 2026 12:28:07 +0100 Subject: [PATCH 2/2] fix(web): preserve machine update exclusions --- .../UpdateEverythingDialog.browser.test.tsx | 41 +++++++++++++++++++ .../sidebar/UpdateEverythingDialog.tsx | 11 ++--- .../sidebar/updateEverything.logic.test.ts | 10 +++++ .../sidebar/updateEverything.logic.ts | 4 ++ 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx index 6e400557607e..632e214a92da 100644 --- a/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx +++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.browser.test.tsx @@ -4,6 +4,7 @@ import { page, userEvent } from "vite-plus/test/browser"; import "~/index.css"; import { render } from "../../../test/render"; +import { describeUpdateEverything } from "./updateEverything.logic"; import { UpdateEverythingDialog } from "./UpdateEverythingDialog"; it("lets a remote user update the laptop without updating the PC host", async () => { @@ -47,3 +48,43 @@ it("lets a remote user update the laptop without updating the PC host", async () await userEvent.click(page.getByRole("button", { name: "Update", exact: true })); expect(onConfirm).toHaveBeenCalledWith({ environmentIds: [laptop], updateLocal: false }); }); +it("keeps local provider exclusions when an app update arrives", async () => { + const pc = EnvironmentId.make("pc"); + const laptop = EnvironmentId.make("laptop"); + const onConfirm = vi.fn(); + const dialog = (app: "this-app" | null) => ( + {}} + selectable + checking + version={app === null ? null : "0.2.0"} + providerCount={2} + rows={ + describeUpdateEverything({ + localVersion: app === null ? null : "0.2.0", + localMachines: [{ environmentId: pc, label: "PC host" }], + servers: [], + providerMachines: [ + { environmentId: pc, label: "PC host", providers: ["Codex"] }, + { environmentId: laptop, label: "Laptop", providers: ["Claude"] }, + ], + skipped: [], + }).rows + } + restarts={[]} + onConfirm={onConfirm} + /> + ); + await using screen = await render(dialog(null)); + await userEvent.click(page.getByRole("checkbox", { name: "PC host" })); + await screen.rerender(dialog("this-app")); + await expect.element(page.getByRole("checkbox", { name: "PC host" })).not.toBeChecked(); + await expect + .element(page.getByText(/1 provider update/)) + .toHaveTextContent("1 provider update · Checking for more…"); + await userEvent.click(page.getByRole("button", { name: "Update", exact: true })); + expect(onConfirm).toHaveBeenCalledWith({ environmentIds: [laptop], updateLocal: false }); + await userEvent.click(page.getByRole("checkbox", { name: "Laptop" })); + await expect.element(page.getByText("Checking for more…", { exact: true })).toBeVisible(); +}); diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx index 01f4ec45d141..20d7d6c7e270 100644 --- a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx +++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx @@ -115,7 +115,7 @@ function UpdateEverythingDialogContent({ }: UpdateEverythingDialogContentProps) { const [excluded, setExcluded] = useState>(() => new Set()); const rowKey = (row: UpdateEverythingRow) => - row.app === "this-app" ? "this-app" : (row.environmentId ?? "this-app"); + row.hostedByThisApp || 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( @@ -127,6 +127,7 @@ function UpdateEverythingDialogContent({ const selectedProviderCount = selectable ? selected.reduce((count, row) => count + row.providers.length, 0) : providerCount; + const selectedVersion = selected.some((row) => row.app !== null) ? version : null; const stopped = useAtomValue( runningThreadCountAtom( restarts @@ -155,17 +156,17 @@ function UpdateEverythingDialogContent({ {title ?? `Update ${selected.length} ${selected.length === 1 ? "machine" : "machines"}?`} - {version !== null ? ( + {selectedVersion !== null ? ( <> - T3 Code {version} + T3 Code {selectedVersion} ) : null} - {version !== null && selectedProviderCount > 0 ? " · " : null} + {selectedVersion !== null && selectedProviderCount > 0 ? " · " : null} {selectedProviderCount > 0 ? `${selectedProviderCount} provider ${selectedProviderCount === 1 ? "update" : "updates"}` : null} {checking - ? `${version !== null || providerCount > 0 ? " · " : ""}Checking for more…` + ? `${selectedVersion !== null || selectedProviderCount > 0 ? " · " : ""}Checking for more…` : null} diff --git a/apps/web/src/components/sidebar/updateEverything.logic.test.ts b/apps/web/src/components/sidebar/updateEverything.logic.test.ts index 2a90d45113fb..6c1615638cad 100644 --- a/apps/web/src/components/sidebar/updateEverything.logic.test.ts +++ b/apps/web/src/components/sidebar/updateEverything.logic.test.ts @@ -362,6 +362,16 @@ describe("describeUpdateEverything", () => { ], }); + expect( + describeUpdateEverything({ + localVersion: null, + localMachines: [{ environmentId: id("local"), label: "Mainbook" }], + servers: [], + providerMachines: [{ environmentId: id("local"), label: "Mainbook", providers: ["Codex"] }], + skipped: [], + }).rows[0], + ).toMatchObject({ app: null, hostedByThisApp: true }); + expect( rows.map(({ label, app, skipped, providers }) => [label, app, skipped, providers]), ).toEqual([ diff --git a/apps/web/src/components/sidebar/updateEverything.logic.ts b/apps/web/src/components/sidebar/updateEverything.logic.ts index 8c92bf1ccb43..dd715c290c46 100644 --- a/apps/web/src/components/sidebar/updateEverything.logic.ts +++ b/apps/web/src/components/sidebar/updateEverything.logic.ts @@ -600,6 +600,7 @@ export interface UpdateEverythingPlan { } export interface UpdateEverythingRow { + readonly hostedByThisApp?: boolean; readonly environmentId: EnvironmentId | null; readonly label: string; readonly app: "this-app" | "server" | "desktop-app" | null; @@ -644,6 +645,9 @@ export function describeUpdateEverything(plan: UpdateEverythingPlan): { const row = rows.get(environmentId) ?? { environmentId, label, + hostedByThisApp: + environmentId === null || + plan.localMachines.some((machine) => machine.environmentId === environmentId), app: null, skipped: null, providers: [],