From 990b6ba12aa931df3cdf1d0fa028a433e5e8c427 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Thu, 5 Mar 2026 20:51:51 -0800 Subject: [PATCH] ok --- Sources/ContentView.swift | 84 +++++++++++++++++++++++++++++++- Sources/TabManager.swift | 24 ++++++++- Sources/TerminalController.swift | 14 +++--- Sources/Workspace.swift | 12 +++++ 4 files changed, 123 insertions(+), 11 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 0b9bd263..9a50947e 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -1797,6 +1797,7 @@ struct ContentView: View { ForEach(mountedWorkspaces) { tab in let isSelectedWorkspace = selectedWorkspaceId == tab.id let isRetiringWorkspace = retiringWorkspaceId == tab.id + let shouldPrimeInBackground = tabManager.pendingBackgroundWorkspaceLoadIds.contains(tab.id) // Keep the retiring workspace visible during handoff, but never input-active. // Allowing both selected+retiring workspaces to be input-active lets the // old workspace steal first responder (notably with WKWebView), which can @@ -1823,6 +1824,9 @@ struct ContentView: View { .allowsHitTesting(isSelectedWorkspace) .accessibilityHidden(!isVisible) .zIndex(isSelectedWorkspace ? 2 : (isRetiringWorkspace ? 1 : 0)) + .task(id: shouldPrimeInBackground ? tab.id : nil) { + await primeBackgroundWorkspaceIfNeeded(workspaceId: tab.id) + } } } .opacity(sidebarSelectionState.selection == .tabs ? 1 : 0) @@ -2167,6 +2171,10 @@ struct ContentView: View { reconcileMountedWorkspaceIds() }) + view = AnyView(view.onReceive(tabManager.$pendingBackgroundWorkspaceLoadIds) { _ in + reconcileMountedWorkspaceIds() + }) + view = AnyView(view.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidSetTitle)) { notification in guard let tabId = notification.userInfo?[GhosttyNotificationKey.tabId] as? UUID, tabId == tabManager.selectedTabId else { return } @@ -2227,6 +2235,7 @@ struct ContentView: View { if let previousSelectedWorkspaceId, !existingIds.contains(previousSelectedWorkspaceId) { self.previousSelectedWorkspaceId = tabManager.selectedTabId } + tabManager.pruneBackgroundWorkspaceLoads(existingIds: existingIds) reconcileMountedWorkspaceIds(tabs: tabs) selectedTabIds = selectedTabIds.filter { existingIds.contains($0) } if selectedTabIds.isEmpty, let selectedId = tabManager.selectedTabId { @@ -2531,9 +2540,10 @@ struct ContentView: View { let currentTabs = tabs ?? tabManager.tabs let orderedTabIds = currentTabs.map { $0.id } let effectiveSelectedId = selectedId ?? tabManager.selectedTabId - let pinnedIds = retiringWorkspaceId.map { Set([ $0 ]) } ?? [] + let handoffPinnedIds = retiringWorkspaceId.map { Set([ $0 ]) } ?? [] + let pinnedIds = handoffPinnedIds.union(tabManager.pendingBackgroundWorkspaceLoadIds) let isCycleHot = tabManager.isWorkspaceCycleHot - let shouldKeepHandoffPair = isCycleHot && !pinnedIds.isEmpty + let shouldKeepHandoffPair = isCycleHot && !handoffPinnedIds.isEmpty let baseMaxMounted = shouldKeepHandoffPair ? WorkspaceMountPolicy.maxMountedWorkspacesDuringCycle : WorkspaceMountPolicy.maxMountedWorkspaces @@ -2570,6 +2580,76 @@ struct ContentView: View { #endif } + private enum BackgroundWorkspacePrimeState { + case pending + case completed(reason: String) + } + + private func primeBackgroundWorkspaceIfNeeded(workspaceId: UUID) async { + let shouldPrime = await MainActor.run { + tabManager.pendingBackgroundWorkspaceLoadIds.contains(workspaceId) + } + guard shouldPrime else { return } + +#if DEBUG + let startedAt = ProcessInfo.processInfo.systemUptime + dlog("workspace.backgroundPrime.start workspace=\(workspaceId.uuidString.prefix(5))") +#endif + + let timeout = Date().addingTimeInterval(2.0) + while !Task.isCancelled { + let state = await MainActor.run { + stepBackgroundWorkspacePrime(workspaceId: workspaceId) + } + switch state { + case .pending: + if Date() < timeout { + try? await Task.sleep(nanoseconds: 50_000_000) + continue + } + await MainActor.run { + tabManager.completeBackgroundWorkspaceLoad(for: workspaceId) + } +#if DEBUG + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000 + dlog( + "workspace.backgroundPrime.finish workspace=\(workspaceId.uuidString.prefix(5)) " + + "reason=timeout ms=\(String(format: "%.2f", elapsedMs))" + ) +#endif + return + case .completed(let reason): +#if DEBUG + let elapsedMs = (ProcessInfo.processInfo.systemUptime - startedAt) * 1000 + dlog( + "workspace.backgroundPrime.finish workspace=\(workspaceId.uuidString.prefix(5)) " + + "reason=\(reason) ms=\(String(format: "%.2f", elapsedMs))" + ) +#endif + return + } + } + } + + @MainActor + private func stepBackgroundWorkspacePrime(workspaceId: UUID) -> BackgroundWorkspacePrimeState { + guard tabManager.pendingBackgroundWorkspaceLoadIds.contains(workspaceId) else { + return .completed(reason: "already_cleared") + } + guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { + tabManager.completeBackgroundWorkspaceLoad(for: workspaceId) + return .completed(reason: "workspace_removed") + } + + workspace.requestBackgroundTerminalSurfaceStartIfNeeded() + guard workspace.hasLoadedTerminalSurface() else { + return .pending + } + + tabManager.completeBackgroundWorkspaceLoad(for: workspaceId) + return .completed(reason: "surface_ready") + } + private func addTab() { tabManager.addTab() sidebarSelectionState.selection = .tabs diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index abdb3ea6..cd5ed2da 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -564,6 +564,7 @@ class TabManager: ObservableObject { @Published var tabs: [Workspace] = [] @Published private(set) var isWorkspaceCycleHot: Bool = false + @Published private(set) var pendingBackgroundWorkspaceLoadIds: Set = [] /// Global monotonically increasing counter for CMUX_PORT ordinal assignment. /// Static so port ranges don't overlap across multiple windows (each window has its own TabManager). @@ -799,6 +800,7 @@ class TabManager: ObservableObject { func addWorkspace( workingDirectory overrideWorkingDirectory: String? = nil, select: Bool = true, + eagerLoadTerminal: Bool = false, placementOverride: NewWorkspacePlacement? = nil ) -> Workspace { sentryBreadcrumb("workspace.create", data: ["tabCount": tabs.count + 1]) @@ -819,6 +821,10 @@ class TabManager: ObservableObject { } else { tabs.append(newWorkspace) } + if eagerLoadTerminal { + requestBackgroundWorkspaceLoad(for: newWorkspace.id) + newWorkspace.requestBackgroundTerminalSurfaceStartIfNeeded() + } if select { selectedTabId = newWorkspace.id NotificationCenter.default.post( @@ -837,9 +843,25 @@ class TabManager: ObservableObject { return newWorkspace } + func requestBackgroundWorkspaceLoad(for workspaceId: UUID) { + guard pendingBackgroundWorkspaceLoadIds.insert(workspaceId).inserted else { return } + } + + func completeBackgroundWorkspaceLoad(for workspaceId: UUID) { + guard pendingBackgroundWorkspaceLoadIds.remove(workspaceId) != nil else { return } + } + + func pruneBackgroundWorkspaceLoads(existingIds: Set) { + let pruned = pendingBackgroundWorkspaceLoadIds.intersection(existingIds) + guard pruned != pendingBackgroundWorkspaceLoadIds else { return } + pendingBackgroundWorkspaceLoadIds = pruned + } + // Keep addTab as convenience alias @discardableResult - func addTab(select: Bool = true) -> Workspace { addWorkspace(select: select) } + func addTab(select: Bool = true, eagerLoadTerminal: Bool = false) -> Workspace { + addWorkspace(select: select, eagerLoadTerminal: eagerLoadTerminal) + } func terminalPanelForWorkspaceConfigInheritanceSource() -> TerminalPanel? { guard let workspace = selectedWorkspace else { return nil } diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 56d7205b..dd9d9a1a 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -2687,10 +2687,11 @@ class TerminalController { let startedAt = ProcessInfo.processInfo.systemUptime #endif v2MainSync { - let ws = tabManager.addWorkspace(workingDirectory: cwd, select: shouldFocus) - if !shouldFocus, let terminalPanel = ws.focusedTerminalPanel { - terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded() - } + let ws = tabManager.addWorkspace( + workingDirectory: cwd, + select: shouldFocus, + eagerLoadTerminal: !shouldFocus + ) newId = ws.id } #if DEBUG @@ -10299,10 +10300,7 @@ class TerminalController { let startedAt = ProcessInfo.processInfo.systemUptime #endif DispatchQueue.main.sync { - let workspace = tabManager.addTab(select: focus) - if !focus, let terminalPanel = workspace.focusedTerminalPanel { - terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded() - } + let workspace = tabManager.addTab(select: focus, eagerLoadTerminal: !focus) newTabId = workspace.id } #if DEBUG diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index a4a870ea..cf3d064b 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -1480,6 +1480,18 @@ final class Workspace: Identifiable, ObservableObject { return surfaceKind(for: panel) } + func requestBackgroundTerminalSurfaceStartIfNeeded() { + for terminalPanel in panels.values.compactMap({ $0 as? TerminalPanel }) { + terminalPanel.surface.requestBackgroundSurfaceStartIfNeeded() + } + } + + func hasLoadedTerminalSurface() -> Bool { + let terminalPanels = panels.values.compactMap { $0 as? TerminalPanel } + guard !terminalPanels.isEmpty else { return true } + return terminalPanels.contains { $0.surface.surface != nil } + } + func panelTitle(panelId: UUID) -> String? { guard let panel = panels[panelId] else { return nil } let fallback = panelTitles[panelId] ?? panel.displayTitle