From 5def1b6c852e19c0dbf35a30b90d8f1c312ae304 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 18 Oct 2026 14:37:37 -0700 Subject: [PATCH 1/5] cmux-next: overlays are themed Liquid Glass, and tooltips let more through Overlays that carry text (palette, hover cards, popovers, find and prompt bars, web popups) were pinned to the solid popup surface (Leo 2026-10-12). Leo, 2026-10-18: tooltips are too opaque and menus warrant themed glass. They now follow this Mac's material: Liquid Glass tinted with the theme (its refracting rim included), vibrancy before macOS 26, and the solid surface under Reduce Transparency. The overlay tint is 1.25x the chrome glass tint since overlays sit over text. Tooltips' least tint drops from 0.86 to 0.6; the 4.5:1 rule over black and white decides above it (about 0.7 dark, 0.6 light). Co-Authored-By: Claude Opus 5.5 --- .../Sources/CmuxNextDesign/Glass.swift | 15 +++++++------- .../CmuxNextDesign/OverlaySurface.swift | 8 ++++---- .../Tooltip/TooltipSurface.swift | 5 +++-- .../Tunables/ChromeTunables.swift | 2 +- .../PopupSurfaceTests.swift | 20 ++++++++++++++----- webviews/src/pages/shared/desktop.css | 4 ++-- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Glass.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Glass.swift index 7fd4ce5ed5e2..60c944b31420 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Glass.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Glass.swift @@ -28,18 +28,19 @@ public struct Glass { }() /// A floating panel over content that carries text (palette, dialogs, - /// hover cards, find and prompt bars, page info, popovers, toasts) and - /// hosts `content` edge to edge: the solid popup surface - /// (`Palette.popupSurface`) with a hairline, never glass, so the text - /// under it never shows through (Leo, 2026-10-12). It follows the theme - /// live. Use this for every overlay; it takes clicks unless - /// `interactive` is false. + /// hover cards, find and prompt bars, page info, popovers) and hosts + /// `content` edge to edge: Liquid Glass tinted with the theme, with its + /// own refracting rim, like iOS's menus and folders (Leo, 2026-10-18, + /// replacing the solid surface of 2026-10-12); the frosted vibrancy + /// material before macOS 26, and the solid popup surface under Reduce + /// Transparency. It follows the theme live. Use this for every overlay; + /// it takes clicks unless `interactive` is false. public static func makeOverlayPanel( content: NSView? = nil, cornerRadius: CGFloat = Metrics.panelCornerRadius, interactive: Bool = true ) -> OverlaySurfaceView { - let surface = OverlaySurfaceView(material: .opaque, interactive: interactive, cornerRadius: cornerRadius) + let surface = OverlaySurfaceView(interactive: interactive, cornerRadius: cornerRadius) surface.translatesAutoresizingMaskIntoConstraints = false if let content { content.translatesAutoresizingMaskIntoConstraints = true diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift index 460a9feee4b1..e47cfad7516e 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift @@ -8,10 +8,10 @@ public import AppKit /// Content goes in `contentView`, above the material, and may size the /// surface through constraints. Every floating panel that carries text /// (palette, dialogs, hover cards, find and prompt bars, page info, -/// popovers) is one of these, made with `Glass.makeOverlayPanel`, which pins -/// it to the solid popup surface (`Palette.popupSurface`); glass would let -/// the text under it show through. Overlays without text (the drop overlay, -/// dock backdrop) follow this Mac's material. +/// popovers) is one of these, made with `Glass.makeOverlayPanel`, and +/// follows this Mac's material like the overlays without text (the drop +/// overlay, dock backdrop): themed Liquid Glass, whose tint +/// (`glassOverlayTintStrength`) keeps its text over busy content. @MainActor public final class OverlaySurfaceView: NSView { public private(set) var material: OverlayMaterial diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tooltip/TooltipSurface.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tooltip/TooltipSurface.swift index fac863e56f18..e84c3c251882 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tooltip/TooltipSurface.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tooltip/TooltipSurface.swift @@ -20,8 +20,9 @@ public nonisolated struct TooltipSurface: Hashable, Sendable { public static let blurRadius: Double = 24 /// The least tint alpha (pages' fallback without the host's tint: /// `--tooltip-tint`): dense enough that the glass reads as a surface, - /// light enough that the backdrop's color shows. - public static let minimumTintAlpha: Double = 0.86 + /// light enough that the backdrop's color shows. The 4.5:1 rule decides + /// above it (about 0.7 dark, 0.6 light; Leo 2026-10-18: tooltips were too opaque at 0.86). + public static let minimumTintAlpha: Double = 0.6 /// The shadow: elevation 1, small and tight (`--elevation-1` in pages). @MainActor public static var shadow: Elevation { PopupStyle.elevation1 } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift index 606a1113436a..7ceb52eac720 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift @@ -60,7 +60,7 @@ public nonisolated enum ChromeTunables { default: 0.14, range: 0...0.6, step: 0.01, unit: .fraction, code: "ChromeTunables.opaqueOverlayLift") public static let glassOverlayTintStrength = Tunable.number( "glass.overlayTintStrength", .glass, "Overlay tint strength", help: "Multiplier on the theme glass tint's alpha for overlay surfaces (palette, hover cards, browser bars, drop overlay).", - default: 1, range: 0...3, step: 0.05, unit: .multiplier, code: "ChromeTunables.glassOverlayTintStrength") + default: 1.25, range: 0...3, step: 0.05, unit: .multiplier, code: "ChromeTunables.glassOverlayTintStrength") // MARK: Scroll fade diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/PopupSurfaceTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/PopupSurfaceTests.swift index 96ed53915988..ba6acd86452d 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/PopupSurfaceTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/PopupSurfaceTests.swift @@ -3,17 +3,27 @@ import CmuxTheme @testable import CmuxNextDesign import Testing -/// Leo, 2026-10-12: popovers, hover cards, the palette and the find bars were glass, so the text -/// under them showed through and read as text on text. Every overlay that carries text is the -/// one solid popup surface (the theme's opaque overlay fill), whatever this Mac's transparency -/// setting, and pages get the same color (`--cmux-popup-surface`) for their menus and tooltips. +/// Leo, 2026-10-18: overlays that carry text (popovers, hover cards, the palette, the find bars) +/// are themed Liquid Glass, like iOS's menus, replacing the solid surface of 2026-10-12. Under +/// Reduce Transparency they are the one solid popup surface (the theme's opaque overlay fill), and +/// pages get the same color (`--cmux-popup-surface`) for their menus and tooltips. @MainActor @Suite(.serialized) struct PopupSurfaceTests { /// A see-through theme (60% background opacity): the window under a popup is translucent, the /// popup surface must not be. private let input = ThemeInput(background: ThemeRGB(hex: 0x2B1650), foreground: ThemeRGB(hex: 0xF4E3FF), backgroundOpacity: 0.6) - @Test func overlayPanelsAreTheOpaquePopupSurface() throws { + @Test func overlayPanelsFollowThisMacsMaterial() { + let panel = Glass.makeOverlayPanel(content: NSTextField(labelWithString: "Find")) + #expect(panel.materialOverride == nil) + #expect(panel.material == OverlayMaterial.select(liquidGlassAvailable: Glass.isLiquidGlassAvailable, + reduceTransparency: ReduceTransparency.shared.isEnabled)) + } + + @Test func overlayPanelsAreTheOpaquePopupSurfaceUnderReduceTransparency() throws { + let pinned = ReduceTransparency.shared.override + ReduceTransparency.shared.override = true + defer { ReduceTransparency.shared.override = pinned } let scope = ThemeScope(level: .room) scope.setOverride(try #require(ThemeSpec("Stress Purple")), input: input, animated: false) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 400, height: 300), styleMask: [.titled], diff --git a/webviews/src/pages/shared/desktop.css b/webviews/src/pages/shared/desktop.css index 0fd3c322d70e..e60c5b5dac08 100644 --- a/webviews/src/pages/shared/desktop.css +++ b/webviews/src/pages/shared/desktop.css @@ -355,10 +355,10 @@ input[type="submit"] { /* TOOLTIP SURFACE (CmuxNextDesign/Tooltip/TooltipSurface.swift; tooltip-surface.test.ts holds the two in step). Frosted glass: the host's tint (--cmux-tooltip-tint: the popup surface at the least alpha that keeps the text 4.5:1 over pure black and pure white, whatever the blur - averages), else the popup surface at 86%; a 24px blur; the separator rim one device pixel + averages), else the popup surface at 60%; a 24px blur; the separator rim one device pixel (--cmux-hairline); elevation 1. It fades in with the popup open and out with the popup close (the native fadeIn and fadeOut tokens), rising 2px. Reduce Transparency: the opaque popup surface. */ - --tooltip-tint: var(--cmux-tooltip-tint, color-mix(in srgb, var(--surface-popup) 86%, transparent)); + --tooltip-tint: var(--cmux-tooltip-tint, color-mix(in srgb, var(--surface-popup) 60%, transparent)); --tooltip-blur: blur(24px) saturate(1.6); --tooltip-rim-width: var(--cmux-hairline); --tooltip-shadow: var(--elevation-1); From 945e1ba82a3c40bd85d3bf4461dbbdb7a6b1e86a Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 18 Oct 2026 14:38:03 -0700 Subject: [PATCH 2/5] cmux-next media: the glass tour opens the command palette over the art Co-Authored-By: Claude Opus 5.5 --- scripts/cmux-next/media/tours/glass.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/cmux-next/media/tours/glass.json b/scripts/cmux-next/media/tours/glass.json index fa5938e65139..a4f3b79c31cb 100644 --- a/scripts/cmux-next/media/tours/glass.json +++ b/scripts/cmux-next/media/tours/glass.json @@ -12,6 +12,8 @@ {"title": "Split right", "cmux": ["action", "run", "splitRight"]}, {"title": "Agent chat beside the terminal", "cmux": ["agent", "new-chat"], "wait": 4, "optional": "agent chat views are not ported yet"}, + {"title": "Command palette on glass over art", "cmux": ["action", "run", "commandPalette"], "wait": 2}, + {"title": "Close the palette", "cmux": ["action", "run", "commandPalette"], "wait": 1}, {"title": "Soft Blue over art", "cmux": ["settings", "set", "appearance.theme", "Soft Blue"], "wait": 3}, {"title": "Soft Blue Light over art", "cmux": ["settings", "set", "appearance.theme", "Soft Blue Light"], "wait": 3}, {"title": "Clay Orange Light over art", "cmux": ["settings", "set", "appearance.theme", "Clay Orange Light"], "wait": 3}, From 2c05e3a938aa6b14dea410e68400fbfea8a533dc Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sun, 18 Oct 2026 14:47:45 -0700 Subject: [PATCH 3/5] cmux-next: overlay rebuild keeps added views on top and the focused field A Reduce Transparency switch rebuilt the material view over the floating chat grip and dropped a focused field in the panel. The dialog contrast tests check the solid card, so they pin Reduce Transparency; docs that still said every text overlay is solid now say it follows the material. Co-Authored-By: Claude Opus 5.5 --- .../CmuxNextDesign/OverlaySurface.swift | 9 +++++++-- .../Views/SidebarUpdateNotesPanel.swift | 2 +- .../PopupSurfaceCallersTests.swift | 7 ++++--- .../CmuxDialogLightContrastTests.swift | 18 +++++++++++++++++- .../spec-proposals/visuals/design-tokens.json | 2 +- .../spec-proposals/visuals/design-tokens.md | 2 +- webviews/src/ui/README.md | 5 +++-- 7 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift index e47cfad7516e..67991ab72ea8 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift @@ -82,6 +82,8 @@ public final class OverlaySurfaceView: NSView { } private func rebuild() { + // Moving the content out of the window resigns a focused field in it; it takes the keys back after. + let focused = (window?.firstResponder as? NSView).flatMap { $0.isDescendant(of: contentView) ? $0 : nil } materialView?.removeFromSuperview() contentView.removeFromSuperview() tintView = nil @@ -121,8 +123,10 @@ public final class OverlaySurfaceView: NSView { tintView?.frame = view.bounds contentView.frame = view.bounds } - addSubview(view) + // Below any view a caller added on the surface itself (the floating chat's grip). + addSubview(view, positioned: .below, relativeTo: nil) materialView = view + if let focused, let window = focused.window { window.makeFirstResponder(focused) } applyShape() applyTheme() // Content colors may depend on the material (a veil that only @@ -161,7 +165,8 @@ public final class OverlaySurfaceView: NSView { } /// The theme's glass tint, its alpha scaled by the overlay tint - /// strength tunable (1, the default, is the theme tint unchanged). + /// strength tunable (default 1.25, a little denser than the theme + /// tint so text over busy content stays readable; 1 is unchanged). private var overlayTint: NSColor { let tint = Palette.glassTint let strength = ChromeTunables.glassOverlayTintStrength.value diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/SidebarUpdateNotesPanel.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/SidebarUpdateNotesPanel.swift index 3fc8279b3d34..0f1dcae7593d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/SidebarUpdateNotesPanel.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextSidebar/Views/SidebarUpdateNotesPanel.swift @@ -3,7 +3,7 @@ import CmuxNextDesign /// The update card's highlights popup (Leo 2026-10-13): the popup every cmux popover is, not /// AppKit's NSPopover with its stock arrow. A borderless child window of the sidebar's window -/// holding the solid popup surface with its hairline and shadow (`Glass.makeOverlayPanel`, +/// holding the popup surface (themed glass, or the solid card under Reduce Transparency) with its hairline and shadow (`Glass.makeOverlayPanel`, /// `PopupStyle`), beside the card toward the content (left of it for a sidebar on the right, or /// with no room on the right) and top-aligned with it, in the card's theme. It takes clicks /// ("See all changes") and never becomes key, so the keys stay with the window. The app going diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PopupSurfaceCallersTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PopupSurfaceCallersTests.swift index bcc90fc452b9..2e098bc4e650 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PopupSurfaceCallersTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/PopupSurfaceCallersTests.swift @@ -6,9 +6,10 @@ import AppKit import CmuxTheme import Testing -/// Leo, 2026-10-12: every overlay that carries text is the solid popup surface. These had their own -/// glass, vibrancy or a translucent card fill: the Share cmux dialog. (The omnibar's suggestion card -/// has its own color since cx-d0d.102; the refusal pill is a toast now, #19759.) +/// Leo, 2026-10-12: every overlay that carries text was the solid popup surface; on 2026-10-18 the +/// overlay panels went back to this Mac's material (themed glass). The Share cmux dialog keeps the +/// solid surface it pins itself. (The omnibar's suggestion card has its own color since cx-d0d.102; +/// the refusal pill is a toast now, #19759.) @MainActor @Suite(.serialized) struct PopupSurfaceCallersTests { @Test func theShareDialogIsThePopupSurface() { #expect(ShareCmuxView(writeToPasteboard: { _ in }).surface.material == .opaque) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/CmuxDialogLightContrastTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/CmuxDialogLightContrastTests.swift index e25b934fc40b..bdddd6589ea4 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/CmuxDialogLightContrastTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/CmuxDialogLightContrastTests.swift @@ -7,11 +7,19 @@ import Testing /// ink once, while the dialog was built outside any window, so a dialog shown in a light /// window (or one whose theme turned light while it showed) kept another theme's text over the /// light surface. The labels must re-read their ink in the scope they draw in, in an inactive -/// (never key) window too, and the surface must stay a light card, not a grey slab. +/// (never key) window too, and the surface must stay a light card, not a grey slab. These +/// check the solid card a dialog draws under Reduce Transparency; on glass the tint carries it. @MainActor @Suite(.serialized) struct CmuxDialogLightContrastTests { private static let light = ThemeInput(background: ThemeRGB(hex: 0xFFFFFF), foreground: ThemeRGB(hex: 0x1D1D1F)) private static let dark = ThemeInput(background: ThemeRGB(hex: 0x1E1E1E), foreground: ThemeRGB(hex: 0xE6E6E6)) + /// Pins the solid popup surface for one test; returns the setting to restore. + private static func pinOpaque() -> Bool? { + let pinned = ReduceTransparency.shared.override + ReduceTransparency.shared.override = true + return pinned + } + private static func scope(_ input: ThemeInput, _ name: String) throws -> ThemeScope { let scope = ThemeScope(level: .room) scope.setOverride(ThemeSpec(name), input: input, animated: false) @@ -42,6 +50,8 @@ import Testing } @Test func aDialogMovedIntoALightWindowDrawsReadableText() throws { + let pinned = Self.pinOpaque() + defer { ReduceTransparency.shared.override = pinned } let darkWindow = try Self.window(in: try Self.scope(Self.dark, "Contrast Dark")) let lightScope = try Self.scope(Self.light, "Contrast Light") let lightWindow = try Self.window(in: lightScope) @@ -56,6 +66,8 @@ import Testing /// The mirror case: whichever theme the app scope has when the dialog is built, one of the /// two moves lands it in the other one. @Test func aDialogMovedIntoADarkWindowDrawsReadableText() throws { + let pinned = Self.pinOpaque() + defer { ReduceTransparency.shared.override = pinned } let lightWindow = try Self.window(in: try Self.scope(Self.light, "Contrast Light First")) let darkScope = try Self.scope(Self.dark, "Contrast Dark Second") let darkWindow = try Self.window(in: darkScope) @@ -67,6 +79,8 @@ import Testing } @Test func aDialogWhoseThemeTurnsLightDrawsReadableText() throws { + let pinned = Self.pinOpaque() + defer { ReduceTransparency.shared.override = pinned } let scope = try Self.scope(Self.dark, "Contrast Switch") let window = try Self.window(in: scope) defer { window.close() } @@ -78,6 +92,8 @@ import Testing } @Test func aDarkDialogDrawsItsWindowsInk() throws { + let pinned = Self.pinOpaque() + defer { ReduceTransparency.shared.override = pinned } let scope = try Self.scope(Self.dark, "Contrast Dark Only") let window = try Self.window(in: scope) defer { window.close() } diff --git a/plans/cmux-next/spec-proposals/visuals/design-tokens.json b/plans/cmux-next/spec-proposals/visuals/design-tokens.json index 8c4a39fe8176..4e073d87cb94 100644 --- a/plans/cmux-next/spec-proposals/visuals/design-tokens.json +++ b/plans/cmux-next/spec-proposals/visuals/design-tokens.json @@ -2040,7 +2040,7 @@ "source": "Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlayMaterial.swift (ThemeTokens.opaqueOverlayFill); Packages/macOS/CmuxNext/Sources/CmuxNextDesign/OverlaySurface.swift (OverlaySurfaceView.applyTheme); Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift:40-42 (ChromeTunables.opaqueOverlayLift)" }, "glassTintStrength": { - "value": 1, + "value": 1.25, "source": "Packages/macOS/CmuxNext/Sources/CmuxNextDesign/Tunables/ChromeTunables.swift:43-45 (ChromeTunables.glassOverlayTintStrength)", "appliesTo": "OverlaySurface surfaces (materials.overlay.appliesTo). Raw Glass.makePanel panels use token:glassTint unscaled." }, diff --git a/plans/cmux-next/spec-proposals/visuals/design-tokens.md b/plans/cmux-next/spec-proposals/visuals/design-tokens.md index fbe8d1b31b6d..f2700b1cf6a1 100644 --- a/plans/cmux-next/spec-proposals/visuals/design-tokens.md +++ b/plans/cmux-next/spec-proposals/visuals/design-tokens.md @@ -211,7 +211,7 @@ Every listed surface draws through `OverlaySurfaceView`: the drop overlay, drop The "older macOS" column cannot run in the macOS app today: `Packages/macOS/CmuxNext/Package.swift` requires macOS 26. Ports on a platform without Liquid Glass use the vibrancy row where the platform has a blur, else the opaque fallback. -The overlay tint strength Debug tunable (default 1, `ChromeTunables.swift:43-45 (ChromeTunables.glassOverlayTintStrength)`) scales the tint of `OverlaySurface` only. `Glass.makePanel` panels use `glassTint` unscaled. +The overlay tint strength Debug tunable (default 1.25, `ChromeTunables.swift:43-45 (ChromeTunables.glassOverlayTintStrength)`) scales the tint of `OverlaySurface` only. `Glass.makePanel` panels use `glassTint` unscaled. ## 5. Motion diff --git a/webviews/src/ui/README.md b/webviews/src/ui/README.md index 10b3d05765f7..fe10fdf6de4f 100644 --- a/webviews/src/ui/README.md +++ b/webviews/src/ui/README.md @@ -96,9 +96,10 @@ Rules: - Popups, menus, pickers and tooltips render in the portal container (`usePortalContainer`), never inside a component, so no parent's stacking context (`transform`, `filter`, `opacity`, `isolation`) or `overflow` traps or clips them. Inside a component use only base, raised and sticky. -- Menus, popovers, pickers and tooltips are the solid popup surface, `background: var(--surface-popup)` +- Menus, popovers, pickers and tooltips are the popup surface, `background: var(--surface-popup)` (desktop.css: the host's `--cmux-popup-surface`, the native overlays' `Palette.popupSurface`), with - no `backdrop-filter`. A translucent surface that carries text keeps at least + no `backdrop-filter` (a page cannot blur the native glass behind it; native overlays are themed glass, + solid under Reduce Transparency). A translucent surface that carries text keeps at least `--surface-text-min` of its fill: `color-mix(in srgb, var(--surface-text-min), transparent)`. - A blur uses `backdrop-filter: var(--surface-blur)`, or the file has its own `@media (prefers-reduced-transparency: reduce)` rule that makes the surface opaque. Both tokens turn From fcb8623b17ca3315943edcdfe582dac16e9dadff Mon Sep 17 00:00:00 2001 From: "cmux-autofix[bot]" Date: Mon, 19 Oct 2026 06:38:34 +0000 Subject: [PATCH 4/5] chore(next): PR autofix at 0e0808920c45 - vp fmt webviews/src/agent-session/acpmux/replyHref.ts - vp fmt webviews/src/pages/markdown/README.md - vp fmt webviews/src/pages/markdown/links.ts - vp fmt webviews/test/markdown-cmux-app-links.test.ts Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39210210960 --- webviews/src/agent-session/acpmux/replyHref.ts | 5 ++++- webviews/src/pages/markdown/README.md | 10 +++++++++- webviews/src/pages/markdown/links.ts | 3 ++- webviews/test/markdown-cmux-app-links.test.ts | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/webviews/src/agent-session/acpmux/replyHref.ts b/webviews/src/agent-session/acpmux/replyHref.ts index 2f822170bc68..afa44e277099 100644 --- a/webviews/src/agent-session/acpmux/replyHref.ts +++ b/webviews/src/agent-session/acpmux/replyHref.ts @@ -23,7 +23,10 @@ export function safeHref(href: string): string | undefined { /// (`javascript:`, `file:`, `data:`, `ssh:`...) as text. export function appHref(href: string): string | undefined { const text = href.trim(); - if (!/^cmux(-[a-z0-9-]+)?:\/\/(workspace|pane|tab|session|chief)\/[A-Za-z0-9._~\-/]+(#[A-Za-z0-9._~\-]+)?$/.test(text)) return undefined; + if ( + !/^cmux(-[a-z0-9-]+)?:\/\/(workspace|pane|tab|session|chief)\/[A-Za-z0-9._~\-/]+(#[A-Za-z0-9._~\-]+)?$/.test(text) + ) + return undefined; let url: URL; try { url = new URL(text); diff --git a/webviews/src/pages/markdown/README.md b/webviews/src/pages/markdown/README.md index 66e9693b58e3..7b58868aaa5c 100644 --- a/webviews/src/pages/markdown/README.md +++ b/webviews/src/pages/markdown/README.md @@ -10,6 +10,14 @@ A save is a minimal-change write (`sourceMap.ts`). The editor keeps each top-lev `host.ts` lists the ops and streams the native host (diff-host.md S6) implements: `cmux.markdown.config`, `cmux.markdown.save {path, text, baseHash}` (refused with `cmux.markdown.conflict` on a hash mismatch), `cmux.markdown.open` (also for followed links), `cmux.markdown.openLink`, `cmux.markdown.resolveLinks`, `cmux.markdown.listFiles`, the page commands `save`, `back`, `forward` and `link`, the streams `cmux.markdown.changes` and `cmux.markdown.look`, the `save` page command (Cmd-S), and same-origin resources for images (`assetBase`, the file's folder only) and the diagram libraries (`libBase`: `mermaid.js`, `vega.js`). The page runs under the strict PageCSP: Vega uses `vega-interpreter`, code highlighting the Shiki JavaScript engine. +## Editable everywhere + +Every markdown file opens editable, whoever opened it (Open File..., `cmux open`, an agent, a link) and in any workspace, Home included. The host sends `readOnly` with a `readOnlyReason` only for a real reason, and the page names it: `encoding` (not UTF-8), `binary`, `permission` (the file is not writable) or `transcript` (a chat transcript the host opens read only). A change on disk reloads the page when it has no unsaved edits and raises the conflict banner when it has. + +## Text size + +Cmd-= (also Cmd-Shift-=, Cmd-+) and Cmd-- are the app key dispatcher's `zoomIn` and `zoomOut` page commands: one pixel per step, 8 to 40, in rich and source mode. Cmd-0 (`zoomReset`) goes back to the default, `markdown.font.size` (Settings > Appearance > Markdown Viewer > Default Font Size, 16 when unset). The page applies the size as `--cmux-md-zoom` (size over the default) on the body and code sizes (`fontZoom.ts`); a viewer keeps its size across a page reload, and a viewer at the default follows a new default. + ## Links A plain click on a link places the caret; Cmd-click follows it (the cursor turns into a pointer while Cmd is held), and in a read-only file a click follows. Hovering shows a card with the full URL or the resolved path, how the link opens, and "Not found" for a relative target that does not exist (also marked with a wavy underline). Where a followed link goes (`linkRouter.ts`): @@ -46,7 +54,7 @@ Lengths given as numbers are pixels (`em` where noted); strings are CSS values. | `markdown.defaultMode` | `"rich"` | The mode a file opens in: `"rich"` or `"source"`. | | `markdown.toolbar` | `true` | Show the toolbar (file, save status, mode switch). | | `markdown.font.family` | system UI font | `--cmux-md-font-family` | -| `markdown.font.size` | `16` | `--cmux-md-font-size` | +| `markdown.font.size` | `16` | `--cmux-md-font-size`; the default text size (Settings row; Cmd-0 returns to it) | | `markdown.font.weight` | `400` | `--cmux-md-font-weight` | | `markdown.font.lineHeight` | `1.6` | `--cmux-md-line-height` | | `markdown.headings.family` | the body font | `--cmux-md-heading-font-family` | diff --git a/webviews/src/pages/markdown/links.ts b/webviews/src/pages/markdown/links.ts index 50533601fc94..67d5f3990243 100644 --- a/webviews/src/pages/markdown/links.ts +++ b/webviews/src/pages/markdown/links.ts @@ -16,7 +16,8 @@ export interface ParsedLink { /** `cmux://workspace/ws_…`, `pane/…`, `tab/…`, `session/…`, `chief/…` in the `cmux` scheme or a build's * `cmux-…` one; the host checks the ids again (DeepLink.parse) and opens nothing it does not know. */ -const APP_LINK = /^cmux(-[a-z0-9-]+)?:\/\/(workspace|pane|tab|session|chief)\/[A-Za-z0-9._~\-/]+(#[A-Za-z0-9._~\-]+)?$/i; +const APP_LINK = + /^cmux(-[a-z0-9-]+)?:\/\/(workspace|pane|tab|session|chief)\/[A-Za-z0-9._~\-/]+(#[A-Za-z0-9._~\-]+)?$/i; const MARKDOWN_PATH = /\.(md|markdown|mdx|mdown|mkd)$/i; diff --git a/webviews/test/markdown-cmux-app-links.test.ts b/webviews/test/markdown-cmux-app-links.test.ts index bc3bc8b95204..77954962fcc7 100644 --- a/webviews/test/markdown-cmux-app-links.test.ts +++ b/webviews/test/markdown-cmux-app-links.test.ts @@ -38,7 +38,11 @@ test("following a cmux app link on the Markdown page asks the host to open it as const calls: { op: string; params: unknown }[] = []; const client = { call: async (op: string, params: unknown) => (calls.push({ op, params }), {}) }; const router = new LinkRouter({ - store: { getState: () => ({ config: { path: "/Users/me/notes.md" } }), navigate: async () => null, go: async () => null } as never, + store: { + getState: () => ({ config: { path: "/Users/me/notes.md" } }), + navigate: async () => null, + go: async () => null, + } as never, client: client as never, resolver: { get: () => undefined }, scrollToAnchor: () => false, From 2a0ab72dc1659461801d7b34baaa2b5e6685340c Mon Sep 17 00:00:00 2001 From: "cmux-autofix[bot]" Date: Mon, 19 Oct 2026 07:02:49 +0000 Subject: [PATCH 5/5] chore(next): PR autofix at 3449459afd7f - regenerated Packages/macOS/CmuxNext/ci-target-graph.json - vp fmt webviews/src/pages/markdown/README.md Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/39212629328 --- Packages/macOS/CmuxNext/ci-target-graph.json | 2 ++ webviews/src/pages/markdown/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Packages/macOS/CmuxNext/ci-target-graph.json b/Packages/macOS/CmuxNext/ci-target-graph.json index 9f595a3d6c22..a00292f34374 100644 --- a/Packages/macOS/CmuxNext/ci-target-graph.json +++ b/Packages/macOS/CmuxNext/ci-target-graph.json @@ -215,6 +215,7 @@ "CmuxNextTasks", "CmuxNextTerminal", "CmuxNextTerminalFind", + "CmuxNextTerminalGeometry", "CmuxNextUpdater", "CmuxNextWakeups", "CmuxNextWorkspaceNaming" @@ -269,6 +270,7 @@ "CmuxNextRemoteLocalhostService", "CmuxNextTabs", "CmuxNextTasks", + "CmuxNextTerminalGeometry", "CmuxNextWakeups" ], "packages": [ diff --git a/webviews/src/pages/markdown/README.md b/webviews/src/pages/markdown/README.md index 07f48930937d..7b58868aaa5c 100644 --- a/webviews/src/pages/markdown/README.md +++ b/webviews/src/pages/markdown/README.md @@ -54,7 +54,7 @@ Lengths given as numbers are pixels (`em` where noted); strings are CSS values. | `markdown.defaultMode` | `"rich"` | The mode a file opens in: `"rich"` or `"source"`. | | `markdown.toolbar` | `true` | Show the toolbar (file, save status, mode switch). | | `markdown.font.family` | system UI font | `--cmux-md-font-family` | -| `markdown.font.size` | `16` | `--cmux-md-font-size`; the default text size (Settings row; Cmd-0 returns to it) | +| `markdown.font.size` | `16` | `--cmux-md-font-size`; the default text size (Settings row; Cmd-0 returns to it) | | `markdown.font.weight` | `400` | `--cmux-md-font-weight` | | `markdown.font.lineHeight` | `1.6` | `--cmux-md-line-height` | | `markdown.headings.family` | the body font | `--cmux-md-heading-font-family` |