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
4 changes: 4 additions & 0 deletions Sources/TerminalWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,10 @@ final class WindowTerminalPortal: NSObject {

synchronizeHostedView(withId: hostedId)
scheduleDeferredFullSynchronizeAll()
// Session/window restore can queue additional ancestor layout shifts (sidebar width,
// split positions) after the initial bind tick. Queue a later external sync so the
// portal catches that settled geometry instead of staying at the seeded frame.
scheduleExternalGeometrySynchronize()
pruneDeadEntries()
}

Expand Down
77 changes: 77 additions & 0 deletions cmuxTests/TerminalAndGhosttyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2939,6 +2939,83 @@ final class TerminalWindowPortalLifecycleTests: XCTestCase {
)
}

func testBindQueuesExternalGeometrySyncForQueuedLayoutShift() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 420),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer {
NotificationCenter.default.post(name: NSWindow.willCloseNotification, object: window)
window.orderOut(nil)
}

let surface = TerminalSurface(
tabId: UUID(),
context: GHOSTTY_SURFACE_CONTEXT_SPLIT,
configTemplate: nil,
workingDirectory: nil
)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}

let shiftedContainer = NSView(frame: NSRect(x: 40, y: 60, width: 260, height: 180))
contentView.addSubview(shiftedContainer)
let anchor = NSView(frame: NSRect(x: 0, y: 0, width: 260, height: 180))
shiftedContainer.addSubview(anchor)
let hosted = surface.hostedView
TerminalWindowPortalRegistry.bind(
hostedView: hosted,
to: anchor,
visibleInUI: true,
expectedSurfaceId: surface.id,
expectedGeneration: surface.portalBindingGeneration()
)
TerminalWindowPortalRegistry.synchronizeForAnchor(anchor)

let anchorCenter = NSPoint(x: anchor.bounds.midX, y: anchor.bounds.midY)
let originalWindowPoint = anchor.convert(anchorCenter, to: nil)
let originalAnchorFrameInWindow = anchor.convert(anchor.bounds, to: nil)
XCTAssertNotNil(
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(originalWindowPoint, in: window),
"Initial hit-testing should resolve the portal-hosted terminal at its original window position"
)

DispatchQueue.main.async {
shiftedContainer.frame.origin.x += 72
contentView.layoutSubtreeIfNeeded()
window.displayIfNeeded()
}

RunLoop.current.run(until: Date().addingTimeInterval(0.05))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Time-based wait may be flaky under CI load

RunLoop.current.run(until: Date().addingTimeInterval(0.05)) has to drain three async hops in 50 ms (outer block from scheduleExternalGeometrySynchronize → inner performSync block → the queued layout-shift block). On a heavily loaded CI runner the budget can be tight.

The pre-existing testScheduledExternalGeometrySyncWaitsForQueuedLayoutShift uses the same 50 ms pattern and is presumably passing, so this is not a new risk — but if either test ever becomes intermittent, the fix is two sequential drainMainQueue() calls instead of the wall-clock sleep:

// instead of RunLoop.current.run(until: Date().addingTimeInterval(0.05))
drainMainQueue()  // fires hop-1 → queues hop-2; layout-shift block also dispatched by now
drainMainQueue()  // fires hop-2 (performSync) — portal is now at settled geometry

No action required unless CI flakiness is observed.


let shiftedAnchorFrameInWindow = anchor.convert(anchor.bounds, to: nil)
XCTAssertGreaterThan(
shiftedAnchorFrameInWindow.minX,
originalAnchorFrameInWindow.minX + 1,
"The queued layout shift should move the anchor to the right"
)
let retiredStaleWindowPoint = NSPoint(
x: (originalAnchorFrameInWindow.minX + shiftedAnchorFrameInWindow.minX) / 2,
y: shiftedAnchorFrameInWindow.midY
)
let shiftedWindowPoint = NSPoint(
x: (originalAnchorFrameInWindow.maxX + shiftedAnchorFrameInWindow.maxX) / 2,
y: shiftedAnchorFrameInWindow.midY
)
XCTAssertNil(
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(retiredStaleWindowPoint, in: window),
"Bind should queue a later external sync so restore-like ancestor shifts do not leave a stale portal in the sidebar region"
)
XCTAssertNotNil(
TerminalWindowPortalRegistry.terminalViewAtWindowPoint(shiftedWindowPoint, in: window),
"Bind should refresh the portal after queued ancestor layout settles"
)
}

func testScheduledExternalGeometrySyncKeepsDragDrivenResizeResponsive() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 420),
Expand Down
Loading