Merge pull request #1145 from manaflow-ai/issue-1144-browser-address-bar-disappears

Fix browser omnibar disappearing after Cmd+Shift+Enter
This commit is contained in:
Austin Wang 2026-03-10 13:28:12 -07:00 committed by GitHub
commit 22ae558b6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 608 additions and 2 deletions

View file

@ -6241,6 +6241,80 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
writeGotoSplitTestData(updates)
}
private func recordGotoSplitZoomIfNeeded() {
guard isGotoSplitUITestRecordingEnabled() else { return }
recordGotoSplitZoomRetry(attempt: 0)
}
private func recordGotoSplitZoomRetry(attempt: Int) {
let delays: [Double] = [0.05, 0.1, 0.2, 0.35, 0.5]
let delay = attempt < delays.count ? delays[attempt] : delays.last!
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
guard let self,
let workspace = self.tabManager?.selectedWorkspace else { return }
let browserPanel = workspace.panels.values.compactMap { $0 as? BrowserPanel }.first
let otherTerminal = workspace.panels.values.compactMap { $0 as? TerminalPanel }.first
let browserSnapshot = browserPanel.flatMap {
BrowserWindowPortalRegistry.debugSnapshot(for: $0.webView)
}
var updates = self.gotoSplitFindStateSnapshot(for: workspace)
updates["splitZoomedAfterToggle"] = workspace.bonsplitController.isSplitZoomed ? "true" : "false"
updates["zoomedPaneIdAfterToggle"] = workspace.bonsplitController.zoomedPaneId?.description ?? ""
updates["browserPanelIdAfterToggle"] = browserPanel?.id.uuidString ?? ""
updates["browserContainerHiddenAfterToggle"] = browserSnapshot.map { $0.containerHidden ? "true" : "false" } ?? ""
updates["browserVisibleFlagAfterToggle"] = browserSnapshot.map { $0.visibleInUI ? "true" : "false" } ?? ""
updates["browserFrameAfterToggle"] = browserSnapshot.map {
String(
format: "%.1f,%.1f %.1fx%.1f",
$0.frameInWindow.origin.x,
$0.frameInWindow.origin.y,
$0.frameInWindow.size.width,
$0.frameInWindow.size.height
)
} ?? ""
updates["otherTerminalPanelIdAfterToggle"] = otherTerminal?.id.uuidString ?? ""
updates["otherTerminalHostHiddenAfterToggle"] = otherTerminal.map { $0.hostedView.isHidden ? "true" : "false" } ?? ""
updates["otherTerminalVisibleFlagAfterToggle"] = otherTerminal.map { $0.hostedView.debugPortalVisibleInUI ? "true" : "false" } ?? ""
updates["otherTerminalFrameAfterToggle"] = otherTerminal.map {
let frame = $0.hostedView.debugPortalFrameInWindow
return String(
format: "%.1f,%.1f %.1fx%.1f",
frame.origin.x,
frame.origin.y,
frame.size.width,
frame.size.height
)
} ?? ""
let settled: Bool = {
if workspace.bonsplitController.isSplitZoomed {
if let focusedPanelId = workspace.focusedPanelId,
workspace.terminalPanel(for: focusedPanelId) != nil {
guard let browserSnapshot else { return false }
return browserSnapshot.containerHidden && !browserSnapshot.visibleInUI
}
guard let otherTerminal else { return true }
return otherTerminal.hostedView.isHidden && !otherTerminal.hostedView.debugPortalVisibleInUI
}
let browserRestored = browserSnapshot.map { !$0.containerHidden && $0.visibleInUI } ?? true
let terminalRestored = otherTerminal.map {
!$0.hostedView.isHidden && $0.hostedView.debugPortalVisibleInUI
} ?? true
return browserRestored && terminalRestored
}()
if !settled && attempt < delays.count - 1 {
self.recordGotoSplitZoomRetry(attempt: attempt + 1)
return
}
self.writeGotoSplitTestData(updates)
}
}
private func writeGotoSplitTestData(_ updates: [String: String]) {
guard let path = gotoSplitUITestDataPath() else { return }
var payload = loadGotoSplitTestData(at: path)
@ -7544,6 +7618,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
if matchShortcut(event: event, shortcut: KeyboardShortcutSettings.shortcut(for: .toggleSplitZoom)) {
_ = tabManager?.toggleFocusedSplitZoom()
#if DEBUG
recordGotoSplitZoomIfNeeded()
#endif
return true
}

View file

@ -2856,6 +2856,19 @@ final class WindowBrowserPortal: NSObject {
}
#endif
func debugSnapshot(forWebViewId webViewId: ObjectIdentifier) -> BrowserWindowPortalRegistry.DebugSnapshot? {
guard let entry = entriesByWebViewId[webViewId] else { return nil }
let frameInWindow: CGRect = {
guard let container = entry.containerView, container.window != nil else { return .zero }
return container.convert(container.bounds, to: nil)
}()
return BrowserWindowPortalRegistry.DebugSnapshot(
visibleInUI: entry.visibleInUI,
containerHidden: entry.containerView?.isHidden ?? true,
frameInWindow: frameInWindow
)
}
func webViewAtWindowPoint(_ windowPoint: NSPoint) -> WKWebView? {
guard ensureInstalled() else { return nil }
let point = hostView.convert(windowPoint, from: nil)
@ -2875,6 +2888,12 @@ final class WindowBrowserPortal: NSObject {
@MainActor
enum BrowserWindowPortalRegistry {
struct DebugSnapshot {
let visibleInUI: Bool
let containerHidden: Bool
let frameInWindow: CGRect
}
private static var portalsByWindowId: [ObjectIdentifier: WindowBrowserPortal] = [:]
private static var webViewToWindowId: [ObjectIdentifier: ObjectIdentifier] = [:]
@ -3038,6 +3057,13 @@ enum BrowserWindowPortalRegistry {
portal.forceRefreshWebView(withId: webViewId, reason: reason)
}
static func debugSnapshot(for webView: WKWebView) -> DebugSnapshot? {
let webViewId = ObjectIdentifier(webView)
guard let windowId = webViewToWindowId[webViewId],
let portal = portalsByWindowId[windowId] else { return nil }
return portal.debugSnapshot(forWebViewId: webViewId)
}
#if DEBUG
static func debugPortalCount() -> Int {
portalsByWindowId.count

View file

@ -6382,6 +6382,15 @@ final class GhosttySurfaceScrollView: NSView {
}
}
var debugPortalVisibleInUI: Bool {
surfaceView.isVisibleInUI
}
var debugPortalFrameInWindow: CGRect {
guard window != nil else { return .zero }
return convert(bounds, to: nil)
}
func setActive(_ active: Bool) {
let wasActive = isActive
isActive = active

View file

@ -1720,7 +1720,13 @@ final class BrowserPanel: Panel, ObservableObject {
let inWindow: Bool
let area: CGFloat
}
private struct PortalHostLock {
let hostId: ObjectIdentifier
let paneId: UUID
}
private var activePortalHostLease: PortalHostLease?
private var pendingDistinctPortalHostReplacementPaneId: UUID?
private var lockedPortalHost: PortalHostLock?
private var webViewCancellables = Set<AnyCancellable>()
private var navigationDelegate: BrowserNavigationDelegate?
private var uiDelegate: BrowserUIDelegate?
@ -1773,6 +1779,22 @@ final class BrowserPanel: Panel, ObservableObject {
lease.inWindow && lease.area > portalHostAreaThreshold
}
func preparePortalHostReplacementForNextDistinctClaim(
inPane paneId: PaneID,
reason: String
) {
pendingDistinctPortalHostReplacementPaneId = paneId.id
if lockedPortalHost?.paneId == paneId.id {
lockedPortalHost = nil
}
#if DEBUG
dlog(
"browser.portal.host.rearm panel=\(id.uuidString.prefix(5)) " +
"reason=\(reason) pane=\(paneId.id.uuidString.prefix(5))"
)
#endif
}
func claimPortalHost(
hostId: ObjectIdentifier,
paneId: PaneID,
@ -1788,6 +1810,11 @@ final class BrowserPanel: Panel, ObservableObject {
)
if let current = activePortalHostLease {
if let lock = lockedPortalHost,
(lock.hostId != current.hostId || lock.paneId != current.paneId) {
lockedPortalHost = nil
}
if current.hostId == hostId {
activePortalHostLease = next
return true
@ -1795,12 +1822,47 @@ final class BrowserPanel: Panel, ObservableObject {
let currentUsable = Self.portalHostIsUsable(current)
let nextUsable = Self.portalHostIsUsable(next)
let isSamePaneReplacement = current.paneId == paneId.id
let shouldForceDistinctReplacement =
isSamePaneReplacement &&
pendingDistinctPortalHostReplacementPaneId == paneId.id &&
inWindow
if shouldForceDistinctReplacement {
#if DEBUG
dlog(
"browser.portal.host.claim panel=\(id.uuidString.prefix(5)) " +
"reason=\(reason) host=\(hostId) pane=\(paneId.id.uuidString.prefix(5)) " +
"inWin=\(inWindow ? 1 : 0) size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " +
"replacingHost=\(current.hostId) replacingPane=\(current.paneId.uuidString.prefix(5)) " +
"replacingInWin=\(current.inWindow ? 1 : 0) replacingArea=\(String(format: "%.1f", current.area)) " +
"forced=1"
)
#endif
activePortalHostLease = next
pendingDistinctPortalHostReplacementPaneId = nil
lockedPortalHost = PortalHostLock(hostId: hostId, paneId: paneId.id)
return true
}
let lockBlocksSamePaneReplacement =
isSamePaneReplacement &&
currentUsable &&
lockedPortalHost?.hostId == current.hostId &&
lockedPortalHost?.paneId == current.paneId
let shouldReplace =
current.paneId != paneId.id ||
!currentUsable ||
(nextUsable && next.area > (current.area * Self.portalHostReplacementAreaGainRatio))
(
!lockBlocksSamePaneReplacement &&
nextUsable &&
next.area > (current.area * Self.portalHostReplacementAreaGainRatio)
)
if shouldReplace {
if lockedPortalHost?.hostId == current.hostId &&
lockedPortalHost?.paneId == current.paneId {
lockedPortalHost = nil
}
#if DEBUG
dlog(
"browser.portal.host.claim panel=\(id.uuidString.prefix(5)) " +
@ -1820,7 +1882,8 @@ final class BrowserPanel: Panel, ObservableObject {
"reason=\(reason) host=\(hostId) pane=\(paneId.id.uuidString.prefix(5)) " +
"inWin=\(inWindow ? 1 : 0) size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " +
"ownerHost=\(current.hostId) ownerPane=\(current.paneId.uuidString.prefix(5)) " +
"ownerInWin=\(current.inWindow ? 1 : 0) ownerArea=\(String(format: "%.1f", current.area))"
"ownerInWin=\(current.inWindow ? 1 : 0) ownerArea=\(String(format: "%.1f", current.area)) " +
"locked=\(lockBlocksSamePaneReplacement ? 1 : 0)"
)
#endif
return false
@ -1842,6 +1905,9 @@ final class BrowserPanel: Panel, ObservableObject {
func releasePortalHostIfOwned(hostId: ObjectIdentifier, reason: String) -> Bool {
guard let current = activePortalHostLease, current.hostId == hostId else { return false }
activePortalHostLease = nil
if lockedPortalHost?.hostId == hostId {
lockedPortalHost = nil
}
#if DEBUG
dlog(
"browser.portal.host.release panel=\(id.uuidString.prefix(5)) " +

View file

@ -3247,9 +3247,28 @@ final class Workspace: Identifiable, ObservableObject {
@discardableResult
func toggleSplitZoom(panelId: UUID) -> Bool {
let wasSplitZoomed = bonsplitController.isSplitZoomed
guard let paneId = paneId(forPanelId: panelId) else { return false }
guard bonsplitController.togglePaneZoom(inPane: paneId) else { return false }
focusPanel(panelId)
reconcileTerminalPortalVisibilityForCurrentRenderedLayout()
reconcileBrowserPortalVisibilityForCurrentRenderedLayout(reason: "workspace.toggleSplitZoom")
scheduleTerminalPortalVisibilityReconcileAfterSplitZoom(remainingPasses: 4)
scheduleBrowserPortalVisibilityReconcileAfterSplitZoom(
remainingPasses: 4,
reason: "workspace.toggleSplitZoom"
)
scheduleTerminalGeometryReconcile()
if let browserPanel = browserPanel(for: panelId) {
browserPanel.preparePortalHostReplacementForNextDistinctClaim(
inPane: paneId,
reason: "workspace.toggleSplitZoom"
)
scheduleBrowserPortalReconcileAfterSplitZoom(panelId: panelId, remainingPasses: 4)
if wasSplitZoomed && !bonsplitController.isSplitZoomed {
scheduleBrowserSplitZoomExitFocusReassert(panelId: panelId, remainingPasses: 4)
}
}
return true
}
@ -3512,6 +3531,248 @@ final class Workspace: Identifiable, ObservableObject {
}
}
private func renderedVisiblePanelIdsForCurrentLayout() -> Set<UUID> {
let renderedPaneIds = bonsplitController.zoomedPaneId.map { [$0] } ?? bonsplitController.allPaneIds
var visiblePanelIds: Set<UUID> = []
for paneId in renderedPaneIds {
let selectedTab = bonsplitController.selectedTab(inPane: paneId) ?? bonsplitController.tabs(inPane: paneId).first
guard let selectedTab,
let panelId = panelIdFromSurfaceId(selectedTab.id),
panels[panelId] != nil else {
continue
}
visiblePanelIds.insert(panelId)
}
if let focusedPanelId,
panels[focusedPanelId] != nil,
let focusedPaneId = paneId(forPanelId: focusedPanelId),
renderedPaneIds.contains(where: { $0.id == focusedPaneId.id }) {
visiblePanelIds.insert(focusedPanelId)
}
return visiblePanelIds
}
private func reconcileTerminalPortalVisibilityForCurrentRenderedLayout() {
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
for panel in panels.values {
guard let terminalPanel = panel as? TerminalPanel else { continue }
let shouldBeVisible = visiblePanelIds.contains(terminalPanel.id)
terminalPanel.hostedView.setVisibleInUI(shouldBeVisible)
terminalPanel.hostedView.setActive(shouldBeVisible && focusedPanelId == terminalPanel.id)
TerminalWindowPortalRegistry.updateEntryVisibility(
for: terminalPanel.hostedView,
visibleInUI: shouldBeVisible
)
}
}
private func terminalPortalVisibilityNeedsFollowUp() -> Bool {
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
for panel in panels.values {
guard let terminalPanel = panel as? TerminalPanel else { continue }
let shouldBeVisible = visiblePanelIds.contains(terminalPanel.id)
let hostedView = terminalPanel.hostedView
if shouldBeVisible {
if hostedView.isHidden || hostedView.window == nil || hostedView.superview == nil {
return true
}
} else if !hostedView.isHidden {
return true
}
}
return false
}
private func scheduleTerminalPortalVisibilityReconcileAfterSplitZoom(remainingPasses: Int) {
guard remainingPasses > 0 else { return }
DispatchQueue.main.async { [weak self] in
guard let self else { return }
for window in NSApp.windows {
window.contentView?.layoutSubtreeIfNeeded()
window.contentView?.displayIfNeeded()
}
self.reconcileTerminalPortalVisibilityForCurrentRenderedLayout()
if self.terminalPortalVisibilityNeedsFollowUp(), remainingPasses > 1 {
self.scheduleTerminalPortalVisibilityReconcileAfterSplitZoom(
remainingPasses: remainingPasses - 1
)
}
}
}
private func reconcileBrowserPortalVisibilityForCurrentRenderedLayout(reason: String) {
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
for panel in panels.values {
guard let browserPanel = panel as? BrowserPanel else { continue }
let shouldBeVisible = visiblePanelIds.contains(browserPanel.id)
if shouldBeVisible {
BrowserWindowPortalRegistry.updateEntryVisibility(
for: browserPanel.webView,
visibleInUI: true,
zPriority: 2
)
let anchorView = browserPanel.portalAnchorView
let anchorReady =
anchorView.window != nil &&
anchorView.superview != nil &&
anchorView.bounds.width > 1 &&
anchorView.bounds.height > 1
if anchorReady {
BrowserWindowPortalRegistry.synchronizeForAnchor(anchorView)
BrowserWindowPortalRegistry.refresh(
webView: browserPanel.webView,
reason: reason
)
}
} else {
BrowserWindowPortalRegistry.updateEntryVisibility(
for: browserPanel.webView,
visibleInUI: false,
zPriority: 0
)
BrowserWindowPortalRegistry.hide(
webView: browserPanel.webView,
source: reason
)
}
}
}
private func browserPortalVisibilityNeedsFollowUp() -> Bool {
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
for panel in panels.values {
guard let browserPanel = panel as? BrowserPanel else { continue }
guard visiblePanelIds.contains(browserPanel.id) else { continue }
let anchorView = browserPanel.portalAnchorView
let anchorReady =
anchorView.window != nil &&
anchorView.superview != nil &&
anchorView.bounds.width > 1 &&
anchorView.bounds.height > 1
if !anchorReady ||
browserPanel.webView.window == nil ||
browserPanel.webView.superview == nil ||
!BrowserWindowPortalRegistry.isWebView(browserPanel.webView, boundTo: anchorView) {
return true
}
}
return false
}
private func scheduleBrowserPortalVisibilityReconcileAfterSplitZoom(
remainingPasses: Int,
reason: String
) {
guard remainingPasses > 0 else { return }
DispatchQueue.main.async { [weak self] in
guard let self else { return }
for window in NSApp.windows {
window.contentView?.layoutSubtreeIfNeeded()
window.contentView?.displayIfNeeded()
}
self.reconcileBrowserPortalVisibilityForCurrentRenderedLayout(reason: reason)
if self.browserPortalVisibilityNeedsFollowUp(), remainingPasses > 1 {
self.scheduleBrowserPortalVisibilityReconcileAfterSplitZoom(
remainingPasses: remainingPasses - 1,
reason: reason
)
}
}
}
// Browser panes host WKWebView in the window portal. After pane zoom toggles,
// force a few post-layout sync passes so the portal does not outlive the omnibar chrome.
private func scheduleBrowserPortalReconcileAfterSplitZoom(panelId: UUID, remainingPasses: Int) {
guard remainingPasses > 0 else { return }
DispatchQueue.main.async { [weak self] in
guard let self, let browserPanel = self.browserPanel(for: panelId) else { return }
for window in NSApp.windows {
window.contentView?.layoutSubtreeIfNeeded()
window.contentView?.displayIfNeeded()
}
let anchorView = browserPanel.portalAnchorView
let anchorReady =
anchorView.window != nil &&
anchorView.superview != nil &&
anchorView.bounds.width > 1 &&
anchorView.bounds.height > 1
if anchorReady {
BrowserWindowPortalRegistry.synchronizeForAnchor(anchorView)
BrowserWindowPortalRegistry.refresh(
webView: browserPanel.webView,
reason: "workspace.toggleSplitZoom"
)
}
let portalNeedsFollowUpPass =
!anchorReady ||
browserPanel.webView.window == nil ||
browserPanel.webView.superview == nil
if portalNeedsFollowUpPass {
self.scheduleBrowserPortalReconcileAfterSplitZoom(
panelId: panelId,
remainingPasses: remainingPasses - 1
)
}
}
}
// Browser panes can briefly keep the portal-hosted WKWebView visible while Bonsplit is
// still rebuilding the unzoomed pane host. Reassert pane/tab selection after layout settles
// so the SwiftUI chrome does not remain hidden until another browser focus command runs.
private func scheduleBrowserSplitZoomExitFocusReassert(panelId: UUID, remainingPasses: Int) {
guard remainingPasses > 0 else { return }
DispatchQueue.main.async { [weak self] in
guard let self, self.browserPanel(for: panelId) != nil else { return }
guard let paneId = self.paneId(forPanelId: panelId),
let tabId = self.surfaceIdFromPanelId(panelId) else { return }
let selectionConverged =
self.bonsplitController.focusedPaneId == paneId &&
self.bonsplitController.selectedTab(inPane: paneId)?.id == tabId
let anchorReady: Bool = {
guard let browserPanel = self.browserPanel(for: panelId) else { return false }
let anchorView = browserPanel.portalAnchorView
return
anchorView.window != nil &&
anchorView.superview != nil &&
anchorView.bounds.width > 1 &&
anchorView.bounds.height > 1
}()
if !selectionConverged {
self.focusPanel(panelId)
self.scheduleFocusReconcile()
}
if !selectionConverged || !anchorReady {
self.scheduleBrowserSplitZoomExitFocusReassert(
panelId: panelId,
remainingPasses: remainingPasses - 1
)
}
}
}
private func scheduleMovedTerminalRefresh(panelId: UUID) {
guard terminalPanel(for: panelId) != nil else { return }

View file

@ -106,6 +106,10 @@ struct WorkspaceContentView: View {
workspace.bonsplitController.focusPane(paneId)
}
}
// Split zoom swaps Bonsplit between the full split tree and a single pane view.
// Recreate the Bonsplit subtree on zoom enter/exit so stale pre-zoom pane chrome
// cannot remain stacked above portal-hosted browser content.
.id(splitZoomRenderIdentity)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
syncBonsplitNotificationBadges()
@ -174,6 +178,10 @@ struct WorkspaceContentView: View {
}
}
private var splitZoomRenderIdentity: String {
workspace.bonsplitController.zoomedPaneId.map { "zoom:\($0.id.uuidString)" } ?? "unzoomed"
}
static func resolveGhosttyAppearanceConfig(
reason: String = "unspecified",
backgroundOverride: NSColor? = nil,

View file

@ -554,6 +554,150 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
)
}
func testCmdShiftEnterKeepsBrowserOmnibarHittableAcrossZoomRoundTripWhenWebViewFocused() {
let app = XCUIApplication()
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
launchAndEnsureForeground(app)
XCTAssertTrue(
waitForData(keys: ["browserPanelId", "webViewFocused"], timeout: 10.0),
"Expected goto_split setup data to be written"
)
guard let setup = loadData() else {
XCTFail("Missing goto_split setup data")
return
}
guard let browserPanelId = setup["browserPanelId"] else {
XCTFail("Missing browserPanelId in goto_split setup data")
return
}
XCTAssertEqual(setup["webViewFocused"], "true", "Expected WKWebView to be first responder for this test")
let omnibar = app.textFields["BrowserOmnibarTextField"].firstMatch
let pill = app.descendants(matching: .any).matching(identifier: "BrowserOmnibarPill").firstMatch
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0), "Expected browser omnibar text field before zoom")
XCTAssertTrue(pill.waitForExistence(timeout: 6.0), "Expected browser omnibar pill before zoom")
// Reproduce the loaded-page state from the bug report before toggling zoom.
app.typeKey("l", modifierFlags: [.command])
XCTAssertTrue(waitForElementToBecomeHittable(pill, timeout: 6.0), "Expected browser omnibar pill before navigation")
pill.click()
app.typeKey("a", modifierFlags: [.command])
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
app.typeText(zoomRoundTripPageURL)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [])
XCTAssertTrue(
waitForOmnibarToContain(omnibar, value: "data:text/html", timeout: 8.0),
"Expected browser to finish navigating to the regression page before zoom. value=\(String(describing: omnibar.value))"
)
let browserPane = app.otherElements["BrowserPanelContent.\(browserPanelId)"].firstMatch
XCTAssertTrue(browserPane.waitForExistence(timeout: 6.0), "Expected browser pane content before zoom")
browserPane.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).click()
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
XCTAssertTrue(
waitForDataMatch(timeout: 8.0) { data in
data["splitZoomedAfterToggle"] == "true" &&
data["otherTerminalHostHiddenAfterToggle"] == "true" &&
data["otherTerminalVisibleFlagAfterToggle"] == "false"
},
"Expected Cmd+Shift+Enter zoom-in to hide the non-browser terminal portal. data=\(loadData() ?? [:])"
)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
XCTAssertTrue(
waitForDataMatch(timeout: 8.0) { data in
data["splitZoomedAfterToggle"] == "false" &&
data["otherTerminalHostHiddenAfterToggle"] == "false" &&
data["otherTerminalVisibleFlagAfterToggle"] == "true"
},
"Expected Cmd+Shift+Enter zoom-out to restore the non-browser terminal portal. data=\(loadData() ?? [:])"
)
XCTAssertTrue(omnibar.waitForExistence(timeout: 6.0), "Expected browser omnibar text field after Cmd+Shift+Enter zoom round-trip")
XCTAssertTrue(pill.waitForExistence(timeout: 6.0), "Expected browser omnibar pill after Cmd+Shift+Enter zoom round-trip")
XCTAssertTrue(
waitForElementToBecomeHittable(pill, timeout: 6.0),
"Expected browser omnibar to stay hittable after Cmd+Shift+Enter zoom round-trip"
)
let page = app.webViews.firstMatch
XCTAssertTrue(page.waitForExistence(timeout: 6.0), "Expected browser web area after Cmd+Shift+Enter")
XCTAssertLessThanOrEqual(
pill.frame.maxY,
page.frame.minY + 12,
"Expected browser omnibar to remain above the web content after Cmd+Shift+Enter. pill=\(pill.frame) page=\(page.frame)"
)
pill.click()
app.typeKey("a", modifierFlags: [.command])
app.typeKey(XCUIKeyboardKey.delete.rawValue, modifierFlags: [])
app.typeText("issue1144")
XCTAssertTrue(
waitForOmnibarToContain(omnibar, value: "issue1144", timeout: 4.0),
"Expected browser omnibar to stay editable after Cmd+Shift+Enter. value=\(String(describing: omnibar.value))"
)
}
func testCmdShiftEnterHidesBrowserPortalWhenTerminalPaneZooms() {
let app = XCUIApplication()
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_SETUP"] = "1"
app.launchEnvironment["CMUX_UI_TEST_GOTO_SPLIT_PATH"] = dataPath
app.launchEnvironment["CMUX_UI_TEST_FOCUS_SHORTCUTS"] = "1"
launchAndEnsureForeground(app)
XCTAssertTrue(
waitForData(keys: ["terminalPaneId", "browserPanelId", "webViewFocused"], timeout: 10.0),
"Expected goto_split setup data to be written"
)
guard let setup = loadData() else {
XCTFail("Missing goto_split setup data")
return
}
guard let expectedTerminalPaneId = setup["terminalPaneId"] else {
XCTFail("Missing terminalPaneId in goto_split setup data")
return
}
app.typeKey("h", modifierFlags: [.command, .control])
XCTAssertTrue(
waitForDataMatch(timeout: 5.0) { data in
data["focusedPaneId"] == expectedTerminalPaneId && data["focusedPanelKind"] == "terminal"
},
"Expected Cmd+Ctrl+H to focus the terminal pane before zoom. data=\(loadData() ?? [:])"
)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
XCTAssertTrue(
waitForDataMatch(timeout: 8.0) { data in
data["splitZoomedAfterToggle"] == "true" &&
data["browserContainerHiddenAfterToggle"] == "true" &&
data["browserVisibleFlagAfterToggle"] == "false"
},
"Expected Cmd+Shift+Enter zoom-in on the terminal pane to hide the browser portal. data=\(loadData() ?? [:])"
)
app.typeKey(XCUIKeyboardKey.return.rawValue, modifierFlags: [.command, .shift])
XCTAssertTrue(
waitForDataMatch(timeout: 8.0) { data in
data["splitZoomedAfterToggle"] == "false" &&
data["browserContainerHiddenAfterToggle"] == "false" &&
data["browserVisibleFlagAfterToggle"] == "true"
},
"Expected Cmd+Shift+Enter zoom-out from the terminal pane to restore the browser portal. data=\(loadData() ?? [:])"
)
}
func testCmdDSplitsRightWhenOmnibarFocused() {
let app = XCUIApplication()
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
@ -806,10 +950,25 @@ final class BrowserPaneNavigationKeybindUITests: XCTestCase {
return value.contains(expectedSubstring)
}
private func waitForElementToBecomeHittable(_ element: XCUIElement, timeout: TimeInterval) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if element.exists && element.isHittable {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
}
return element.exists && element.isHittable
}
private var autofocusRacePageURL: String {
"data:text/html,%3Cinput%20id%3D%22q%22%3E%3Cscript%3EsetTimeout%28function%28%29%7Bdocument.getElementById%28%22q%22%29.focus%28%29%3Blocation.hash%3D%22focused%22%3B%7D%2C700%29%3B%3C%2Fscript%3E"
}
private var zoomRoundTripPageURL: String {
"data:text/html,%3Ctitle%3EIssue%201144%3C/title%3E%3Cbody%20style%3D%22margin:0;background:%231d1f24;color:white;font-family:system-ui;height:2200px%22%3E%3Cmain%20style%3D%22padding:32px%22%3E%3Ch1%3EIssue%201144%20Regression%20Page%3C/h1%3E%3Cp%3EZoom%20should%20not%20leave%20stale%20split%20chrome%20above%20the%20browser%20omnibar.%3C/p%3E%3C/main%3E%3C/body%3E"
}
private func launchAndEnsureForeground(_ app: XCUIApplication, timeout: TimeInterval = 12.0) {
app.launch()
XCTAssertTrue(