From 1bfe9b4e42404f4c83ba2629736803ef1f03b457 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 31 Oct 2026 22:21:34 +0000 Subject: [PATCH 1/3] fix(next): keep New Tab spare renderer warm Leo asked for every New Tab and new workspace to show a fully rendered composer on the first frame. Keep the parked renderer visible to its offscreen parking view and immediately begin a replacement warmup after each handout. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-790564c45249a0c4cd53 --- .../CmuxNextApp/NewTab/NewTabSparePool.swift | 25 +++++++++++++++---- .../NewTabSpareVisibilityTests.swift | 3 ++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift index 97a76c0c6436..acf91632fbfa 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift @@ -107,7 +107,9 @@ final class NewTabSparePool { /// The parked native spare for the new tab `key`, out of the parking view, or nil (the page is made /// cold). The pane's view for `key` claims it (``claimNative(_:)``); the next spare follows when input is quiet. func takeNative(for key: String) -> NativeNewTabHostingView? { - defer { scheduleWarm() } + // Re-park the replacement in the next main-actor turn. Waiting for the idle-input + // debounce here leaves the next Cmd-T/Cmd-N with no warm page at all. + defer { scheduleWarmImmediately() } guard let host = nativeSpare else { return nil } nativeSpare = nil host.removeFromSuperview() @@ -249,7 +251,10 @@ final class NewTabSparePool { usedThisSession = true observeWindows() if target == nil, let window { retarget(window) } - defer { scheduleWarm() } + // Hand the parked page out now, then start making its replacement without waiting for + // the user's input to go quiet. The replacement is background work on the next turn; + // a later New Tab click therefore does not fall back to a cold mount. + defer { scheduleWarmImmediately() } guard let view = slot.take() else { return nil } snapshotGeneration &+= 1 view.redraw.newTabFrameCaptureChanged = nil @@ -297,9 +302,9 @@ final class NewTabSparePool { view.redraw.rememberFrame(of: view, appearance: appearance) { [weak self, weak view] in guard let self, let view, snapshotGeneration == generation, slot.peek === view, view.superview === parking else { return } - // A transparent offscreen view still receives frames. Hiding stops its renderer's - // animations and lets the transport hold nonurgent pushes until adoption. - view.isHidden = true + // The offscreen parking view already keeps the page out of sight while allowing + // WebKit/Chromium to render. Keep the view visible so its renderer stays warm; + // hiding it here suspends the renderer and makes the next handout cold again. } } } @@ -370,6 +375,16 @@ final class NewTabSparePool { armWarmTimer() } + /// Replaces a handed-out spare on the next main-actor turn. A handout is a background + /// rewarm boundary, not an idle-input boundary: the pool must have another fully rendered + /// composer ready for the next New Tab or new workspace. + private func scheduleWarmImmediately() { + let web = services.agentTabs.canHostChat && (slot.shouldWarm || unparked != nil) + guard target != nil, web || (wantsNative && nativeSpare == nil) else { return } + watchMemoryPressure() + warmTimer.schedule(after: .zero) { @MainActor [weak self] in self?.warmNow() } + } + /// Whether this run has warmed a spare yet: the first one starts at once, before the person /// types (a cold launch's first Cmd-N then finds a page that is loading or loaded, cx-ec99). private var warmedOnce = false diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift index fd83d5835b77..46178b103b4f 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift @@ -64,7 +64,8 @@ struct NewTabSpareVisibilityTests { let view = try #require(pool.spare?.view), host = try #require(view.superview) // The first remembered cover also uses the injected snapshot, with no compositor. view.model.markPainted() - try await PasswordManagerRouteTests.until({ view.isHidden }) + try await PasswordManagerRouteTests.until({ !snapshots.isEmpty }) + #expect(!view.isHidden, "the parked spare must keep its renderer warm after its cover is captured") let frames = AgentPaneNewTabFrame.shared let appearance = host.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) let initial = try #require(snapshots.last) From 3cb97a76f1b2f4004b2e2e4f36e01d387fc77dd9 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 31 Oct 2026 22:26:36 +0000 Subject: [PATCH 2/3] test(next): keep parked spare visible after replacement cover Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-790564c45249a0c4cd53 --- .../Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift index 46178b103b4f..67ac95b99f39 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/NewTabSpareVisibilityTests.swift @@ -80,13 +80,14 @@ struct NewTabSpareVisibilityTests { #expect(!view.isHidden, "the replacement cover cannot be captured from a suspended renderer") try await PasswordManagerRouteTests.until({ !nextFrames.isEmpty }) nextFrames.removeFirst()() - try await PasswordManagerRouteTests.until({ !view.redraw.givesNewTabFrame && view.isHidden }) + try await PasswordManagerRouteTests.until({ !view.redraw.givesNewTabFrame }) + #expect(!view.isHidden, "the replacement cover must leave the parked renderer warm") let cover = try #require(frames.image(size: host.bounds.size, appearance: appearance)) #expect(cover.isValid && cover.size == host.bounds.size, "the replacement must be a usable cover") try #require(snapshots.count == beforeReplacement + 2) #expect(cover === snapshots[beforeReplacement], "the shared cover must be the replacement snapshot") #expect(view.redraw.takeRememberedFrame(size: view.frame.size, appearance: appearance) === snapshots.last, - "the parked page must remember its replacement cover before sleeping") + "the parked page must remember its replacement cover while staying warm") // Adoption during another capture invalidates its eventual sleep callback. frames.chatsAnswered(.init(chats: [], nextCursor: nil, ready: true, enabled: true, design: captureID + "-adoption"), From fc66f4bb8bfce028ab94c81d672c78be21cfb3da Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 31 Oct 2026 22:32:39 +0000 Subject: [PATCH 3/3] fix(next): only rewarm after a spare handout Do not create a replacement web view for a cold handout that found no spare; preserve the idle path when no parked page was available. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-790564c45249a0c4cd53 --- .../Sources/CmuxNextApp/NewTab/NewTabSparePool.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift index acf91632fbfa..4a0f554fcb71 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/NewTab/NewTabSparePool.swift @@ -107,10 +107,10 @@ final class NewTabSparePool { /// The parked native spare for the new tab `key`, out of the parking view, or nil (the page is made /// cold). The pane's view for `key` claims it (``claimNative(_:)``); the next spare follows when input is quiet. func takeNative(for key: String) -> NativeNewTabHostingView? { - // Re-park the replacement in the next main-actor turn. Waiting for the idle-input + guard let host = nativeSpare else { return nil } + // Re-park the replacement in the next main-actor turn. Waiting for the idle-input // debounce here leaves the next Cmd-T/Cmd-N with no warm page at all. defer { scheduleWarmImmediately() } - guard let host = nativeSpare else { return nil } nativeSpare = nil host.removeFromSuperview() // One opening at a time: a spare an earlier open took but no pane claimed (its tab closed first) goes. @@ -251,11 +251,11 @@ final class NewTabSparePool { usedThisSession = true observeWindows() if target == nil, let window { retarget(window) } + guard let view = slot.take() else { return nil } // Hand the parked page out now, then start making its replacement without waiting for // the user's input to go quiet. The replacement is background work on the next turn; // a later New Tab click therefore does not fall back to a cold mount. defer { scheduleWarmImmediately() } - guard let view = slot.take() else { return nil } snapshotGeneration &+= 1 view.redraw.newTabFrameCaptureChanged = nil view.isHidden = false