From b9d98943b8246844f19915b5f9e708eda151d4d2 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Sat, 7 Nov 2026 02:10:57 +0800 Subject: [PATCH 1/5] fix(web): checking for updates also checks provider versions Co-Authored-By: Claude Opus 5.5 --- .../ProviderUpdatesAction.browser.test.tsx | 25 +++++++++++++------ .../src/components/ProviderUpdatesAction.tsx | 21 ++++++++++++++++ .../components/settings/SettingsPanels.tsx | 3 +++ .../components/sidebar/SidebarUpdatePill.tsx | 3 +++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx b/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx index d6ca540686f0..fda0119aab12 100644 --- a/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx @@ -21,6 +21,7 @@ const state = vi.hoisted(() => ({ }[], providers: new Map>(), requests: [] as EnvironmentId[], + checks: [] as { environmentId: EnvironmentId; input: unknown }[], sessions: new Map>>(), })); @@ -41,18 +42,19 @@ vi.mock(import("@t3tools/client-runtime/state/session"), async (original) => { }; }); vi.mock(import("~/state/server"), () => ({ - serverEnvironment: { updateProvider: {} } as never, + serverEnvironment: { updateProvider: "update", refreshProviders: "refresh" } as never, })); vi.mock(import("~/state/use-atom-command"), () => ({ - useAtomCommand: (() => - async ({ environmentId }: { environmentId: EnvironmentId }) => { - state.requests.push(environmentId); + useAtomCommand: ((command: string) => + async ({ environmentId, input }: { environmentId: EnvironmentId; input: unknown }) => { + if (command === "refresh") state.checks.push({ environmentId, input }); + else state.requests.push(environmentId); return AsyncResult.success(undefined); }) as never, })); import { appAtomRegistry } from "~/rpc/atomRegistry"; -import { useProviderUpdates } from "./ProviderUpdatesAction"; +import { useCheckProviderUpdates, useProviderUpdates } from "./ProviderUpdatesAction"; const provider: ServerProvider = { instanceId: ProviderInstanceId.make("codex"), @@ -102,6 +104,7 @@ function setup() { state.environments.map(({ environmentId }) => [environmentId, [provider]]), ); state.requests = []; + state.checks = []; const sessions = new Map>>( [ [state.environments[0]!.environmentId, Atom.make(session(["orchestration:read"]))], @@ -118,14 +121,17 @@ function setup() { return { sessions }; } -it("bulk provider updates exclude connections without a confirmed management grant", async () => { +it("provider updates and checks exclude connections without a confirmed management grant", async () => { const fixture = setup(); let updates!: ReturnType; + let check!: ReturnType; function Probe() { const nextUpdates = useProviderUpdates(); + const nextCheck = useCheckProviderUpdates(); useEffect(() => { updates = nextUpdates; - }, [nextUpdates]); + check = nextCheck; + }, [nextUpdates, nextCheck]); return null; } await using _screen = await render( @@ -140,6 +146,11 @@ it("bulk provider updates exclude connections without a confirmed management gra await updates.updateAll({ quiet: true }); }); expect(state.requests).toEqual([writableId]); + await act(async () => { + await check(); + }); + // Only a model refresh drops the server's cached latest provider versions. + expect(state.checks).toEqual([{ environmentId: writableId, input: { refreshModels: true } }]); await act(() => { appAtomRegistry.set(fixture.sessions.get(writableId)!, session(["orchestration:read"])); diff --git a/apps/web/src/components/ProviderUpdatesAction.tsx b/apps/web/src/components/ProviderUpdatesAction.tsx index 57573806e45d..351d1dddf6a2 100644 --- a/apps/web/src/components/ProviderUpdatesAction.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.tsx @@ -16,6 +16,27 @@ import { Button } from "./ui/button"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; +// `refreshModels` is the only refresh that drops the server's hour-long latest-version cache. +export function useCheckProviderUpdates() { + const { environments } = useEnvironments(); + const manageableIds = useEnvironmentsWithScope(environments, AuthProvidersManageScope); + const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { + reportFailure: false, + }); + return () => + Promise.all( + environments + .filter( + (environment) => + environment.connection.phase === "connected" && + manageableIds.has(environment.environmentId), + ) + .map(({ environmentId }) => + refreshProviders({ environmentId, input: { refreshModels: true } }), + ), + ); +} + export function useProviderUpdates() { const { environments } = useEnvironments(); const manageableIds = useEnvironmentsWithScope(environments, AuthProvidersManageScope); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 00bdcf6cf57b..a37bf1581990 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -110,6 +110,7 @@ import { StorageCleanupSections } from "./StorageSettings"; import { UpdateTrackSwitchDialog } from "./UpdateTrackSwitchDialog"; import { environmentIdsKey } from "~/state/environmentAtomList"; import { environmentSessionStatesAtom } from "~/state/session"; +import { useCheckProviderUpdates } from "../ProviderUpdatesAction"; import { canUpdateServer } from "../ServerUpdateAction"; import { useUpdateEverythingRun } from "../sidebar/updateEverythingRun"; import { awaitsTrackSwitch, channelSwitchLabel } from "../sidebar/updateEverything.logic"; @@ -162,6 +163,7 @@ function AboutVersionSection() { ); // A second run would replace the first run's progress and local download. const updateRun = useUpdateEverythingRun(); + const checkProviderUpdates = useCheckProviderUpdates(); const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge); const selectedUpdateChannel = updateState?.channel ?? "latest"; @@ -252,6 +254,7 @@ function AboutVersionSection() { } if (typeof bridge.checkForUpdate !== "function") return; + void checkProviderUpdates(); void bridge .checkForUpdate() .then((result) => { diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index 6871b9149fd4..da635794879f 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -28,6 +28,7 @@ import { shouldToastDesktopUpdateActionResult, } from "../desktopUpdate.logic"; import { showDesktopUpdateDownloadedToast } from "../desktopUpdate.toast"; +import { useCheckProviderUpdates } from "../ProviderUpdatesAction"; import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; import { Popover, PopoverCreateHandle, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { SidebarMenuItem } from "../ui/sidebar"; @@ -230,6 +231,7 @@ function SidebarUpdateControl() { const localUpdateVersion = state && action !== "none" ? getDesktopUpdateDownloadedVersion(state) : null; const everything = useUpdateEverything(localUpdateVersion); + const checkProviderUpdates = useCheckProviderUpdates(); const runsEverything = everything.hasUpdates && !isDownloading; const run = useUpdateEverythingRun(); const isRunningEverything = run !== null; @@ -325,6 +327,7 @@ function SidebarUpdateControl() { setIsCheckAnimationLatched(true); setCheckAnimationKey((key) => key + 1); } + void checkProviderUpdates(); void bridge .checkForUpdate() .then((result) => { From 59d227e6767f9fe96564401fafe89c5a800bfef4 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Sat, 7 Nov 2026 02:29:46 +0800 Subject: [PATCH 2/5] fix(web): report failed provider checks and keep the button busy until they finish Co-Authored-By: Claude Opus 5.5 --- .../src/components/ProviderUpdatesAction.tsx | 39 ++++++++++++++++--- .../components/sidebar/SidebarUpdatePill.tsx | 9 +++-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/ProviderUpdatesAction.tsx b/apps/web/src/components/ProviderUpdatesAction.tsx index 351d1dddf6a2..50b4c78fa7ca 100644 --- a/apps/web/src/components/ProviderUpdatesAction.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.tsx @@ -1,5 +1,9 @@ import { AuthProvidersManageScope, PROVIDER_DISPLAY_NAMES } from "@t3tools/contracts"; -import { settlePromise } from "@t3tools/client-runtime/state/runtime"; +import { + isAtomCommandInterrupted, + settlePromise, + squashAtomCommandFailure, +} from "@t3tools/client-runtime/state/runtime"; import { useRef, useState } from "react"; import { useEnvironmentProviders, useEnvironments } from "~/state/environments"; @@ -23,18 +27,41 @@ export function useCheckProviderUpdates() { const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); - return () => - Promise.all( + const inFlight = useRef | null>(null); + + const check = async () => { + const failures = await Promise.all( environments .filter( (environment) => environment.connection.phase === "connected" && manageableIds.has(environment.environmentId), ) - .map(({ environmentId }) => - refreshProviders({ environmentId, input: { refreshModels: true } }), - ), + .map(async ({ environmentId, label }) => { + const result = await refreshProviders({ environmentId, input: { refreshModels: true } }); + if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return []; + const error = squashAtomCommandFailure(result); + return [`${label}: ${error instanceof Error ? error.message : "Refresh failed."}`]; + }), + ); + const lines = failures.flat(); + if (lines.length === 0) return; + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Could not check provider updates", + description: {lines.join("\n")}, + }), ); + }; + + // A repeat click must not start a second model rediscovery while one is running. + return () => { + inFlight.current ??= check().finally(() => { + inFlight.current = null; + }); + return inFlight.current; + }; } export function useProviderUpdates() { diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index da635794879f..28659aaf1614 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -327,8 +327,7 @@ function SidebarUpdateControl() { setIsCheckAnimationLatched(true); setCheckAnimationKey((key) => key + 1); } - void checkProviderUpdates(); - void bridge + const appCheck = bridge .checkForUpdate() .then((result) => { if (result.checked) return; @@ -351,8 +350,10 @@ function SidebarUpdateControl() { description: error instanceof Error ? error.message : "Update check failed.", }), ); - }) - .finally(() => setIsActionPending(false)); + }); + void Promise.allSettled([appCheck, checkProviderUpdates()]).then(() => + setIsActionPending(false), + ); }; const runEverything = async () => { From 59f236a073e308fb4df861e72c3b6c3a7d94ceff Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Sat, 7 Nov 2026 02:36:11 +0800 Subject: [PATCH 3/5] fix(web): avoid ??= so React Compiler still optimizes the provider check hook Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/ProviderUpdatesAction.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ProviderUpdatesAction.tsx b/apps/web/src/components/ProviderUpdatesAction.tsx index 50b4c78fa7ca..c9de6ce6a426 100644 --- a/apps/web/src/components/ProviderUpdatesAction.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.tsx @@ -57,10 +57,12 @@ export function useCheckProviderUpdates() { // A repeat click must not start a second model rediscovery while one is running. return () => { - inFlight.current ??= check().finally(() => { + if (inFlight.current) return inFlight.current; + const running = check().finally(() => { inFlight.current = null; }); - return inFlight.current; + inFlight.current = running; + return running; }; } From a31dd74579a6ae24d8e0877a30a745f2f3f2d8a3 Mon Sep 17 00:00:00 2001 From: Yuruzuu Date: Sat, 7 Nov 2026 02:41:10 +0800 Subject: [PATCH 4/5] fix(web): the update button keeps spinning until provider checks finish Co-Authored-By: Claude Opus 5.5 --- .../ProviderUpdatesAction.browser.test.tsx | 27 ++++++++++++++++--- .../components/sidebar/SidebarUpdatePill.tsx | 11 ++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx b/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx index fda0119aab12..72dded8fa01a 100644 --- a/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx @@ -7,6 +7,7 @@ import { ProviderInstanceId, type ServerProvider, } from "@t3tools/contracts"; +import * as Cause from "effect/Cause"; import { AsyncResult, Atom } from "effect/reactivity"; import { useEffect } from "react"; import { expect, it, vi } from "vite-plus/test"; @@ -22,6 +23,7 @@ const state = vi.hoisted(() => ({ providers: new Map>(), requests: [] as EnvironmentId[], checks: [] as { environmentId: EnvironmentId; input: unknown }[], + checkResult: null as AsyncResult.AsyncResult | null, sessions: new Map>>(), })); @@ -47,13 +49,17 @@ vi.mock(import("~/state/server"), () => ({ vi.mock(import("~/state/use-atom-command"), () => ({ useAtomCommand: ((command: string) => async ({ environmentId, input }: { environmentId: EnvironmentId; input: unknown }) => { - if (command === "refresh") state.checks.push({ environmentId, input }); - else state.requests.push(environmentId); - return AsyncResult.success(undefined); + if (command !== "refresh") { + state.requests.push(environmentId); + return AsyncResult.success(undefined); + } + state.checks.push({ environmentId, input }); + return state.checkResult ?? AsyncResult.success(undefined); }) as never, })); import { appAtomRegistry } from "~/rpc/atomRegistry"; +import { toastManager } from "./ui/toast"; import { useCheckProviderUpdates, useProviderUpdates } from "./ProviderUpdatesAction"; const provider: ServerProvider = { @@ -105,6 +111,7 @@ function setup() { ); state.requests = []; state.checks = []; + state.checkResult = null; const sessions = new Map>>( [ [state.environments[0]!.environmentId, Atom.make(session(["orchestration:read"]))], @@ -152,6 +159,20 @@ it("provider updates and checks exclude connections without a confirmed manageme // Only a model refresh drops the server's cached latest provider versions. expect(state.checks).toEqual([{ environmentId: writableId, input: { refreshModels: true } }]); + using toast = vi.spyOn(toastManager, "add"); + state.checkResult = AsyncResult.failure(Cause.interrupt()); + await act(async () => { + await check(); + }); + expect(toast).not.toHaveBeenCalled(); + state.checkResult = AsyncResult.failure(Cause.fail(new Error("offline"))); + await act(async () => { + await check(); + }); + expect(toast).toHaveBeenCalledWith( + expect.objectContaining({ title: "Could not check provider updates" }), + ); + await act(() => { appAtomRegistry.set(fixture.sessions.get(writableId)!, session(["orchestration:read"])); }); diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index 28659aaf1614..60dba17ac3be 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -201,6 +201,7 @@ function SidebarUpdateControl() { // The desktop updates this machine, whose policy the primary environment reports. const updatesDenial = managedUpdatesDenial(usePrimaryManagedPolicy()); const [isActionPending, setIsActionPending] = useState(false); + const [isCheckingProviders, setIsCheckingProviders] = useState(false); const [isConfirmingEverything, setIsConfirmingEverything] = useState(false); const [checkAnimationKey, setCheckAnimationKey] = useState(0); const [releaseNotesPopoverHandle] = useState(() => PopoverCreateHandle()); @@ -237,7 +238,7 @@ function SidebarUpdateControl() { const isRunningEverything = run !== null; const showCheckIcon = shouldShowDesktopUpdateCheckIcon({ isAnimationLatched: isCheckAnimationLatched, - isChecking: state?.status === "checking", + isChecking: state?.status === "checking" || isCheckingProviders, prefersReducedMotion, }); const { iconStatus, showUpdateDetails, showUpdateIconState } = resolveSidebarUpdatePresentation({ @@ -351,9 +352,9 @@ function SidebarUpdateControl() { }), ); }); - void Promise.allSettled([appCheck, checkProviderUpdates()]).then(() => - setIsActionPending(false), - ); + setIsCheckingProviders(true); + const providerCheck = checkProviderUpdates().finally(() => setIsCheckingProviders(false)); + void Promise.allSettled([appCheck, providerCheck]).then(() => setIsActionPending(false)); }; const runEverything = async () => { @@ -389,7 +390,7 @@ function SidebarUpdateControl() { const handleCheckAnimationIteration = () => { setIsCheckAnimationLatched( shouldContinueDesktopUpdateCheckAnimation({ - isChecking: state?.status === "checking", + isChecking: state?.status === "checking" || isCheckingProviders, prefersReducedMotion, }), ); From 73eed4c17da74af730b16e6dde6163f99e2c54ee Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 9 Nov 2026 12:25:11 -0800 Subject: [PATCH 5/5] fix(web): opening the update dialog checks every machine for provider updates The update button only reaches the check branch when no update is known. With one update known, it opens the dialog, so machines that still cached an older latest version never showed their rows. The dialog now starts the same check when it opens and says so while it runs. This check is quiet: the user did not ask for it, and servers restart during the update. Co-Authored-By: Claude Code --- .../ProviderUpdatesAction.browser.test.tsx | 6 +++++- apps/web/src/components/ProviderUpdatesAction.tsx | 11 ++++++----- .../src/components/sidebar/SidebarUpdatePill.tsx | 13 +++++++++---- .../components/sidebar/UpdateEverythingDialog.tsx | 6 ++++++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx b/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx index 72dded8fa01a..a00914ad9dcd 100644 --- a/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.browser.test.tsx @@ -156,7 +156,7 @@ it("provider updates and checks exclude connections without a confirmed manageme await act(async () => { await check(); }); - // Only a model refresh drops the server's cached latest provider versions. + // A model refresh always drops the server's cached latest provider versions. expect(state.checks).toEqual([{ environmentId: writableId, input: { refreshModels: true } }]); using toast = vi.spyOn(toastManager, "add"); @@ -166,6 +166,10 @@ it("provider updates and checks exclude connections without a confirmed manageme }); expect(toast).not.toHaveBeenCalled(); state.checkResult = AsyncResult.failure(Cause.fail(new Error("offline"))); + await act(async () => { + await check({ quiet: true }); + }); + expect(toast).not.toHaveBeenCalled(); await act(async () => { await check(); }); diff --git a/apps/web/src/components/ProviderUpdatesAction.tsx b/apps/web/src/components/ProviderUpdatesAction.tsx index c9de6ce6a426..4834487d55e6 100644 --- a/apps/web/src/components/ProviderUpdatesAction.tsx +++ b/apps/web/src/components/ProviderUpdatesAction.tsx @@ -20,7 +20,8 @@ import { Button } from "./ui/button"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; -// `refreshModels` is the only refresh that drops the server's hour-long latest-version cache. +// `refreshModels` always drops the server's hour-long latest-version cache. `modelCatalogOnly` +// drops it only when the model manifest changed. export function useCheckProviderUpdates() { const { environments } = useEnvironments(); const manageableIds = useEnvironmentsWithScope(environments, AuthProvidersManageScope); @@ -29,7 +30,7 @@ export function useCheckProviderUpdates() { }); const inFlight = useRef | null>(null); - const check = async () => { + const check = async (quiet: boolean) => { const failures = await Promise.all( environments .filter( @@ -45,7 +46,7 @@ export function useCheckProviderUpdates() { }), ); const lines = failures.flat(); - if (lines.length === 0) return; + if (quiet || lines.length === 0) return; toastManager.add( stackedThreadToast({ type: "error", @@ -56,9 +57,9 @@ export function useCheckProviderUpdates() { }; // A repeat click must not start a second model rediscovery while one is running. - return () => { + return ({ quiet = false }: { readonly quiet?: boolean } = {}) => { if (inFlight.current) return inFlight.current; - const running = check().finally(() => { + const running = check(quiet).finally(() => { inFlight.current = null; }); inFlight.current = running; diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index 60dba17ac3be..3c18b2c7a927 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -201,7 +201,8 @@ function SidebarUpdateControl() { // The desktop updates this machine, whose policy the primary environment reports. const updatesDenial = managedUpdatesDenial(usePrimaryManagedPolicy()); const [isActionPending, setIsActionPending] = useState(false); - const [isCheckingProviders, setIsCheckingProviders] = useState(false); + const [providerCheck, setProviderCheck] = useState<"button" | "dialog" | null>(null); + const isCheckingProviders = providerCheck === "button"; const [isConfirmingEverything, setIsConfirmingEverything] = useState(false); const [checkAnimationKey, setCheckAnimationKey] = useState(0); const [releaseNotesPopoverHandle] = useState(() => PopoverCreateHandle()); @@ -286,6 +287,9 @@ function SidebarUpdateControl() { if (runsEverything) { setIsConfirmingEverything(true); + // Machines that still cache an older latest version add their rows while the dialog is open. + setProviderCheck("dialog"); + void checkProviderUpdates({ quiet: true }).finally(() => setProviderCheck(null)); return; } @@ -352,9 +356,9 @@ function SidebarUpdateControl() { }), ); }); - setIsCheckingProviders(true); - const providerCheck = checkProviderUpdates().finally(() => setIsCheckingProviders(false)); - void Promise.allSettled([appCheck, providerCheck]).then(() => setIsActionPending(false)); + setProviderCheck("button"); + const providersChecked = checkProviderUpdates().finally(() => setProviderCheck(null)); + void Promise.allSettled([appCheck, providersChecked]).then(() => setIsActionPending(false)); }; const runEverything = async () => { @@ -415,6 +419,7 @@ function SidebarUpdateControl() { providerCount={everything.providerCount} rows={everything.rows} restarts={everything.restarts} + checking={providerCheck === "dialog"} onConfirm={() => void runEverything()} /> ); diff --git a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx index 9bf101b6fa4e..86c1fd2841c7 100644 --- a/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx +++ b/apps/web/src/components/sidebar/UpdateEverythingDialog.tsx @@ -55,6 +55,8 @@ interface UpdateEverythingDialogProps { readonly disabledReason?: string | null; /** The confirmed work is starting; it cannot be cancelled from here. */ readonly busy?: boolean; + /** Machines are still looking for provider updates, so more rows can appear. */ + readonly checking?: boolean; } type UpdateEverythingDialogContentProps = Omit< @@ -102,6 +104,7 @@ function UpdateEverythingDialogContent({ notes = [], disabledReason = null, busy = false, + checking = false, }: UpdateEverythingDialogContentProps) { const stopped = useAtomValue( runningThreadCountAtom( @@ -141,6 +144,9 @@ function UpdateEverythingDialogContent({ {providerCount > 0 ? `${providerCount} provider ${providerCount === 1 ? "update" : "updates"}` : null} + {checking + ? `${version !== null || providerCount > 0 ? " · " : ""}Checking for more…` + : null}