Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b1a75b2
plans(cmux-next): liquid glass lens design
teamleaderleo Oct 21, 2026
1b8eabf
feat(cmux-next): liquid glass lens primitive and Glass setting
teamleaderleo Oct 21, 2026
447af4e
Merge remote-tracking branch 'origin/feat-cmux-next' into cmux/glass-…
teamleaderleo Oct 21, 2026
a885dbe
fix(cmux-next glass): review fixes for the lens and backdrop feed
teamleaderleo Oct 21, 2026
9155499
Merge remote-tracking branch 'origin/feat-cmux-next' into cmux/glass-…
teamleaderleo Oct 21, 2026
c8fb79f
fix(cmux-next glass): lens stays inert in a DOM without observers
teamleaderleo Oct 21, 2026
c9b3b7f
Merge remote-tracking branch 'origin/feat-cmux-next' into cmux/glass-…
teamleaderleo Oct 21, 2026
55af381
Merge remote-tracking branch 'origin/feat-cmux-next' into cmux/glass-…
teamleaderleo Oct 21, 2026
db2750f
chore(next): PR autofix (settings exports for appearance.glass)
teamleaderleo Oct 21, 2026
d8cc183
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
b20853a
Merge remote-tracking branch 'origin/feat-cmux-next' into cmux/glass-…
teamleaderleo Oct 21, 2026
a6850aa
Merge remote-tracking branch 'origin/feat-cmux-next' into cmux/glass-…
teamleaderleo Oct 21, 2026
1d24ff1
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
253cc7d
chore(next): generated settings exports follow the base
teamleaderleo Oct 21, 2026
09633d8
fix(cmux-next glass): the tint's mix source passes the never-opaque c…
teamleaderleo Oct 21, 2026
94f45ef
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
c881e0b
test(cmux-next glass): no void operator in the Linux WebKit skip
teamleaderleo Oct 21, 2026
89d6428
fix(cmux-next glass): WebGL1 lens reads an explicit mip level where i…
teamleaderleo Oct 21, 2026
d1e868c
fix(cmux-next glass): the lens engine loads after the page, out of it…
teamleaderleo Oct 21, 2026
9e6fe02
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
b734bf1
chore(next): settings exports regenerated for the Glass setting
teamleaderleo Oct 21, 2026
5159222
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
6126d33
chore(next): settings exports regenerated for the Glass setting after…
teamleaderleo Oct 21, 2026
89078c9
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
800de14
chore(next): settings exports regenerated for the Glass setting after…
teamleaderleo Oct 21, 2026
713737a
cmux-next glass: the lens is an opt-in toggle; Plain Glass stays the …
teamleaderleo Oct 21, 2026
f3edbc1
feat(next): Liquid Glass lens is a Debug toggle, off by default
teamleaderleo Oct 21, 2026
2cf3a2a
chore(next): action surfaces regenerated for the Debug Liquid Glass t…
teamleaderleo Oct 21, 2026
6acb984
Merge remote-tracking branch 'origin/feat-cmux-next' into tmp/glass-m…
teamleaderleo Oct 21, 2026
048ea3c
chore(next): action surfaces regenerated after the base merge
teamleaderleo Oct 21, 2026
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 @@ -8647,6 +8647,137 @@
}
}
},
"action.appearance.glass.lens.toggle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Toggle Liquid Glass Lens"
}
},
"ja": {
"stringUnit": {
"state": "needs_review",
"value": "リキッドグラスのレンズを切り替え"
}
},
"ar": {
"stringUnit": {
"state": "needs_review",
"value": "تبديل عدسة الزجاج السائل"
}
},
"bs": {
"stringUnit": {
"state": "needs_review",
"value": "Uključi/isključi sočivo tečnog stakla"
}
},
"da": {
"stringUnit": {
"state": "needs_review",
"value": "Slå flydende glas-linse til/fra"
}
},
"de": {
"stringUnit": {
"state": "needs_review",
"value": "Liquid-Glass-Linse umschalten"
}
},
"es": {
"stringUnit": {
"state": "needs_review",
"value": "Activar o desactivar la lente de cristal líquido"
}
},
"fr": {
"stringUnit": {
"state": "needs_review",
"value": "Activer/désactiver la lentille de verre liquide"
}
},
"it": {
"stringUnit": {
"state": "needs_review",
"value": "Attiva/disattiva lente in vetro liquido"
}
},
"km": {
"stringUnit": {
"state": "needs_review",
"value": "បិទ/បើកកញ្ចក់កែវរាវ"
}
},
"ko": {
"stringUnit": {
"state": "needs_review",
"value": "리퀴드 글래스 렌즈 켜기/끄기"
}
},
"nb": {
"stringUnit": {
"state": "needs_review",
"value": "Slå flytende glass-linse av/på"
}
},
"pl": {
"stringUnit": {
"state": "needs_review",
"value": "Przełącz soczewkę płynnego szkła"
}
},
"pt-BR": {
"stringUnit": {
"state": "needs_review",
"value": "Alternar Lente de Vidro Líquido"
}
},
"ru": {
"stringUnit": {
"state": "needs_review",
"value": "Переключить линзу жидкого стекла"
}
},
"th": {
"stringUnit": {
"state": "needs_review",
"value": "สลับเลนส์กระจกเหลว"
}
},
"tr": {
"stringUnit": {
"state": "needs_review",
"value": "Sıvı Cam Merceğini Aç/Kapat"
}
},
"uk": {
"stringUnit": {
"state": "needs_review",
"value": "Перемкнути лінзу рідкого скла"
}
},
"vi": {
"stringUnit": {
"state": "needs_review",
"value": "Bật/tắt thấu kính kính lỏng"
}
},
"zh-Hans": {
"stringUnit": {
"state": "needs_review",
"value": "切换液态玻璃透镜"
}
},
"zh-Hant": {
"stringUnit": {
"state": "needs_review",
"value": "切換液態玻璃透鏡"
}
}
}
},
"action.attachTextBoxFile": {
"extractionState": "manual",
"localizations": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,16 @@ nonisolated enum SettingsActionCatalog: ActionCatalogGroup {
keywords: ["titlebar", "title bar", "window", "workspace name", "show", "appearance"], category: .settings,
symbol: "macwindow.badge.plus", surfaces: [.palette], cliName: "settings use-standard-titlebar"
),
// Leo 2026-10-21: the lens is a toggle to see and feel before it is the default. Flips the
// Debug choice `glass.lens` between Plain Glass (the default) and Lens, live, from the
// palette or the Debug menu (DEV and NIGHTLY).
ActionDescriptor(
id: "appearance.glass.lens.toggle",
title: String(localized: "action.appearance.glass.lens.toggle", defaultValue: "Toggle Liquid Glass Lens", bundle: .module),
keywords: ["glass", "liquid glass", "lens", "refraction", "wallpaper", "plain glass", "appearance"],
category: .settings, symbol: "drop", surfaces: [.palette, .menu], mainMenu: .debug, isDebugOnly: true,
surfacePlan: ActionSurfacePlan(cli: .exempt(.devOnly), contextMenuExemption: .noObject)
),
ActionDescriptor(
id: "appearance.customize",
title: String(localized: "action.appearance.customize", defaultValue: "Customize Appearance…", bundle: .module),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ public final class AgentActivityHostView: NSView {
/// Activity events can arrive as a batch, and serializing the full projection for every
/// element makes the main actor and WebKit do redundant work while the page is catching up.
private var statePushScheduled = false
/// The wallpaper still the page's glass lenses (`cmuxTheme.backdrop`), pushed after each theme.
private lazy var glassFeed = GlassBackdropFeed(host: webView, zoom: { [webView] in webView.pageZoom * webView.magnification }) { [weak self] in
self?.webView.evaluateJavaScript($0, completionHandler: nil)
}
/// Pushes the theme again on every repaint of this view's scope (the glass mode included).
private lazy var themeFollower = ThemeFollower(self) { [weak self] in self?.applyTheme() }

/// Makes an Activity web view for a model and its native data source.
public init(model: AgentActivityModel, source: any AgentActivitySource) {
Expand Down Expand Up @@ -75,11 +81,15 @@ public final class AgentActivityHostView: NSView {
/// Gives the page this view's scope colors when they changed; `reload` after a navigation,
/// whose new document has only the document-start colors.
fileprivate func applyTheme(reload: Bool = false) {
themeFollower.rebind()
if reload { appliedTheme = nil }
let theme = Self.theme(themeScope)
guard theme.payloadJSON != appliedTheme else { return }
appliedTheme = theme.payloadJSON
webView.evaluateJavaScript(theme.applyScript, completionHandler: nil)
if theme.payloadJSON != appliedTheme {
appliedTheme = theme.payloadJSON
webView.evaluateJavaScript(theme.applyScript, completionHandler: nil)
}
// After the theme, which sets `data-glass`; a reload is a new document with no still yet.
glassFeed.refresh(newDocument: reload)
}

private static func theme(_ scope: ThemeScope) -> WebTheme {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ public final class AgentPaneView: NSView {
/// Pushes every color change of this view's scope to the page, shown or hidden, so a page
/// shown after a change paints its first frame in the new colors (cx-8w027.12).
private lazy var themeFollower = ThemeFollower(self) { [weak self] in self?.applyTheme() }
/// The wallpaper still the old host's page lenses (the page host's ``PageWebView`` has its own).
private lazy var glassFeed = GlassBackdropFeed(host: webView, zoom: { [webView] in webView.pageZoom * webView.magnification }) { [weak self] in
self?.evaluateScript($0)
}
private var uiScaleObservation: Task<Void, Never>?
public var zoom: Double = 1 {
didSet { guard zoom != oldValue else { return }; applyZoom() }
Expand Down Expand Up @@ -280,6 +284,7 @@ public final class AgentPaneView: NSView {
return
}
webView.pageZoom = Double(DesignSettings.shared.uiScale) * zoom
glassFeed.refresh() // the zoom moves the art's rect in CSS px
webView.evaluateJavaScript(DesignSettings.shared.interfaceScaleScript, completionHandler: nil)
}

Expand Down Expand Up @@ -427,9 +432,12 @@ public final class AgentPaneView: NSView {
deliveredThemeEvent = event
pageEvents.publish(event)
} else {
guard let script = AgentPaneTheme.script(tokens, surface: surface), force || script != deliveredThemeScript else { return }
deliveredThemeScript = script
evaluateScript(script)
if let script = AgentPaneTheme.script(tokens, surface: surface), force || script != deliveredThemeScript {
deliveredThemeScript = script
evaluateScript(script)
}
// After the theme, which sets `data-glass`; a forced push is a new document or its handshake.
glassFeed.refresh(newDocument: force)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// After two quick unexpected ends in a row, Chromium starts only
// when the user reloads a browser tab.
services.observeBorders()
services.observeGlass()
services.updateFeedObservation = UpdateFeedPosting.follow(services)
// The runtime pack check wires the engine reason at once; its download waits until asked.
services.startChromiumRuntime()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import CmuxNextDesign

extension AppServices {
/// Repaints every window when the glass mode pages draw changes (the Debug choice `glass.lens`, Reduce
/// Transparency, Low Power Mode): every web view pushes its theme (`data-glass`) and its
/// wallpaper still again (plans/cmux-next/glass-lens-DESIGN.md).
func observeGlass() {
glassWatch = GlassModeWatch { ThemeStore.shared.repaintAll() }
}
}
2 changes: 2 additions & 0 deletions Packages/macOS/CmuxNext/Sources/CmuxNextApp/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ final class AppServices {
var chromiumLikelyObservations: [Task<Void, Never>] = []
/// Repaints on `appearance.borders` changes (`observeBorders`).
var borderObservation: Task<Void, Never>?
/// Repaints when the glass mode pages draw changes (`observeGlass`).
var glassWatch: GlassModeWatch?
/// Posts this launch's installed-update Feed card once (`UpdateFeedPosting.follow`).
var updateFeedObservation: Task<Void, Never>?
/// Page menus and the open-menu diagnostic shared by browser hosts.
Expand Down
44 changes: 44 additions & 0 deletions Packages/macOS/CmuxNext/Sources/CmuxNextApp/GlassModeWatch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import AppKit
import CmuxNextCompat
import CmuxNextDesign

/// Calls back when the glass mode pages draw (`GlassLensMode.current`) changes: the Debug choice `glass.lens`,
/// Reduce Transparency (system or Debug override) or Low Power Mode. Events only, nothing polls.
final class GlassModeWatch {
private var shown: GlassLensMode
private let onChange: @MainActor () -> Void
private var settings: Task<Void, Never>?
private var lowPower: LowPowerModeObservation?
private var display: (any NSObjectProtocol)?

init(onChange: @escaping @MainActor () -> Void) {
shown = .current
self.onChange = onChange
settings = Task { [weak self] in
for await _ in ObservationStream({ GlassLensMode.tunable.value }) { self?.check() }
}
lowPower = LowPowerMode.system.observe { [weak self] _ in self?.check() }
display = NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.accessibilityDisplayOptionsDidChangeNotification, object: nil, queue: .main) { [weak self] _ in
MainActor.assumeIsolated { self?.check() } // main-proof: observer on queue: .main
}
}

// cx-t7hu: not an isolated deinit; its runtime hop traps when released inside an action's task-local scope.
nonisolated deinit {
guard Thread.isMainThread else { return } // main-proof: returns off the main thread
MainActor.assumeIsolated {
settings?.cancel()
lowPower?.cancel()
if let display { NSWorkspace.shared.notificationCenter.removeObserver(display) }
}
}

/// Calls back when the mode is not the one last seen.
func check() {
let now = GlassLensMode.current
guard now != shown else { return }
shown = now
onChange()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ enum AppearanceHandlers {
for style in TitlebarStyle.allCases {
registry.bind(ActionID(rawValue: "appearance.titlebar.\(style.rawValue)"), run: { _ in try setTitlebar(style, context) })
}
registry.bind("appearance.glass.lens.toggle", run: { _ in GlassLensMode.choose(GlassLensMode.tunable.value.lensToggled) })
registry.bind("appearance.interfaceSize.reset", run: { _ in
let path = context.design.interfaceSizePath; try requireUnmanaged(path, context); context.design.resetInterfaceSize()
context.writeSetting("reset interface size", path, nil, reloadOnFailure: true)
Expand Down
Loading