This commit is contained in:
austinpower1258 2026-03-05 20:51:51 -08:00
parent 1408cbb68c
commit 990b6ba12a
4 changed files with 123 additions and 11 deletions

View file

@ -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

View file

@ -564,6 +564,7 @@ class TabManager: ObservableObject {
@Published var tabs: [Workspace] = []
@Published private(set) var isWorkspaceCycleHot: Bool = false
@Published private(set) var pendingBackgroundWorkspaceLoadIds: Set<UUID> = []
/// 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<UUID>) {
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 }

View file

@ -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

View file

@ -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