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
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ public nonisolated struct BrowserResizeTunables {
help: "While a Chromium page lays out at a new window or pane size, the gap beside its last frame shows the page's own background color. Off shows Chromium's white and the pane ground.",
default: true, code: "BrowserResizeTunables.pageBackdrop")

/// Off (the default): a Chromium page draws in its own child window over
/// the pane. On: the page's pixels live in the cmux window's layer tree
/// (fork API 26, `CEFHostedPageView`); the page window stays as a
/// transparent input target. Applies to pages shown after the change.
/// On (the default since cmux-ad m19, 2026-11-05: perf paths ship on, no switches): the page's pixels live in the
/// cmux window's layer tree (fork API 26, `CEFHostedPageView`); the page window stays as a transparent input
/// target. Off: a Chromium page draws in its own child window over the pane. Applies to pages shown after the change.
public static let layerHosting = Tunable<Bool>.toggle(
"browserResize.layerHosting", section, "Host Chromium pages in the window's layers",
help: "A Chromium page draws inside the cmux window's own layer tree, so it moves, clips and shows with the panes in one frame. Its page window stays on top only to take input. Needs Chromium fork API 26; applies when a page is shown next.",
default: false, code: "BrowserResizeTunables.layerHosting")
default: true, code: "BrowserResizeTunables.layerHosting")

/// While a page is layer hosted (`layerHosting`), how long a resize of
/// its pane may hold the window's frame for Chromium's frame at the new
Expand All @@ -36,7 +35,7 @@ public nonisolated struct BrowserResizeTunables {
public static let syncDeadline = Tunable<Double>.number(
"browserResize.syncDeadline", section, "Hosted page resize wait",
help: "How long a pane resize waits for a layer-hosted Chromium page to draw at the new size, so the page and the pane change in the same frame. Never more than half the shortest display frame (4 ms with a 120 Hz display). Applies at the page's next show. 0 never waits.",
default: 0.008, range: 0...0.1, step: 0.001, unit: .seconds, code: "BrowserResizeTunables.syncDeadline")
default: 0, range: 0...0.1, step: 0.001, unit: .seconds, code: "BrowserResizeTunables.syncDeadline")

/// During the window's live resize, how long each Chromium page (hosted or
/// not) may hold the window's frame for its frame at the new size (fork
Expand All @@ -45,7 +44,7 @@ public nonisolated struct BrowserResizeTunables {
public static let liveResizeDeadline = Tunable<Double>.number(
"browserResize.liveResizeDeadline", section, "Page wait during window resize",
help: "While the window is resized, how long each Chromium page may hold a frame to draw at the new size, so it resizes with the window. Never more than half the shortest display frame. 0 never waits; the page then shows its last frame until it draws.",
default: 0.016, range: 0...0.1, step: 0.001, unit: .seconds, code: "BrowserResizeTunables.liveResizeDeadline")
default: 0, range: 0...0.1, step: 0.001, unit: .seconds, code: "BrowserResizeTunables.liveResizeDeadline")

/// With `dragResize` at `resizeSync`, how long each Chromium page over a dragged pane (hosted or not) may hold the
/// window's frame for its frame at the new size, from the drag's first move until its release landed (fork API 27),
Expand All @@ -54,7 +53,7 @@ public nonisolated struct BrowserResizeTunables {
public static let dragResizeDeadline = Tunable<Double>.number(
"browserResize.dragResizeDeadline", section, "Page wait during a divider drag",
help: "With the Resize sync drag mode, how long each Chromium page beside a dragged divider may hold a frame to draw at the pane's new size, so the page and its pane change in the same frame. Never more than half the shortest display frame. Needs Chromium fork API 27. 0 never waits.",
default: 0.016, range: 0...0.1, step: 0.001, unit: .seconds, code: "BrowserResizeTunables.dragResizeDeadline")
default: 0, range: 0...0.1, step: 0.001, unit: .seconds, code: "BrowserResizeTunables.dragResizeDeadline")

/// How a Chromium page follows a pane divider or a column or row edge a person drags
/// (Leo, 2026-10-26: "resizing the panes it propagates so fucking slowly to the right side browser tab
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ public nonisolated struct TerminalDragTunables {
public nonisolated init() {}
public static let section = TunableSection(id: "terminalDrag", title: "Terminal Drag", symbol: "rectangle.split.2x1", order: 51)

/// On (the default): through a divider drag the terminal keeps its surface size and its text, clipped where the
/// Off (the default since cmux-ad m19, 2026-11-05: perf paths ship on, no switches): when on, through a divider
/// drag the terminal keeps its surface size and its text, clipped where the
/// pane is narrower and beside the terminal background where it is wider, and reflows once when the drag lands
/// (RESIZE VIDEOS A3: the text wrapped again on every drag frame). Off: the terminal draws its text reflowed to the
/// pane on every drag frame (the PTY still resizes once, at the landing).
public static let holdsSurfaceDuringDrag = Tunable<Bool>.toggle(
"terminalDrag.holdSurface", section, "Hold terminal text during a divider drag",
help: "While a divider is dragged, each terminal keeps its size and text (clipped or padded) and reflows once when the drag lands. Off: the text reflows to the pane on every frame of the drag.",
default: true, code: "TerminalDragTunables.holdsSurfaceDuringDrag")
default: false, code: "TerminalDragTunables.holdsSurfaceDuringDrag")

/// Off (the default): a resized terminal's view takes its new bounds at once and Ghostty's frame of that size
/// On (the default since cmux-ad m19, 2026-11-05: perf paths ship on, no switches; this switch and the off path
/// are deleted within 24 h). Off: a resized terminal's view takes its new bounds at once and Ghostty's frame of that size
/// lands 1-4 refreshes later (FRAME OWNER REFACTOR P2), and a key resize holds the surface at one size per press.
/// On: ready-or-keep (stage S4): in every commit a terminal's bounds and its pixels change together, or both keep
/// their last size, and in a key resize the terminal shows its pane's size in every frame while its PTY gets one
Expand All @@ -27,7 +29,7 @@ public nonisolated struct TerminalDragTunables {
public static let framesOwnedByCommit = Tunable<Bool>.toggle(
"motion.frameOwner.terminals", section, "Terminal frame and size in one commit",
help: "A resized terminal shows its new size only in the commit that has its frame drawn at that size; until then it keeps its last size and frame. In a key resize the terminal follows its pane in every frame, and its program gets one size per press. Off: the size changes at once and the frame comes 1-4 refreshes later, and a key resize keeps one terminal size per press.",
default: false, code: "TerminalDragTunables.framesOwnedByCommit")
default: true, code: "TerminalDragTunables.framesOwnedByCommit")

public static var all: [TunableDescriptor] { [holdsSurfaceDuringDrag.descriptor, framesOwnedByCommit.descriptor] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,17 @@ import Foundation
import GhosttyNextKit
import Testing

/// A person's divider drag keeps each terminal's text where it is until the drag lands (RESIZE VIDEOS A3, film
/// col-drag-fast frames 124-131: the left column's text wrapped again on every drag frame). The PTY already resizes
/// once per drag (#31613), but the surface took the pane's size on every drag event and drew its grid reflowed to it
/// (Ghostty's reflow preview), so the wrap point moved in every frame. Through the drag the surface now keeps its
/// size (clipped where the pane is narrower, the terminal background beside it where wider), as a Chromium page
/// keeps its size through a drag; when the drag lands it takes the pane's size and reflows once.
/// cmux-ad m14/m18 (the drag hold `terminalDrag.holdSurface` is off by default since cmux-ad m19, 2026-11-05): in each
/// frame of a divider drag each terminal's surface takes its pane's size, so Ghostty's reflow preview re-wraps its text
/// at that size; its PTY still resizes once, when the drag lands (`DividerDragTerminalResizeCountTests`). Before m19 the
/// hold kept each surface at its drag-start size until the drag landed (RESIZE VIDEOS A3).
/// Real terminal sessions behind the app's ResizeCoordinator (`SplitTerminalResizeCountTests`); the test is the clock.
@MainActor
@Suite(.closesItsWindows)
struct DividerDragTerminalSteadyTests {
typealias Harness = SplitTerminalResizeCountTests

@Test func aDividerDragKeepsEachTerminalsSurfaceSizeUntilItLands() async throws {
@Test func aDividerDragGivesEachTerminalsSurfaceItsPanesSizeInEveryFrame() async throws {
_ = NSApplication.shared
let screen = Harness.Screen(Harness.pair)
defer { screen.close() }
Expand All @@ -43,7 +41,8 @@ struct DividerDragTerminalSteadyTests {
}
let hostWidth = try #require(left.superview?.bounds.width)
#expect(hostWidth < before.width - 100, "the drag narrowed p1's pane (\(before.width) -> \(hostWidth))")
#expect(sizes == [before.width], "p1's surface took \(sizes.count) widths during the drag: \(sizes.sorted())")
#expect(sizes.count >= 25, "p1's surface took \(sizes.count) widths in 30 drag steps: the drag holds it (\(sizes.sorted()))")
#expect(abs(left.bounds.width - hostWidth) < 40, "p1's surface follows its pane in the drag (\(left.bounds.width) in \(hostWidth))")

view.handleDrag(kind: .split("r"), event: .ended(CGPoint(x: start.x - 240, y: start.y), []))
screen.root.syncWithModel()
Expand All @@ -53,12 +52,11 @@ struct DividerDragTerminalSteadyTests {
await screen.drain()
let after = try #require(left.superview?.bounds.width)
#expect(abs(left.bounds.width - after) < 40, "after the drag landed p1's surface fits its pane again (\(left.bounds.width) vs \(after))")
#expect(left.bounds.width < before.width - 100, "the surface reflowed to the narrower pane once the drag landed")
#expect(left.bounds.width < before.width - 100, "the surface keeps the narrower pane's size after the drag landed")
}

/// A surface swapped in mid-drag (a replay puts a fresh surface in the host, `TerminalSession.swapSurface`): the
/// old surface's drag hold goes with it, so the fresh surface takes its pane's size, not the drag start's (review of
/// the drag hold: the old surface's window-leave ran after it had left the host, and the hold stayed for good).
/// A surface swapped in mid-drag (a replay puts a fresh surface in the host, `TerminalSession.swapSurface`) takes its
/// pane's size, not the drag start's.
@Test func aSurfaceSwappedInMidDragTakesItsPanesSize() async throws {
_ = NSApplication.shared
let screen = Harness.Screen(Harness.pair)
Expand All @@ -79,7 +77,8 @@ struct DividerDragTerminalSteadyTests {
screen.root.syncWithModel()
await screen.step()
}
#expect(session.surfaceView.bounds.width == before.width, "the drag holds p1's surface at its drag-start size")
#expect(session.surfaceView.bounds.width < before.width - 100,
"p1's surface follows its narrower pane in the drag (\(session.surfaceView.bounds.width) pt, \(before.width) at the start)")

let (_, continuation) = AsyncStream<TerminalOutgoing>.makeStream()
defer { continuation.finish() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Testing

/// FRAME OWNER REFACTOR S3: the rules the content policy keeps beside PaneGroundCoversPaneTests (the shell never shows
/// inside a pane rect).
/// - In a key resize with `motion.frameOwner.terminals` off (the default until the flip), the terminal surface stays
/// - In a key resize with `motion.frameOwner.terminals` off (the default until cmux-ad m19), the terminal surface stays
/// on the pane edge that does not move, for each side a pane can grow or shrink on, while its host (the container)
/// follows the pane in every frame; the surface takes one size per press, at the press, and none in a spring frame
/// or at the landing (the tests named `...WithTheSwitchOff`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ struct PaneGroundCoversPaneTests {
+ "\(type(of: culprit)) at x \(Int(r.minX))..\(Int(r.maxX)) y \(Int(r.minY))..\(Int(r.maxY))"
}

/// Turns `motion.frameOwner.terminals` on (or, with `on` false, off) for the frames that follow (off by default
/// until its held-key cost is fixed): the key-resize contract tests (cmux-ad m6) run with it on, and the guards of
/// Turns `motion.frameOwner.terminals` on (or, with `on` false, off) for the frames that follow (on by default
/// since cmux-ad m19, 2026-11-05): the key-resize contract tests (cmux-ad m6) run with it on, and the guards of
/// the default path (one surface size per press) with it off. The store is process-wide and another test sets or
/// resets it, so an async test calls this again after each `await`, before its next press or frame (main-actor code
/// between two awaits never interleaves with another test). Its value: whether the switch has the value asked for.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ struct TerminalFrameOwnerMainThreadTests {
_ = NSApplication.shared
TunableStore.shared.activate(file: nil)
TunableStore.shared.register(TerminalDragTunables.all)
// The switch starts off, as in the films; ``close()`` resets it.
// The switch starts off (the pre-m19 default the films used); ``close()`` resets it.
TunableStore.shared.set(TerminalDragTunables.framesOwnedByCommit.key, .bool(false))
drew = WindowDrawPolicy.drawsWhenOccluded
WindowDrawPolicy.drawsWhenOccluded = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ import Testing
/// none. A resize saves again as soon as the terminal is quiet, with no interval.
@Test(arguments: [false, true])
func aResizedTerminalIsSavedAgainWithoutWaitingForTheInterval(switchOn: Bool) async throws {
// Review of #35470 (T5): in both states of `motion.frameOwner.terminals` (off, the default: the surface takes
// Review of #35470 (T5): in both states of `motion.frameOwner.terminals` (off, the default until cmux-ad m19: the surface takes
// its new size in the layout; on: in the commit that shows its frame, a refresh or two later).
defer { PaneGroundCoversPaneTests.releaseFrames() }
try #require(PaneGroundCoversPaneTests.ownFrames(switchOn), "the switch motion.frameOwner.terminals did not take \(switchOn)")
Expand Down Expand Up @@ -233,7 +233,7 @@ import Testing
/// (a frame of the old size is refused at launch), once the size holds, without waiting for its output to stop.
@Test(arguments: [false, true])
func aBusyResizedTerminalIsSavedAtItsNewSize(switchOn: Bool) async throws {
// Review of #35470 (T5): in both states of `motion.frameOwner.terminals` (off, the default: the surface takes
// Review of #35470 (T5): in both states of `motion.frameOwner.terminals` (off, the default until cmux-ad m19: the surface takes
// its new size in the layout; on: in the commit that shows its frame, a refresh or two later).
defer { PaneGroundCoversPaneTests.releaseFrames() }
try #require(PaneGroundCoversPaneTests.ownFrames(switchOn), "the switch motion.frameOwner.terminals did not take \(switchOn)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,11 @@ import Testing
#expect(tracker.reads.last?.screenRect.size == NSSize(width: 420, height: 600))
}

/// cmux#26037: with `browserResize.dragResize` at `resizeSync`, a divider drag makes the shown page's window wait
/// for its frame at each new size from the drag's first move until its release landed, with no held page size (the
/// page lays out live). The end reaches the page window that heard the start.
@Test func aResizeSyncDragMakesThePageWindowWaitUntilTheReleaseLanded() throws {
/// cmux#26037: with `browserResize.dragResize` at `resizeSync`, a divider drag joins the shown page's window to the
/// drag from the drag's first move until its release landed, with no held page size (the page lays out live). The
/// end reaches the page window that heard the start. cmux-ad m18/m19: a pane never waits for a page, so the drag
/// deadline is 0 by default and the page window is never asked to hold a frame.
@Test func aResizeSyncDragJoinsThePageWindowAndNeverWaitsForIt() throws {
Self.setDragResize(.resizeSync)
defer { Self.setDragResize(nil) }
let (runtime, root) = try Self.runtime()
Expand All @@ -233,15 +234,11 @@ import Testing
first.container.paneResizeGestureDidChange(active: true)
#expect(host.hostView.layerHosting.inPaneDrag)
#expect(first.container.heldPageSize == nil, "the page lays out live")
let wait = try #require(TabSpyShim.resizeSyncs.last)
#expect(wait.browser == 10)
// The 16 ms default, under half the shortest frame of this host's displays.
#expect(wait.timeoutMs > 0 && wait.timeoutMs <= 16)
#expect(!TabSpyShim.resizeSyncs.contains { $0.timeoutMs > 0 }, "the drag asked a page to hold a frame: \(TabSpyShim.resizeSyncs)")
first.container.paneResizeGestureDidChange(active: false)
#expect(!host.hostView.layerHosting.inPaneDrag)
#expect(first.container.syncedDragHostings.isEmpty)
#expect(TabSpyShim.resizeSyncs.map(\.browser) == [10, 10])
#expect(TabSpyShim.resizeSyncs.last?.timeoutMs == 0, "a page in its own window stops waiting when the drag lands")
#expect(!TabSpyShim.resizeSyncs.contains { $0.timeoutMs > 0 }, "a page waited at the drag's end: \(TabSpyShim.resizeSyncs)")
}

/// cmux-ad m6 1b, the interim browser contract for key resizes (FRAME OWNER REFACTOR, STATUS 11198; Lawrence: "tabbar
Expand Down Expand Up @@ -366,8 +363,9 @@ import Testing
#expect(first.container.syncedDragHostings.isEmpty)
}

/// A docked DevTools joins the wait when it opens after the first drag move, or when its parked
/// tab becomes visible. Hiding it again releases only its wait, while the shared page stays synced.
/// A docked DevTools joins the drag when it opens after the first drag move, or when its parked tab becomes
/// visible, and is never asked to wait (the drag deadline is 0). Hiding it again releases only its share, while
/// the shared page stays in the drag.
@Test(arguments: [false, true])
func dockedDevToolsJoiningAResizeSyncDragWaitsUntilHidden(_ initiallyParked: Bool) throws {
Self.setDragResize(.resizeSync)
Expand Down Expand Up @@ -398,7 +396,7 @@ import Testing

let devTools = try #require(first.devToolsController.views?.host.layerHosting)
#expect(devTools.inPaneDrag)
#expect(TabSpyShim.resizeSyncs.contains { $0.browser == 20 && $0.timeoutMs > 0 })
#expect(!TabSpyShim.resizeSyncs.contains { $0.browser == 20 && $0.timeoutMs > 0 }, "DevTools was asked to hold a frame (m18: a pane never waits for a page)")
first.container.layoutContent()
first.container.isHidden = true
#expect(!devTools.inPaneDrag, "hidden DevTools spends no frame wait")
Expand Down
Loading
Loading