Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue-1208-browser-pane-stale-content

This commit is contained in:
austinpower1258 2026-03-11 18:54:12 -07:00
commit 1dbd1e5011
4 changed files with 323 additions and 23 deletions

View file

@ -8035,10 +8035,12 @@ struct CMUXCLI {
private func printWelcome() {
let reset = "\u{001B}[0m"
let bold = "\u{001B}[1m"
let dim = "\u{001B}[2m"
func trueColor(_ red: Int, _ green: Int, _ blue: Int) -> String {
"\u{001B}[38;2;\(red);\(green);\(blue)m"
}
let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
let c1 = trueColor(0, 212, 255)
let c2 = trueColor(24, 181, 250)
let c3 = trueColor(48, 150, 245)
@ -8046,7 +8048,17 @@ struct CMUXCLI {
let c5 = trueColor(96, 88, 239)
let c6 = trueColor(110, 73, 238)
let c7 = trueColor(124, 58, 237)
let tagline = trueColor(130, 130, 140)
let tagline: String
let subdued: String
if isDark {
tagline = trueColor(130, 130, 140)
subdued = "\u{001B}[2m"
} else {
tagline = trueColor(90, 90, 98)
subdued = trueColor(100, 100, 108)
}
let logo = """
\(c1) ::\(reset)
@ -8061,14 +8073,14 @@ struct CMUXCLI {
let shortcuts = """
\(bold)Shortcuts\(reset)
\(bold)\u{2318}N\(reset)\(dim) New workspace\(reset)
\(bold)\u{2318}P\(reset)\(dim) Go to workspace\(reset)
\(bold)\u{2318}D\(reset)\(dim) Split right\(reset)
\(bold)\u{2318}\u{21E7}D\(reset)\(dim) Split down\(reset)
\(bold)\u{2318}\u{21E7}P\(reset)\(dim) Command palette\(reset)
\(bold)\u{2318}\u{21E7}R\(reset)\(dim) Rename workspace\(reset)
\(bold)\u{2318}\u{21E7}L\(reset)\(dim) New browser\(reset)
\(bold)\u{2318}\u{21E7}U\(reset)\(dim) Jump to latest unread\(reset)
\(bold)\u{2318}N\(reset)\(subdued) New workspace\(reset)
\(bold)\u{2318}P\(reset)\(subdued) Go to workspace\(reset)
\(bold)\u{2318}D\(reset)\(subdued) Split right\(reset)
\(bold)\u{2318}\u{21E7}D\(reset)\(subdued) Split down\(reset)
\(bold)\u{2318}\u{21E7}P\(reset)\(subdued) Command palette\(reset)
\(bold)\u{2318}\u{21E7}R\(reset)\(subdued) Rename workspace\(reset)
\(bold)\u{2318}\u{21E7}L\(reset)\(subdued) New browser\(reset)
\(bold)\u{2318}\u{21E7}U\(reset)\(subdued) Jump to latest unread\(reset)
"""
print()
@ -8076,14 +8088,14 @@ struct CMUXCLI {
print()
print(shortcuts)
print()
print(" \(bold)Docs\(reset)\(dim) https://cmux.dev/docs\(reset)")
print(" \(bold)Discord\(reset)\(dim) https://discord.gg/xsgFEVrWCZ\(reset)")
print(" \(bold)GitHub\(reset)\(dim) https://github.com/manaflow-ai/cmux (please leave a star ⭐)\(reset)")
print(" \(bold)Email\(reset)\(dim) founders@manaflow.com\(reset)")
print(" \(bold)Docs\(reset)\(subdued) https://cmux.dev/docs\(reset)")
print(" \(bold)Discord\(reset)\(subdued) https://discord.gg/xsgFEVrWCZ\(reset)")
print(" \(bold)GitHub\(reset)\(subdued) https://github.com/manaflow-ai/cmux (please leave a star ⭐)\(reset)")
print(" \(bold)Email\(reset)\(subdued) founders@manaflow.com\(reset)")
print()
print(" \(dim)Run \(reset)\(bold)cmux --help\(reset)\(dim) for all commands.\(reset)")
print(" \(dim)Run \(reset)\(bold)cmux shortcuts\(reset)\(dim) to edit shortcuts.\(reset)")
print(" \(dim)Run \(reset)\(bold)cmux feedback\(reset)\(dim) to report a bug.\(reset)")
print(" \(subdued)Run \(reset)\(bold)cmux --help\(reset)\(subdued) for all commands.\(reset)")
print(" \(subdued)Run \(reset)\(bold)cmux shortcuts\(reset)\(subdued) to edit shortcuts.\(reset)")
print(" \(subdued)Run \(reset)\(bold)cmux feedback\(reset)\(subdued) to report a bug.\(reset)")
print()
}

View file

@ -2259,6 +2259,71 @@ final class WindowBrowserPortal: NSObject {
frame.maxY > bounds.maxY + epsilon
}
private static func hasVisibleInspectorDescendant(in root: NSView) -> Bool {
var stack: [NSView] = [root]
while let current = stack.popLast() {
if current !== root {
let className = String(describing: type(of: current))
if className.contains("WKInspector"),
!current.isHidden,
current.alphaValue > 0,
current.frame.width > 1,
current.frame.height > 1 {
return true
}
}
stack.append(contentsOf: current.subviews)
}
return false
}
private static func inferredBottomDockedInspectorFrame(
in containerView: NSView,
primaryWebView: WKWebView,
epsilon: CGFloat = 1
) -> NSRect? {
let pageFrame = primaryWebView.frame
let containerBounds = containerView.bounds
let candidates = containerView.subviews.compactMap { candidate -> NSRect? in
guard candidate !== primaryWebView else { return nil }
guard hasVisibleInspectorDescendant(in: candidate) else { return nil }
let frame = candidate.frame
guard frame.width > 1, frame.height > 1 else { return nil }
let overlapWidth = min(pageFrame.maxX, frame.maxX) - max(pageFrame.minX, frame.minX)
guard overlapWidth > min(pageFrame.width, frame.width) * 0.7 else { return nil }
guard frame.minY <= containerBounds.minY + epsilon else { return nil }
guard frame.maxY <= pageFrame.minY + epsilon else { return nil }
return frame
}
return candidates.max(by: { $0.height < $1.height })
}
private static func repairedBottomDockedPageFrame(
in containerView: NSView,
primaryWebView: WKWebView,
epsilon: CGFloat = 0.5
) -> NSRect? {
let pageFrame = primaryWebView.frame
let containerBounds = containerView.bounds
guard frameExtendsOutsideBounds(pageFrame, bounds: containerBounds, epsilon: epsilon),
let inspectorFrame = inferredBottomDockedInspectorFrame(
in: containerView,
primaryWebView: primaryWebView
) else {
return nil
}
return NSRect(
x: containerBounds.minX,
y: inspectorFrame.maxY,
width: containerBounds.width,
height: max(0, containerBounds.maxY - inspectorFrame.maxY)
)
}
#if DEBUG
private static func inspectorSubviewCount(in root: NSView) -> Int {
var stack: [NSView] = [root]
@ -3111,7 +3176,30 @@ final class WindowBrowserPortal: NSObject {
#if DEBUG
let inspectorSubviews = Self.inspectorSubviewCount(in: containerView)
#endif
if containerOwnsWebView && Self.frameExtendsOutsideBounds(preNormalizeWebFrame, bounds: containerBounds) {
if containerOwnsWebView,
let repairedBottomDockFrame = Self.repairedBottomDockedPageFrame(
in: containerView,
primaryWebView: webView
) {
let oldWebFrame = preNormalizeWebFrame
CATransaction.begin()
CATransaction.setDisableActions(true)
webView.frame = repairedBottomDockFrame
CATransaction.commit()
#if DEBUG
dlog(
"browser.portal.webframe.bottomDockRepair web=\(browserPortalDebugToken(webView)) " +
"container=\(browserPortalDebugToken(containerView)) old=\(browserPortalDebugFrame(oldWebFrame)) " +
"new=\(browserPortalDebugFrame(repairedBottomDockFrame)) bounds=\(browserPortalDebugFrame(containerBounds)) " +
"inspectorHApprox=\(String(format: "%.1f", inspectorHeightApprox)) " +
"inspectorInsets=\(String(format: "%.1f", inspectorHeightFromInsets)) " +
"inspectorOverflow=\(String(format: "%.1f", inspectorHeightFromOverflow)) " +
"inspectorSubviews=\(inspectorSubviews) " +
"source=\(source)"
)
#endif
refreshReasons.append("webFrameBottomDock")
} else if containerOwnsWebView && Self.frameExtendsOutsideBounds(preNormalizeWebFrame, bounds: containerBounds) {
let oldWebFrame = preNormalizeWebFrame
CATransaction.begin()
CATransaction.setDisableActions(true)

View file

@ -4992,6 +4992,8 @@ struct WebViewRepresentable: NSViewRepresentable {
return true
}
guard !relatedSubviews.isEmpty else { return }
let preserveSlotLocalFrames = sourceSuperview is WindowBrowserSlotView
let sourceSlotBoundsSize = sourceSuperview.bounds.size
#if DEBUG
dlog(
"browser.localHost.reparent.batch reason=\(reason) source=\(Self.objectID(sourceSuperview)) " +
@ -5000,11 +5002,17 @@ struct WebViewRepresentable: NSViewRepresentable {
)
#endif
for view in relatedSubviews {
let frameInWindow = sourceSuperview.convert(view.frame, to: nil)
let className = String(describing: type(of: view))
let targetFrame: NSRect
if preserveSlotLocalFrames {
targetFrame = view.frame
} else {
let frameInWindow = sourceSuperview.convert(view.frame, to: nil)
targetFrame = container.convert(frameInWindow, from: nil)
}
view.removeFromSuperview()
container.addSubview(view, positioned: .above, relativeTo: nil)
view.frame = container.convert(frameInWindow, from: nil)
view.frame = targetFrame
#if DEBUG
dlog(
"browser.localHost.reparent.batch.item reason=\(reason) class=\(className) " +
@ -5012,6 +5020,11 @@ struct WebViewRepresentable: NSViewRepresentable {
)
#endif
}
if preserveSlotLocalFrames, sourceSlotBoundsSize != container.bounds.size {
container.resizeSubviews(withOldSize: sourceSlotBoundsSize)
container.needsLayout = true
container.layoutSubtreeIfNeeded()
}
}
private static func installPortalAnchorView(_ anchorView: NSView, in host: NSView) {

View file

@ -2565,6 +2565,18 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
return nil
}
private func findWindowBrowserSlotView(in root: NSView) -> WindowBrowserSlotView? {
if let slot = root as? WindowBrowserSlotView {
return slot
}
for subview in root.subviews {
if let slot = findWindowBrowserSlotView(in: subview) {
return slot
}
}
return nil
}
func testRestoreReopensInspectorAfterAttachWhenPreferredVisible() {
let (panel, inspector) = makePanelWithInspector()
@ -2858,6 +2870,106 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
"An off-window replacement host should leave DevTools companion views in the visible local host"
)
}
func testVisibleReplacementLocalHostNormalizesBottomDockedInspectorFrames() {
let (panel, _) = makePanelWithInspector()
XCTAssertTrue(panel.showDeveloperTools())
let paneId = PaneID(id: UUID())
let representable = WebViewRepresentable(
panel: panel,
paneId: paneId,
shouldAttachWebView: false,
useLocalInlineHosting: true,
shouldFocusWebView: false,
isPanelFocused: true,
portalZPriority: 0,
paneDropZone: nil,
searchOverlay: nil,
paneTopChromeHeight: 0
)
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 360, height: 240),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let narrowHosting = NSHostingView(rootView: representable)
narrowHosting.frame = NSRect(x: 180, y: 0, width: 180, height: 240)
contentView.addSubview(narrowHosting)
window.makeKeyAndOrderFront(nil)
window.displayIfNeeded()
contentView.layoutSubtreeIfNeeded()
narrowHosting.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
guard let initialSlot = panel.webView.superview as? WindowBrowserSlotView else {
XCTFail("Expected initial local inline slot")
return
}
let inspectorView = WKInspectorProbeView(
frame: NSRect(x: 0, y: 0, width: initialSlot.bounds.width, height: 72)
)
inspectorView.autoresizingMask = [.width]
initialSlot.addSubview(inspectorView)
panel.webView.frame = NSRect(
x: 0,
y: inspectorView.frame.maxY,
width: initialSlot.bounds.width,
height: initialSlot.bounds.height - inspectorView.frame.height
)
initialSlot.layoutSubtreeIfNeeded()
let replacementHosting = NSHostingView(rootView: representable)
replacementHosting.frame = contentView.bounds
replacementHosting.autoresizingMask = [.width, .height]
contentView.addSubview(replacementHosting, positioned: .above, relativeTo: narrowHosting)
contentView.layoutSubtreeIfNeeded()
replacementHosting.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
replacementHosting.rootView = representable
contentView.layoutSubtreeIfNeeded()
replacementHosting.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
narrowHosting.removeFromSuperview()
contentView.layoutSubtreeIfNeeded()
replacementHosting.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
guard let replacementHost = findHostContainerView(in: replacementHosting),
let replacementSlot = findWindowBrowserSlotView(in: replacementHost) else {
XCTFail("Expected replacement local inline host")
return
}
XCTAssertTrue(
panel.webView.superview === replacementSlot,
"A visible replacement local host should take over the hosted page"
)
XCTAssertTrue(
inspectorView.superview === replacementSlot,
"A visible replacement local host should move the DevTools companion views with the page"
)
XCTAssertEqual(inspectorView.frame.minX, 0, accuracy: 0.5)
XCTAssertEqual(inspectorView.frame.minY, 0, accuracy: 0.5)
XCTAssertEqual(inspectorView.frame.width, replacementSlot.bounds.width, accuracy: 0.5)
XCTAssertEqual(inspectorView.frame.height, 72, accuracy: 0.5)
XCTAssertEqual(panel.webView.frame.minX, 0, accuracy: 0.5)
XCTAssertEqual(panel.webView.frame.minY, 72, accuracy: 0.5)
XCTAssertEqual(panel.webView.frame.width, replacementSlot.bounds.width, accuracy: 0.5)
XCTAssertEqual(panel.webView.frame.height, replacementSlot.bounds.height - 72, accuracy: 0.5)
}
}
final class WorkspaceShortcutMapperTests: XCTestCase {
@ -11618,6 +11730,76 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
)
}
func testPortalSyncRepairsBottomDockedInspectorOverflowedPageFrame() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
let portal = WindowBrowserPortal(window: window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
}
let anchor = NSView(frame: NSRect(x: 40, y: 24, width: 260, height: 180))
contentView.addSubview(anchor)
let webView = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
portal.bind(webView: webView, to: anchor, visibleInUI: true)
contentView.layoutSubtreeIfNeeded()
portal.synchronizeWebViewForAnchor(anchor)
guard let slot = webView.superview as? WindowBrowserSlotView else {
XCTFail("Expected browser slot")
return
}
let inspectorHeight: CGFloat = 84
let inspectorContainer = NSView(
frame: NSRect(x: 0, y: 0, width: slot.bounds.width, height: inspectorHeight)
)
inspectorContainer.autoresizingMask = [.width]
let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds)
inspectorView.autoresizingMask = [.width, .height]
inspectorContainer.addSubview(inspectorView)
slot.addSubview(inspectorContainer)
webView.frame = NSRect(
x: 0,
y: inspectorHeight,
width: slot.bounds.width,
height: slot.bounds.height
)
webView.autoresizingMask = [.width, .height]
slot.layoutSubtreeIfNeeded()
portal.synchronizeWebViewForAnchor(anchor)
XCTAssertFalse(slot.isHidden, "Portal sync should keep the hosted browser visible")
XCTAssertEqual(
webView.frame.minY,
inspectorHeight,
accuracy: 0.5,
"Portal sync should keep the page viewport below a bottom-docked inspector instead of shifting the page upward"
)
XCTAssertEqual(
webView.frame.height,
slot.bounds.height - inspectorHeight,
accuracy: 0.5,
"Portal sync should shrink the page viewport to the space above a bottom-docked inspector"
)
XCTAssertEqual(
webView.frame.maxY,
slot.bounds.maxY,
accuracy: 0.5,
"The repaired page viewport should stay flush with the top edge of the slot"
)
}
func testHidingBrowserSlotYieldsOwnedInspectorFirstResponder() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
@ -11627,7 +11809,6 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
)
defer { window.orderOut(nil) }
realizeWindowLayout(window)
guard let contentView = window.contentView else {
XCTFail("Expected content view")
return
@ -11652,10 +11833,16 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
slot.isHidden = true
XCTAssertNil(
window.firstResponder,
XCTAssertFalse(
window.firstResponder === inspectorView,
"Hiding a browser slot should yield any owned inspector responder before it goes off-screen"
)
if let firstResponderView = window.firstResponder as? NSView {
XCTAssertFalse(
firstResponderView === slot || firstResponderView.isDescendant(of: slot),
"Hiding a browser slot should not leave first responder inside the hidden slot"
)
}
}
func testHiddenPortalSyncDoesNotStealLocallyHostedDevToolsWebViewDuringResize() {