Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions Glint/Pane/PaneSurfaceRepresentable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import SwiftUI
import AppKit

enum SurfaceReassertionPolicy {
static func shouldReassert(containerIsAttached: Bool,
expectedSurfaceMatches: Bool,
paneIsVisible: Bool) -> Bool {
containerIsAttached && expectedSurfaceMatches && paneIsVisible
}
}

/// Hosts a stable, store-owned `GhosttySurfaceView` inside a fresh container
/// NSView. SwiftUI may rebuild the container any time the split tree reshapes;
/// the surface itself outlives that and just re-parents.
Expand All @@ -15,6 +23,9 @@ struct PaneSurfaceRepresentable: NSViewRepresentable {
/// updateNSView re-runs ~1/s, and re-grabbing the terminal surface here
/// races the palette's search field and yanks focus back off it.
let deferFocus: Bool
/// Evaluated inside the deferred re-pin, not when SwiftUI builds the view:
/// workspace/tab selection may change before that callback runs.
let isPaneVisible: () -> Bool

func makeNSView(context: Context) -> NoDragContainerView {
let container = NoDragContainerView()
Expand Down Expand Up @@ -131,10 +142,15 @@ struct PaneSurfaceRepresentable: NSViewRepresentable {
// re-parenting the surface into a container that's torn down moments
// later, leaving the live pane blank. Containers that survive the
// commit re-assert their claim right after it; dismantled ones are out
// of the window by then and bail.
// of the window by then and bail. A recycled container can still be in
// the window after a workspace/tab switch, so also verify that this
// surface's pane is the one currently visible.
DispatchQueue.main.async {
guard container.window != nil,
container.expectedSurface === surface else { return }
guard SurfaceReassertionPolicy.shouldReassert(
containerIsAttached: container.window != nil,
expectedSurfaceMatches: container.expectedSurface === surface,
paneIsVisible: isPaneVisible()
) else { return }
Self.pin(surface, in: container)
}
}
Expand Down
7 changes: 6 additions & 1 deletion Glint/Pane/PaneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ struct PaneView: View {
PaneSurfaceRepresentable(
surfaceView: store.surfaceView(workspaceID: workspaceID, paneID: paneID, cwd: cwd),
focused: isFocused,
deferFocus: store.commandPaletteOpen || store.agentChooserIntent != nil
deferFocus: store.commandPaletteOpen || store.agentChooserIntent != nil,
isPaneVisible: {
guard store.selectedWorkspaceID == workspaceID,
let tab = store.selectedWorkspace?.selectedTab else { return false }
return tab.root.leaves.contains(paneID)
}
)
if !isFocused {
// Use a black wash so translucent panes stay translucent; tune
Expand Down
13 changes: 13 additions & 0 deletions GlintTests/PerformanceRegressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,19 @@ final class PerformanceRegressionTests: XCTestCase {
))
}

func testDelayedSurfaceReassertRejectsPaneAfterWorkspaceSwitch() {
XCTAssertTrue(SurfaceReassertionPolicy.shouldReassert(
containerIsAttached: true,
expectedSurfaceMatches: true,
paneIsVisible: true
))
XCTAssertFalse(SurfaceReassertionPolicy.shouldReassert(
containerIsAttached: true,
expectedSurfaceMatches: true,
paneIsVisible: false
))
}

func testBackgroundWorkspaceFirstResponderDoesNotPauseIdleClock() {
XCTAssertTrue(TerminalFocusPolicy.protectsFromIdleOfflining(
appIsActive: true,
Expand Down