From 2bb56d783a92c2974bb3022c73afd7dc61a9ce4e Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 3 Nov 2026 03:32:14 +0000 Subject: [PATCH 1/2] test(agent-pane): the tab row paints nothing behind the header tools, as the page ships The browser test loaded src/styles.css, which the agent pane bundle never loads, and set the row's html color itself, so it passed while the shipped page painted WebTheme's html background (a chat's own color) behind the tools. It now loads only the pane's own sheets under WebTheme's html rule and reads the page's own pixels; the toggled Session Info plate stays. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a7568197607c3cdfb81 --- .../TabRowToolsStayClickableTests.swift | 34 +++--- .../test/chat-header-tab-row-surface.test.ts | 103 +++++++++++------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/TabRowToolsStayClickableTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/TabRowToolsStayClickableTests.swift index 1033b9be04fd..0f29d739e166 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/TabRowToolsStayClickableTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/TabRowToolsStayClickableTests.swift @@ -15,7 +15,6 @@ import Testing /// A chat that draws `need` points of tools in the row whenever the pane offers it, as the page does. final class RowChat: NSView, PaneTabRowSharing { let need: CGFloat - var tabRowSurface: SurfaceKind? = .agentPane private(set) var shared: CGFloat = 0 var onTabRowToolsChange: (() -> Void)? init(need: CGFloat) { @@ -65,12 +64,13 @@ import Testing #expect(chatUnderRow == (CGFloat(width) - pane.toolbarWidth(shown: strip.height) >= chat.need * 2)) } - /// Leo (2026-11-01), on a chat with its own background color (`appearance.surfaces.agentPane`): - /// "why is the background STILL not removed". The page painted the chat's color under its tools, - /// but the row behind the tabs kept the pane's theme background, a darker band across the top. - /// While the chat shares the row, the strip paints the chat's background; otherwise its own. + /// Leo (2026-11-03), on the chat's own color (`appearance.surfaces.agentPane`) drawn as a rectangle + /// behind the header buttons in the pane's tab row: "it's so obvious to just remove the thing". The + /// row has no backing: while the chat shares it, the strip behind the tabs stays clear (only the + /// user's own tab bar color fills it), as the page leaves the row clear behind the tools + /// (webviews header.css, `html:has(.acpmux-shell[data-tab-row])`). #32039 painted the chat's color here. @Test(arguments: [480, 900]) - func theRowBehindTheTabsTakesTheChatsBackground(width: Int) throws { + func theTabRowHasNoBackingWhileTheChatSharesIt(width: Int) throws { let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: CGFloat(width), height: 500), styleMask: [.titled], backing: .buffered, defer: false) window.isReleasedWhenClosed = false @@ -87,19 +87,13 @@ import Testing pane.needsLayout = true pane.layoutSubtreeIfNeeded() } - let shares = pane.contentHost.frame.minY < pane.stripView.frame.maxY - let fill = pane.stripView.layer?.backgroundColor.flatMap { NSColor(cgColor: $0)?.usingColorSpace(.sRGB) } - if shares { - let shown = try #require(fill, "the row behind the tabs has no fill while the chat paints its own") - #expect(abs(shown.redComponent - chatColor.red) < 0.01 && abs(shown.blueComponent - chatColor.blue) < 0.01, - "the strip paints \(shown), not the chat's \(chatColor)") - } else { - #expect(fill == nil, "a chat in its own header row leaves the strip clear") - } - // Nothing sharing the row: the strip is clear again. - chat.tabRowSurface = nil - pane.needsLayout = true - pane.layoutSubtreeIfNeeded() - #expect(pane.stripView.layer?.backgroundColor == nil) + #expect(pane.stripView.layer?.backgroundColor == nil, "the tab row paints the chat's color behind the tabs") + // The user's own tab bar color still fills the strip. + let barColor = ThemeRGB(hex: 0x113355) + pane.paneScope.setSurfaceBackgrounds(SurfaceBackgrounds(overrides: [.agentPane: SurfaceBackground(color: chatColor, opacity: 1), + .tabBar: SurfaceBackground(color: barColor, opacity: 1)])) + pane.themeDidChange() + let fill = try #require(pane.stripView.layer?.backgroundColor.flatMap { NSColor(cgColor: $0)?.usingColorSpace(.sRGB) }) + #expect(abs(fill.blueComponent - barColor.blue) < 0.01, "the strip paints \(fill), not the tab bar's \(barColor)") } } diff --git a/webviews/test/chat-header-tab-row-surface.test.ts b/webviews/test/chat-header-tab-row-surface.test.ts index 25455996423f..48853e21f582 100644 --- a/webviews/test/chat-header-tab-row-surface.test.ts +++ b/webviews/test/chat-header-tab-row-surface.test.ts @@ -171,48 +171,67 @@ await requireBrowserLane("chat-header-tab-row-surface.test.ts", async () => { } } - // The tools in the tab row sit on the row's own surface, as the tabs do: the shell paints no band of the page's - // color behind them, only below the row (Leo, 2026-11-02: "FOR THE FUCKING STUPID BACKGROUND ON THE FUCKING TOP - // RIGHT ICONS TO BE REMOVED"). - test("the tab row shell paints nothing behind the tools", async () => { - const shellCss = [ - read("styles.css"), - read("agent-session/shared/styles.css"), - read("agent-session/acpmux/styles.css"), - read("agent-session/acpmux/header/header.css"), - ].join("\n"); - const page = await browser.newPage({ viewport: { width: 320, height: 120 } }); - try { - // The native row under the page is green; the page's own color is red. - await page.setContent(` -
-
-
-
-
-
-
- `); - const shot = await page.screenshot({ type: "png" }); - const { PNG } = await import("pngjs"); - const png = PNG.sync.read(shot); - const at = (x: number, y: number) => { - const i = (png.width * y + x) * 4; - return [png.data[i], png.data[i + 1], png.data[i + 2]]; - }; - // Behind the tools (inside the 40px row): the row's green. Below the row: the page's red. - expect(at(300, 10)).toEqual([0, 255, 0]); - expect(at(10, 20)).toEqual([0, 255, 0]); - expect(at(10, 80)).toEqual([255, 0, 0]); - } finally { - await page.close(); - } - }); + // The tools in the tab row sit on the row's own surface, as the tabs do: nothing on the page paints behind them, + // only below the row (Leo, 2026-11-02: "FOR THE FUCKING STUPID BACKGROUND ON THE FUCKING TOP RIGHT ICONS TO BE + // REMOVED"; 2026-11-03: "it's so obvious to just remove the thing"). The page here is the agent page as it ships: + // its own sheets only (never src/styles.css, which the pane's bundle does not load) under WebTheme's document rule, + // `html{background:var(--cmux-surface-background) !important}`, with a chat color (appearance.surfaces.agentPane) + // as that background. The toggled tool keeps its plate. + for (const surface of ["rgb(255, 0, 0)", "rgba(255, 0, 0, 0.6)"]) { + test(`the tab row paints nothing behind the tools, page background ${surface}`, async () => { + const shellCss = [ + read("agent-session/shared/styles.css"), + read("agent-session/acpmux/styles.css"), + read("agent-session/acpmux/header/header.css"), + ].join("\n"); + const page = await browser.newPage({ viewport: { width: 320, height: 120 } }); + try { + // The page's own color is red; its canvas is transparent (omitBackground), as WebKit's and see-through + // Chromium's are, so a pixel the page leaves alone reads alpha 0. + await page.setContent(` + + +
+
+
+
+ + +
+
+
+
+ `); + const toggle = page.locator(".acpmux-header-panel-toggle"); + const box = (await toggle.boundingBox())!; + // The page's own pixels, with alpha: omitBackground leaves Chromium's canvas transparent. + const shot = await page.screenshot({ type: "png", omitBackground: true }); + const { PNG } = await import("pngjs"); + const png = PNG.sync.read(shot); + const at = (x: number, y: number) => { + const i = (png.width * y + x) * 4; + return [png.data[i], png.data[i + 1], png.data[i + 2], png.data[i + 3]]; + }; + // Inside the 40px row, beside and between the tools: nothing at all (alpha 0). + expect(at(10, 20)[3]).toBe(0); + expect(at(Math.round(box.x) - 4, 20)[3]).toBe(0); + expect(at(319, 2)[3]).toBe(0); + // The toggled Session Info button keeps its plate. + expect(at(Math.round(box.x + 3), Math.round(box.y + 3))[3]).toBeGreaterThan(0); + // Below the row: the page's own color. + const below = at(10, 80); + expect(below[0]).toBeGreaterThan(200); + expect(below[3]).toBeGreaterThan(100); + } finally { + await page.close(); + } + }); + } test("the loading shell carries the offered row state before the handshake", () => { const app = read("agent-session/acpmux/App.tsx"); From 514860f7b6d1fe9e85f2be5ddfe04fd0962f1a70 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 3 Nov 2026 03:32:14 +0000 Subject: [PATCH 2/2] fix(agent-pane): no backing behind the header buttons in the tab row The agent page's document root paints WebTheme's page background (html{background:var(--cmux-surface-background) !important}): a chat's own color under appearance.surfaces.agentPane, or the opaque surface. The rule meant to clear it in the tab row (#25638) went into webviews/src/styles.css, which the pane's bundle never loads, so the color filled the row behind the tools, and #32039 then painted the same color behind the tabs. The row now has no backing: html is clear while the chat shares the row, the shell paints the page background below the row only, and the strip behind the tabs is clear again (only the user's own tab bar color fills it). Takes back #32039. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-7a7568197607c3cdfb81 --- .../CmuxNextAgentPane/AgentPaneTabRow.swift | 3 --- .../Sources/CmuxNextApp/PaneContentView.swift | 16 ++++------------ .../CmuxNextDesign/PaneTabRowSharing.swift | 6 ------ .../src/agent-session/acpmux/header/header.css | 11 ++++++++--- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTabRow.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTabRow.swift index b73edbb0754d..e002b38a66f7 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTabRow.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextAgentPane/AgentPaneTabRow.swift @@ -109,7 +109,4 @@ extension PaneTabRowSharing where Self: AgentPaneView { public var tabRowToolsNeed: CGFloat? { model.tabRow.sharedNeed(zoom: webView.pageZoom) } public var onTabRowToolsChange: (() -> Void)? { get { model.tabRow.onChange } set { model.tabRow.onChange = newValue } } public func setTabRow(height: CGFloat, trailingInset: CGFloat) { AgentPaneTabRow.offer(points: height, trailingInset: trailingInset, to: self) } - /// The chat's page paints its surface's background (an `appearance.surfaces.agentPane` color) - /// under the header; the pane paints the same behind its tabs, so the row is one fill. - public var tabRowSurface: SurfaceKind? { surfaceKind } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneContentView.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneContentView.swift index 68e8cbb09cae..d2102eec7e0f 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneContentView.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneContentView.swift @@ -26,9 +26,6 @@ final class PaneContentView: NSView, PaneContentChrome { let backdropState = PaneBackdropState() private var paneBackgroundsObservation: Task? let contentHost = NSView() - /// The surface of the content sharing the tab row now (``PaneTabRowSharing/tabRowSurface``), whose - /// background the strip paints behind its tabs (``paintStrip()``); nil while nothing shares it. - private(set) var rowSurface: SurfaceKind? private(set) weak var content: NSView? /// Explicitly observed ownership: first-paint holds can change this without a tab-store update. let nativeToolOwner = PaneNativeToolOwner() @@ -177,8 +174,6 @@ final class PaneContentView: NSView, PaneContentChrome { toolbar?.view.isHidden = shown <= 0 let tools = toolbarWidth(shown: shown), room = bounds.width - tools, row = onTop && !isBandActive && !hidesStrip && hostsContent && shown > 0 ? content as? PaneTabRowSharing : nil let rowContent = row?.fitsTabRow(room: room) == true ? row : nil, rowTools = rowContent?.tabRowWidth(room: room) ?? 0 // a chat's tools in the row when they fit (PaneTabRowSharing) - let surface = rowTools > 0 ? rowContent?.tabRowSurface : nil - if surface != rowSurface { rowSurface = surface; paintStrip() } if !isBandActive { stripView.frame = NSRect(x: 0, y: onTop ? 0 : bounds.height - shown, width: bounds.width - tools - rowTools, height: shown) toolbar?.view.frame = NSRect(x: bounds.width - tools, y: stripView.frame.minY, width: tools, height: shown) @@ -469,15 +464,12 @@ final class PaneContentView: NSView, PaneContentChrome { paintPaneBackdrop() } - /// The strip: clear, or the user's tab bar background (R55). While a chat shares the row, the - /// background its page paints under the header (``PaneTabRowSharing/tabRowSurface``), so the row - /// behind the tabs and the chat's tools is one fill with no band of its own. + /// The strip: clear, or the user's tab bar background (R55). A chat sharing the row never gives it + /// a fill: the row, its tabs and the chat's tools sit on the window's own glass (Leo, 2026-11-03, + /// on the chat's color behind the header buttons: "it's so obvious to just remove the thing"). func paintStrip() { stripView.wantsLayer = true - let surface = rowSurface - stripView.layer?.backgroundColor = stripView.performWithTheme { - (Palette.surfaceOverride(.tabBar) ?? surface.flatMap { Palette.surfaceOverride($0) })?.cgColor - } + stripView.layer?.backgroundColor = stripView.performWithTheme { Palette.surfaceOverride(.tabBar)?.cgColor } } } // A terminal or page edge inside the titlebar band never moves the window; diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/PaneTabRowSharing.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/PaneTabRowSharing.swift index 02d08c1204aa..5a66b02ce3db 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/PaneTabRowSharing.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/PaneTabRowSharing.swift @@ -18,14 +18,9 @@ public protocol PaneTabRowSharing: AnyObject { /// The row the view may draw its tools in: the top `height` points (0: it does not share the /// row now), less `trailingInset` points at the trailing end, where the pane's own toolbar is. func setTabRow(height: CGFloat, trailingInset: CGFloat) - /// The surface whose background (`appearance.surfaces`) the view paints under its header, so the - /// pane paints the same fill behind its tabs while the view shares the row: the row has no fill - /// of its own. Nil: the strip keeps its own (clear, or the user's tab bar background). - var tabRowSurface: SurfaceKind? { get } } extension PaneTabRowSharing { - public var tabRowSurface: SurfaceKind? { nil } /// Points the strip gives up beside the view's tools in a row `room` wide (at most half of it). public func tabRowWidth(room: CGFloat) -> CGFloat { min(max(0, tabRowToolsWidth ?? 0), max(0, room / 2)) } @@ -61,7 +56,6 @@ extension PaneTabRowForwarding { set { tabRowSharer?.onTabRowToolsChange = newValue } } public func setTabRow(height: CGFloat, trailingInset: CGFloat) { tabRowSharer?.setTabRow(height: height, trailingInset: trailingInset) } - public var tabRowSurface: SurfaceKind? { tabRowSharer?.tabRowSurface } } extension NSView { diff --git a/webviews/src/agent-session/acpmux/header/header.css b/webviews/src/agent-session/acpmux/header/header.css index d28556d561c1..8ec94698debc 100644 --- a/webviews/src/agent-session/acpmux/header/header.css +++ b/webviews/src/agent-session/acpmux/header/header.css @@ -136,9 +136,14 @@ /* One row on top (header/tabRow.ts, the chrome.tabs.rowTools Debug setting): the App draws the pane's tabs over the header's left part, so the header is the tab row's height, takes no clicks there and shows only its tools, left of the pane's own "…"; the transcript fades out under the row. */ -/* The tools in the row sit on the row's own surface, as the tabs beside them do: the shell paints the page's color - only below the row, never a band of it behind the tools (Leo, 2026-11-02, above). */ -.acpmux-shell[data-tab-row]{background:linear-gradient(transparent var(--acpmux-tab-row-height),var(--agent-page-bg) var(--acpmux-tab-row-height))} +/* The tools in the row sit on the row's own surface, as the tabs beside them do: nothing on the page paints behind + them, the shell paints the page's color only below the row (Leo, 2026-11-02, above; 2026-11-03, on the chat's color + drawn as a rectangle behind the header buttons: "it's so obvious to just remove the thing"). The document root + carries the page background (WebTheme's html rule, --cmux-surface-background: a chat's own color under + appearance.surfaces.agentPane), so it is clear here and the shell paints that background below the row instead. + test/chat-header-tab-row-surface.test.ts loads the agent page's own sheets with WebTheme's rule. */ +html:has(.acpmux-shell[data-tab-row]){background:transparent !important} +.acpmux-shell[data-tab-row]{background:linear-gradient(transparent var(--acpmux-tab-row-height),var(--agent-page-bg) var(--acpmux-tab-row-height)),linear-gradient(transparent var(--acpmux-tab-row-height),var(--cmux-surface-background,transparent) var(--acpmux-tab-row-height))} .acpmux-header[data-tab-row]{position:relative;z-index:var(--layer-sticky);flex:0 0 var(--acpmux-tab-row-height);height:var(--acpmux-tab-row-height);padding:0 var(--acpmux-tab-row-inset) 0 0;background:transparent;pointer-events:none} .acpmux-header[data-tab-row] .acpmux-header-status-slot{visibility:hidden} /* In the tab row the warning leads the tools (ChatHeaderInlineStatus): the slot is under the tabs. */