From 037b5122d9731698a64150e8c79a8d3bddd67968 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:27:06 -0700 Subject: [PATCH 1/2] Show cached machine plan immediately --- Sources/Cloud/NewMachineSheetPresenter.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Sources/Cloud/NewMachineSheetPresenter.swift b/Sources/Cloud/NewMachineSheetPresenter.swift index 16913fd7d6ec..e28b7b2af929 100644 --- a/Sources/Cloud/NewMachineSheetPresenter.swift +++ b/Sources/Cloud/NewMachineSheetPresenter.swift @@ -243,11 +243,22 @@ final class NewMachineSheetPresenter: NSObject, NewMachineSheetPresenting { let selectionID = UUID() pendingSelectionID = selectionID let coordinator = MachineCreateCoordinator.shared + // Seed the model from the shared cache before the first SwiftUI layout. + // A warmed cache is still revalidated below, but it should not make a + // ready sheet flash its loading state while that happens. + let cachedData = dataCache?.currentData + let cachedLimits = cachedData?.limits + let hasCachedPlan = cachedData?.hasPlan == true let model = NewMachineModel( mode: .newMachine, - plan: nil, + plan: cachedData?.plan, + memoryOptionsMb: cachedLimits?.memoryOptionsMb ?? [], + lockedMemoryOptionsMb: cachedLimits?.lockedMemoryOptionsMb, + memoryUpgradePlanId: cachedLimits?.memoryUpgradePlanId, + memoryUpgradePlansByMb: cachedLimits?.memoryUpgradePlansByMb, + vcpusByMemoryMb: cachedLimits?.vcpusByMemoryMb, selectionWindowID: preferredWindow.flatMap { AppDelegate.shared?.mainWindowId(from: $0) }, - planIsLoading: true, + planIsLoading: !hasCachedPlan, submit: { [weak self] request in guard let self, self.pendingSelectionID == selectionID else { return false } guard let effectiveRequest = self.reserving(request, preferredWindow: preferredWindow) else { return false } From 7ca410e2ef8786f373f7d06c6eff66a9e0a51f64 Mon Sep 17 00:00:00 2001 From: Austin Wang <38676809+austinywang@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:07:13 -0700 Subject: [PATCH 2/2] Address cached sheet review feedback --- Sources/Cloud/NewMachineSheetPresenter.swift | 20 +++++++++----- cmuxTests/NewMachineSheetPresenterTests.swift | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Sources/Cloud/NewMachineSheetPresenter.swift b/Sources/Cloud/NewMachineSheetPresenter.swift index e28b7b2af929..9cb2df79cb68 100644 --- a/Sources/Cloud/NewMachineSheetPresenter.swift +++ b/Sources/Cloud/NewMachineSheetPresenter.swift @@ -27,6 +27,15 @@ final class NewMachineSheetPresenter: NSObject, NewMachineSheetPresenting { /// The app's plan and network catalog cache; nil only in tests. private var dataCache: NewMachineSheetDataCache? { NewMachineSheetDataCache.shared } + /// The plan state used to build a fetching-plan sheet before its first + /// layout. A missing or incomplete cache keeps the loading indicator on; + /// complete cached data is shown immediately while it is revalidated. + static func initialPlanState(from data: NewMachineSheetData?) -> ( + plan: MachinePlanSnapshot?, limits: VMPlanLimits?, isLoading: Bool + ) { + (plan: data?.plan, limits: data?.limits, isLoading: data?.hasPlan != true) + } + /// The shared paywall decision used by both sheet entrypoints. static func shouldPresentUpgrade(for plan: MachinePlanSnapshot?) -> Bool { guard let plan else { return false } @@ -246,19 +255,18 @@ final class NewMachineSheetPresenter: NSObject, NewMachineSheetPresenting { // Seed the model from the shared cache before the first SwiftUI layout. // A warmed cache is still revalidated below, but it should not make a // ready sheet flash its loading state while that happens. - let cachedData = dataCache?.currentData - let cachedLimits = cachedData?.limits - let hasCachedPlan = cachedData?.hasPlan == true + let initialPlan = Self.initialPlanState(from: dataCache?.currentData) + let cachedLimits = initialPlan.limits let model = NewMachineModel( mode: .newMachine, - plan: cachedData?.plan, + plan: initialPlan.plan, memoryOptionsMb: cachedLimits?.memoryOptionsMb ?? [], lockedMemoryOptionsMb: cachedLimits?.lockedMemoryOptionsMb, memoryUpgradePlanId: cachedLimits?.memoryUpgradePlanId, memoryUpgradePlansByMb: cachedLimits?.memoryUpgradePlansByMb, vcpusByMemoryMb: cachedLimits?.vcpusByMemoryMb, selectionWindowID: preferredWindow.flatMap { AppDelegate.shared?.mainWindowId(from: $0) }, - planIsLoading: !hasCachedPlan, + planIsLoading: initialPlan.isLoading, submit: { [weak self] request in guard let self, self.pendingSelectionID == selectionID else { return false } guard let effectiveRequest = self.reserving(request, preferredWindow: preferredWindow) else { return false } @@ -274,7 +282,7 @@ final class NewMachineSheetPresenter: NSObject, NewMachineSheetPresenting { guard let self, let model, self.pendingSelectionID == selectionID else { return } self.beginPlanLoad(model: model, selectionID: selectionID) } - present(model: model, preferredWindow: preferredWindow, loadPlanFromCache: false) + present(model: model, preferredWindow: preferredWindow, loadPlanFromCache: true) beginPlanLoad(model: model, selectionID: selectionID) let request = await withTaskCancellationHandler(operation: { diff --git a/cmuxTests/NewMachineSheetPresenterTests.swift b/cmuxTests/NewMachineSheetPresenterTests.swift index ddf2b1d2449a..0db6c4478cef 100644 --- a/cmuxTests/NewMachineSheetPresenterTests.swift +++ b/cmuxTests/NewMachineSheetPresenterTests.swift @@ -11,6 +11,33 @@ import Testing @MainActor @Suite("New machine sheet plan readiness") struct NewMachineSheetPresenterTests { + @Test("initial plan state shows complete cache data without loading") + func initialPlanStateUsesWarmAndColdCacheStates() { + let limits = VMPlanLimits( + maxActiveVms: 10, + planId: "pro", + freeAccessWindowDays: 0, + memoryOptionsMb: [4096, 8192] + ) + let warm = NewMachineSheetData( + hasPlan: true, + limits: limits, + activeCount: 2, + catalog: nil, + catalogFailed: false + ) + + let warmState = NewMachineSheetPresenter.initialPlanState(from: warm) + #expect(warmState.plan?.planId == "pro") + #expect(warmState.limits?.memoryOptionsMb == [4096, 8192]) + #expect(!warmState.isLoading) + + let coldState = NewMachineSheetPresenter.initialPlanState(from: nil) + #expect(coldState.plan == nil) + #expect(coldState.limits == nil) + #expect(coldState.isLoading) + } + @Test("cached plan takes precedence over a stale caller plan") func cachedPlanTakesPrecedence() { let cached = MachineSnapshotBuilder.planSnapshot(