From ecb9247ece6f2ada892e564b1fb193b6a3bcf620 Mon Sep 17 00:00:00 2001 From: Joshua Van Deren Date: Sun, 2 Aug 2026 13:34:51 -0600 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9E=20Prevent=20Chromium=20PWA=20s?= =?UTF-8?q?hims=20from=20terminating=20on=20snap=20(#1131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Isolate a removable AX resize workaround for Chromium app-mode PWAs so Loop can snap them without triggering the shim's NSInvalidArgumentException / channel-error quit path. Co-authored-by: Cursor --- .../ChromiumPWAResizeWorkaround.swift | 68 +++++++++++++++++++ .../Window Manipulation/WindowEngine.swift | 10 ++- Loop/Window Management/Window/Window.swift | 3 +- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift diff --git a/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift b/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift new file mode 100644 index 00000000..f17ec9bc --- /dev/null +++ b/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift @@ -0,0 +1,68 @@ +// +// ChromiumPWAResizeWorkaround.swift +// Loop +// +// Temporary workaround for https://github.com/mrkai77/Loop/issues/1131 +// +// Chromium-installed PWA shims (`{browser}.app.{id}`) can terminate during Loop’s +// normal AX resize path: +// NSAccessibility Request Received → NSInvalidArgumentException → +// app_shim_controller Channel error → clean terminate. +// +// TO REMOVE when Chromium fixes this: +// 1. Delete this file. +// 2. Delete every call site that references `ChromiumPWAResizeWorkaround` +// (WindowEngine.performResize, WindowEngine.resizeWindow, +// Window.ResolvedProperties.init). +// + +import Foundation + +enum ChromiumPWAResizeWorkaround { + /// Bundle ID bases for Chromium browsers that install macOS PWA shims as + /// `{base}.app.{id}` (optionally `{base}.app.{profile}-{id}`). + private static let browserBundleIDBases: [String] = [ + "com.google.Chrome", + "com.google.Chrome.beta", + "com.google.Chrome.canary", + "com.google.Chrome.dev", + "com.brave.Browser", + "com.brave.Browser.beta", + "com.brave.Browser.nightly", + "com.microsoft.Edge", + "com.microsoft.Edge.Beta", + "com.microsoft.Edge.Dev", + "com.microsoft.Edge.Canary", + "org.chromium.Chromium", + "company.thebrowser.Browser" + ] + + static func applies(to window: Window) -> Bool { + applies(bundleIdentifier: window.nsRunningApplication?.bundleIdentifier) + } + + /// `true` for Chromium PWA / app-mode shims only — not the browser itself. + static func applies(bundleIdentifier: String?) -> Bool { + guard let bundleIdentifier else { return false } + return browserBundleIDBases.contains { bundleIdentifier.hasPrefix($0 + ".app.") } + } + + /// Focus/raise has been observed to contribute to shim termination (#1131). + static func shouldSkipFocus(for window: Window) -> Bool { + applies(to: window) + } + + /// Skip reading/toggling `AXEnhancedUserInterface` for matching windows (#1131). + static func resolvedEnhancedUserInterface(for window: Window) -> Bool { + applies(to: window) ? false : window.enhancedUserInterface + } + + /// Size → position → size without touching `AXEnhancedUserInterface`. + /// The second size write is needed because shims often ignore the first until + /// the origin has been updated. + static func resize(_ window: Window, to targetFrame: CGRect) { + window.setSize(targetFrame.size) + window.setPosition(targetFrame.origin) + window.setSize(targetFrame.size) + } +} diff --git a/Loop/Window Management/Window Manipulation/WindowEngine.swift b/Loop/Window Management/Window Manipulation/WindowEngine.swift index 5d44ac50..9ca03bac 100644 --- a/Loop/Window Management/Window Manipulation/WindowEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowEngine.swift @@ -60,7 +60,10 @@ enum WindowEngine { false } - if Defaults[.focusWindowOnResize] || useSystemWM { + // ChromiumPWAResizeWorkaround (#1131) — remove with that type. + let shouldFocus = useSystemWM + || (Defaults[.focusWindowOnResize] && !ChromiumPWAResizeWorkaround.shouldSkipFocus(for: window)) + if shouldFocus { await window.focus() } @@ -192,6 +195,11 @@ enum WindowEngine { if animate { try await window.setFrameAnimated(targetFrame, bounds: bounds, resolvedProperties: resolvedProperties) actualFrame = window.frame + } else if ChromiumPWAResizeWorkaround.applies(to: window) { + // ChromiumPWAResizeWorkaround (#1131) — remove with that type. + ChromiumPWAResizeWorkaround.resize(window, to: targetFrame) + try Task.checkCancellation() + actualFrame = window.frame } else { await window.setFrame(targetFrame, sizeFirst: willChangeScreens, resolvedProperties: resolvedProperties) try Task.checkCancellation() diff --git a/Loop/Window Management/Window/Window.swift b/Loop/Window Management/Window/Window.swift index 8123de83..91261950 100644 --- a/Loop/Window Management/Window/Window.swift +++ b/Loop/Window Management/Window/Window.swift @@ -615,7 +615,8 @@ extension Window { self.frame = window.frame // 2 AX calls (position + size) self.isResizable = window.isResizable // 1 AX call self.isFullscreen = window.fullscreen // 1 AX call - self.isEnhancedUserInterface = window.enhancedUserInterface // 1 AX call on app element + // ChromiumPWAResizeWorkaround (#1131) — remove with that type. + self.isEnhancedUserInterface = ChromiumPWAResizeWorkaround.resolvedEnhancedUserInterface(for: window) } /// Creates a new snapshot with an updated frame, preserving stable properties. From 61f5b114d53e51b0d9defdaa406ada90f6edf3b7 Mon Sep 17 00:00:00 2001 From: Joshua Van Deren Date: Sun, 2 Aug 2026 13:56:25 -0600 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9D=20Link=20Chromium=20upstream?= =?UTF-8?q?=20issues=20on=20PWA=20resize=20workaround?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Point reviewers at the public duplicate and restricted canonical bugs so the workaround can be removed once Chromium ships a fix. Co-authored-by: Cursor --- .../Window Manipulation/ChromiumPWAResizeWorkaround.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift b/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift index f17ec9bc..9d677ace 100644 --- a/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift +++ b/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift @@ -9,6 +9,10 @@ // NSAccessibility Request Received → NSInvalidArgumentException → // app_shim_controller Channel error → clean terminate. // +// Upstream: +// - Public (dup): https://issues.chromium.org/issues/539984770 +// - Canonical (restricted): https://issues.chromium.org/issues/537448007 +// // TO REMOVE when Chromium fixes this: // 1. Delete this file. // 2. Delete every call site that references `ChromiumPWAResizeWorkaround` From 08747356bda913f2475b711bf35baa1ebd78aa1c Mon Sep 17 00:00:00 2001 From: Joshua Van Deren Date: Sat, 8 Aug 2026 15:10:48 -0600 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9E=20Fix=20makeKeyWindow=20NaN=20?= =?UTF-8?q?coordinates=20that=20kill=20Chromium=20PWA=20shims?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 0xFF-filled windowLocation bytes (decoded as NaN) with a finite off-content point, matching AltTab. Chromium’s Mojo path terminates the app-shim connection when it receives those NaNs (#1131). Also widen the event buffer to 0x100 and drop the Chromium-specific resize workaround. Co-authored-by: Cursor --- Loop/Private APIs/SkyLightToolBelt.swift | 17 +++-- .../ChromiumPWAResizeWorkaround.swift | 72 ------------------- .../Window Manipulation/WindowEngine.swift | 10 +-- Loop/Window Management/Window/Window.swift | 3 +- 4 files changed, 12 insertions(+), 90 deletions(-) delete mode 100644 Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index 43def4e7..c5a3e323 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -117,19 +117,22 @@ enum SkyLightToolBelt { return false } + // Synthetic left-click (down then up) delivered by window id. Ported from AltTab / + // Hammerspoon / yabai. Aim just outside the window so it becomes key without + // clicking content. Do not fill windowLocation with 0xFF — those bytes decode as + // NaN and can terminate Chromium PWA app-shim Mojo connections (#1131). + // Buffer is 0x100 (record length stays 0xf8) to avoid WindowServer OOB reads on + // newer macOS when encoding the event record. + var offContentPoint = CGPoint(x: -1, y: -1) + // `0x01` is left click down, `0x02` is left click up (see `CGEventType`) for byte in [0x01, 0x02] { - // Create raw `SLSEvent` data. - // Future consideration: instead of manually creating the bytes here, investigate: - // - Creating a `SLSEvent` (likely analogous to `CGEvent`) - // - Apply an identifier to the event to help Loop differentiate events that originate from itself - // - Converting the `SLSEvent` to data using `SLEventCreateData` in SkyLight - var bytes = [UInt8](repeating: 0, count: 0xF8) + var bytes = [UInt8](repeating: 0, count: 0x100) bytes[0x04] = 0xF8 bytes[0x08] = UInt8(byte) bytes[0x3A] = 0x10 memcpy(&bytes[0x3C], &wid, MemoryLayout.size) - memset(&bytes[0x20], 0xFF, 0x10) + memcpy(&bytes[0x20], &offContentPoint, MemoryLayout.size) let cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in SLPSPostEventRecordTo(&psn, &pointer.baseAddress!.pointee) } diff --git a/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift b/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift deleted file mode 100644 index 9d677ace..00000000 --- a/Loop/Window Management/Window Manipulation/ChromiumPWAResizeWorkaround.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// ChromiumPWAResizeWorkaround.swift -// Loop -// -// Temporary workaround for https://github.com/mrkai77/Loop/issues/1131 -// -// Chromium-installed PWA shims (`{browser}.app.{id}`) can terminate during Loop’s -// normal AX resize path: -// NSAccessibility Request Received → NSInvalidArgumentException → -// app_shim_controller Channel error → clean terminate. -// -// Upstream: -// - Public (dup): https://issues.chromium.org/issues/539984770 -// - Canonical (restricted): https://issues.chromium.org/issues/537448007 -// -// TO REMOVE when Chromium fixes this: -// 1. Delete this file. -// 2. Delete every call site that references `ChromiumPWAResizeWorkaround` -// (WindowEngine.performResize, WindowEngine.resizeWindow, -// Window.ResolvedProperties.init). -// - -import Foundation - -enum ChromiumPWAResizeWorkaround { - /// Bundle ID bases for Chromium browsers that install macOS PWA shims as - /// `{base}.app.{id}` (optionally `{base}.app.{profile}-{id}`). - private static let browserBundleIDBases: [String] = [ - "com.google.Chrome", - "com.google.Chrome.beta", - "com.google.Chrome.canary", - "com.google.Chrome.dev", - "com.brave.Browser", - "com.brave.Browser.beta", - "com.brave.Browser.nightly", - "com.microsoft.Edge", - "com.microsoft.Edge.Beta", - "com.microsoft.Edge.Dev", - "com.microsoft.Edge.Canary", - "org.chromium.Chromium", - "company.thebrowser.Browser" - ] - - static func applies(to window: Window) -> Bool { - applies(bundleIdentifier: window.nsRunningApplication?.bundleIdentifier) - } - - /// `true` for Chromium PWA / app-mode shims only — not the browser itself. - static func applies(bundleIdentifier: String?) -> Bool { - guard let bundleIdentifier else { return false } - return browserBundleIDBases.contains { bundleIdentifier.hasPrefix($0 + ".app.") } - } - - /// Focus/raise has been observed to contribute to shim termination (#1131). - static func shouldSkipFocus(for window: Window) -> Bool { - applies(to: window) - } - - /// Skip reading/toggling `AXEnhancedUserInterface` for matching windows (#1131). - static func resolvedEnhancedUserInterface(for window: Window) -> Bool { - applies(to: window) ? false : window.enhancedUserInterface - } - - /// Size → position → size without touching `AXEnhancedUserInterface`. - /// The second size write is needed because shims often ignore the first until - /// the origin has been updated. - static func resize(_ window: Window, to targetFrame: CGRect) { - window.setSize(targetFrame.size) - window.setPosition(targetFrame.origin) - window.setSize(targetFrame.size) - } -} diff --git a/Loop/Window Management/Window Manipulation/WindowEngine.swift b/Loop/Window Management/Window Manipulation/WindowEngine.swift index 9ca03bac..5d44ac50 100644 --- a/Loop/Window Management/Window Manipulation/WindowEngine.swift +++ b/Loop/Window Management/Window Manipulation/WindowEngine.swift @@ -60,10 +60,7 @@ enum WindowEngine { false } - // ChromiumPWAResizeWorkaround (#1131) — remove with that type. - let shouldFocus = useSystemWM - || (Defaults[.focusWindowOnResize] && !ChromiumPWAResizeWorkaround.shouldSkipFocus(for: window)) - if shouldFocus { + if Defaults[.focusWindowOnResize] || useSystemWM { await window.focus() } @@ -195,11 +192,6 @@ enum WindowEngine { if animate { try await window.setFrameAnimated(targetFrame, bounds: bounds, resolvedProperties: resolvedProperties) actualFrame = window.frame - } else if ChromiumPWAResizeWorkaround.applies(to: window) { - // ChromiumPWAResizeWorkaround (#1131) — remove with that type. - ChromiumPWAResizeWorkaround.resize(window, to: targetFrame) - try Task.checkCancellation() - actualFrame = window.frame } else { await window.setFrame(targetFrame, sizeFirst: willChangeScreens, resolvedProperties: resolvedProperties) try Task.checkCancellation() diff --git a/Loop/Window Management/Window/Window.swift b/Loop/Window Management/Window/Window.swift index 91261950..8123de83 100644 --- a/Loop/Window Management/Window/Window.swift +++ b/Loop/Window Management/Window/Window.swift @@ -615,8 +615,7 @@ extension Window { self.frame = window.frame // 2 AX calls (position + size) self.isResizable = window.isResizable // 1 AX call self.isFullscreen = window.fullscreen // 1 AX call - // ChromiumPWAResizeWorkaround (#1131) — remove with that type. - self.isEnhancedUserInterface = ChromiumPWAResizeWorkaround.resolvedEnhancedUserInterface(for: window) + self.isEnhancedUserInterface = window.enhancedUserInterface // 1 AX call on app element } /// Creates a new snapshot with an updated frame, preserving stable properties. From d929ce01948ff7c65bf13f7ac88f39beeb42cb6d Mon Sep 17 00:00:00 2001 From: Joshua Van Deren Date: Sat, 8 Aug 2026 15:17:02 -0600 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8E=A8=20Name=20makeKeyWindow=20event?= =?UTF-8?q?-record=20offsets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract magic offsets into MakeKeyWindowEvent for readability, matching AltTab’s structure. No behavior change. Co-authored-by: Cursor --- Loop/Private APIs/SkyLightToolBelt.swift | 53 ++++++++++++++++-------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/Loop/Private APIs/SkyLightToolBelt.swift b/Loop/Private APIs/SkyLightToolBelt.swift index c5a3e323..5e7997cd 100644 --- a/Loop/Private APIs/SkyLightToolBelt.swift +++ b/Loop/Private APIs/SkyLightToolBelt.swift @@ -92,11 +92,34 @@ enum SkyLightToolBelt { } /// + /// Byte layout for the synthetic `CGSEventRecord` posted by `makeKeyWindow`. + /// Offsets match CGSInternal's CGSEvent.h / yabai / AltTab. + private enum MakeKeyWindowEvent { + /// Allocated buffer size. The record's declared length stays `recordLength`; + /// we allocate a little more because newer macOS WindowServer encoding can + /// read past the record (see AltTab / paneru#123). + static let bufferSize = 0x100 + static let lengthOffset = 0x04 + static let recordLength: UInt8 = 0xF8 + static let eventTypeOffset = 0x08 + static let leftMouseDown: UInt8 = 0x01 + static let leftMouseUp: UInt8 = 0x02 + /// Window-relative click point. Just outside the frame so the window becomes + /// key without hitting content. Must be finite — `0xFF` fill decodes as NaN + /// and can terminate Chromium PWA app-shim Mojo connections (#1131). + static let windowLocationOffset = 0x20 + static let offContentPoint = CGPoint(x: -1, y: -1) + static let unknownFlagOffset = 0x3A + static let unknownFlagValue: UInt8 = 0x10 + static let windowIdOffset = 0x3C + } + /// Focuses a window. This will attempt to bring the window to the front and make it the active window. /// Note that this first sets the process as frontmost, *then* sends a left click event to the window itself. /// - /// This method uses a private API to focus the window. - /// The code for this method is derived from the Amethyst source code. Details of its implementation can be found [here](https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468) + /// Uses a private API. Derived from Hammerspoon / yabai / AltTab + /// (https://github.com/Hammerspoon/hammerspoon/issues/370#issuecomment-545545468, + /// https://github.com/lwouis/alt-tab-macos/commit/782f1fe2e7272f185526e3e69eadd08c241fe050). /// /// - Parameters: /// - windowID: The `CGWindowID` of the window to focus. @@ -117,22 +140,16 @@ enum SkyLightToolBelt { return false } - // Synthetic left-click (down then up) delivered by window id. Ported from AltTab / - // Hammerspoon / yabai. Aim just outside the window so it becomes key without - // clicking content. Do not fill windowLocation with 0xFF — those bytes decode as - // NaN and can terminate Chromium PWA app-shim Mojo connections (#1131). - // Buffer is 0x100 (record length stays 0xf8) to avoid WindowServer OOB reads on - // newer macOS when encoding the event record. - var offContentPoint = CGPoint(x: -1, y: -1) - - // `0x01` is left click down, `0x02` is left click up (see `CGEventType`) - for byte in [0x01, 0x02] { - var bytes = [UInt8](repeating: 0, count: 0x100) - bytes[0x04] = 0xF8 - bytes[0x08] = UInt8(byte) - bytes[0x3A] = 0x10 - memcpy(&bytes[0x3C], &wid, MemoryLayout.size) - memcpy(&bytes[0x20], &offContentPoint, MemoryLayout.size) + var offContentPoint = MakeKeyWindowEvent.offContentPoint + + for eventType in [MakeKeyWindowEvent.leftMouseDown, MakeKeyWindowEvent.leftMouseUp] { + var bytes = [UInt8](repeating: 0, count: MakeKeyWindowEvent.bufferSize) + bytes[MakeKeyWindowEvent.lengthOffset] = MakeKeyWindowEvent.recordLength + bytes[MakeKeyWindowEvent.eventTypeOffset] = eventType + bytes[MakeKeyWindowEvent.unknownFlagOffset] = MakeKeyWindowEvent.unknownFlagValue + memcpy(&bytes[MakeKeyWindowEvent.windowIdOffset], &wid, MemoryLayout.size) + memcpy(&bytes[MakeKeyWindowEvent.windowLocationOffset], &offContentPoint, MemoryLayout.size) + let cgStatus = bytes.withUnsafeMutableBufferPointer { pointer in SLPSPostEventRecordTo(&psn, &pointer.baseAddress!.pointee) }