From a9655c20e65b08c735edcad6d67de5edb5e0050c Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 22 Mar 2026 20:19:49 -0700 Subject: [PATCH 1/2] test: cover queued restore-time terminal portal shift --- cmuxTests/TerminalAndGhosttyTests.swift | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index 24ec48a63b..60a2292beb 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -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)) + + 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), From 296e25d4511757a64cc3812a032c8f93be536495 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 22 Mar 2026 20:19:52 -0700 Subject: [PATCH 2/2] fix: resync terminal portal after restore-time bind --- Sources/TerminalWindowPortal.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index 4c4588a8a2..506e192f92 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -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() }