From 6ed6a6a1c677e0d9383dd4a92a6e21e8757a61a6 Mon Sep 17 00:00:00 2001 From: YoruSamaOG <0tox@iterelle.tech> Date: Tue, 20 Oct 2026 22:39:15 -0500 Subject: [PATCH] fix(desktop): skip auxiliary macOS snapshot windows Re-lands #12978 (b4cebe8b1d), reverted in #22222. ActiveWindow.test.ts was deleted by #22557 after the revert, so only the macOS window selection cases this fix adds are restored. --- .../desktop/src/snapShot/ActiveWindow.test.ts | 160 ++++++++++++++++++ apps/desktop/src/snapShot/ActiveWindow.ts | 87 +++++++--- 2 files changed, 223 insertions(+), 24 deletions(-) create mode 100644 apps/desktop/src/snapShot/ActiveWindow.test.ts diff --git a/apps/desktop/src/snapShot/ActiveWindow.test.ts b/apps/desktop/src/snapShot/ActiveWindow.test.ts new file mode 100644 index 000000000000..4d61c78b4e3a --- /dev/null +++ b/apps/desktop/src/snapShot/ActiveWindow.test.ts @@ -0,0 +1,160 @@ +import { assert, beforeEach, it, vi } from "vite-plus/test"; + +const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() })); + +vi.mock("node:child_process", () => ({ execFile: execFileMock })); + +import { activeWindow } from "./ActiveWindow.ts"; + +beforeEach(() => { + execFileMock.mockReset(); +}); + +function stubMacLookup(stdout: string) { + execFileMock.mockImplementation( + ( + _file: string, + _args: ReadonlyArray, + _options: unknown, + callback: (error: Error | null, stdout: string) => void, + ) => callback(null, stdout), + ); +} + +function macLookup( + windows: ReadonlyArray<{ + id: number; + title: string; + bounds: { x: number; y: number; width: number; height: number }; + alpha?: number; + }>, +) { + return JSON.stringify({ + owner: { + name: "Editor", + processId: 123, + path: "/Applications/Editor.app", + bundleId: "com.example.editor", + }, + windows: windows.map((window) => ({ ...window, alpha: window.alpha ?? 1 })), + }); +} + +it("parses the frontmost macOS window from the osascript lookup", async () => { + stubMacLookup( + macLookup([ + { + id: 42, + title: "main.ts", + bounds: { x: 10, y: 20, width: 800, height: 600 }, + }, + ]) + "\n", + ); + + const window = await activeWindow("darwin"); + + assert.deepEqual(window, { + platform: "macos", + id: 42, + title: "main.ts", + bounds: { x: 10, y: 20, width: 800, height: 600 }, + owner: { + name: "Editor", + processId: 123, + path: "/Applications/Editor.app", + bundleId: "com.example.editor", + }, + }); + const [file, args] = execFileMock.mock.calls[0]!; + assert.strictEqual(file, "/usr/bin/osascript"); + assert.deepEqual(args.slice(0, 3), ["-l", "JavaScript", "-e"]); +}); + +it.each([ + { + name: "transparent and untitled helpers", + helpers: [ + { + id: 1, + title: "", + bounds: { x: -1, y: -44, width: 1_680, height: 44 }, + alpha: 0, + }, + { id: 2, title: "", bounds: { x: 0, y: 0, width: 1_680, height: 68 } }, + ], + content: { + id: 3, + title: "Document", + bounds: { x: 0, y: 0, width: 1_680, height: 1_050 }, + }, + }, + { + name: "Chrome strips stacked above the content window", + helpers: [ + { id: 75, title: "", bounds: { x: 0, y: 39, width: 1_800, height: 41 } }, + { id: 74, title: "", bounds: { x: 0, y: 80, width: 1_800, height: 81 } }, + { id: 1691, title: "", bounds: { x: 0, y: 39, width: 1_800, height: 158 } }, + ], + content: { + id: 71, + title: "(10) Video - YouTube", + bounds: { x: 0, y: 161, width: 1_800, height: 1_008 }, + }, + }, + { + name: "a smaller front window's strips with a larger background window", + helpers: [ + { id: 75, title: "", bounds: { x: 100, y: 139, width: 1_200, height: 41 } }, + { id: 74, title: "", bounds: { x: 100, y: 180, width: 1_200, height: 81 } }, + ], + content: { + id: 71, + title: "Video - YouTube", + bounds: { x: 100, y: 261, width: 1_200, height: 700 }, + }, + background: [ + { id: 80, title: "Other document", bounds: { x: 0, y: 39, width: 1_800, height: 1_130 } }, + ], + }, +])("selects the content window behind $name", async ({ helpers, content, background = [] }) => { + stubMacLookup(macLookup([...helpers, content, ...background])); + + const window = await activeWindow("darwin"); + + assert.strictEqual(window?.id, content.id); + assert.deepEqual(window?.bounds, content.bounds); +}); + +it.each([ + { name: "untitled dialog", title: "", bounds: { x: 300, y: 200, width: 400, height: 240 } }, + { name: "titled palette", title: "Palette", bounds: { x: 0, y: 0, width: 800, height: 50 } }, + { name: "offset untitled strip", title: "", bounds: { x: 100, y: 100, width: 800, height: 50 } }, + { name: "tall untitled window", title: "", bounds: { x: 0, y: 0, width: 800, height: 900 } }, +])("keeps a frontmost $name ahead of a larger document", async ({ title, bounds }) => { + stubMacLookup( + macLookup([ + { id: 1, title, bounds }, + { id: 2, title: "Document", bounds: { x: 0, y: 0, width: 800, height: 1_000 } }, + ]), + ); + + const window = await activeWindow("darwin"); + + assert.strictEqual(window?.id, 1); + assert.deepEqual(window?.bounds, bounds); +}); + +it("keeps an untitled content window ahead of a much taller background window", async () => { + const bounds = { x: 0, y: 0, width: 800, height: 900 }; + stubMacLookup( + macLookup([ + { id: 1, title: "", bounds }, + { id: 2, title: "Background document", bounds: { x: 0, y: 0, width: 800, height: 4_000 } }, + ]), + ); + + const window = await activeWindow("darwin"); + + assert.strictEqual(window?.id, 1); + assert.deepEqual(window?.bounds, bounds); +}); diff --git a/apps/desktop/src/snapShot/ActiveWindow.ts b/apps/desktop/src/snapShot/ActiveWindow.ts index f62669a39bf3..08c6cfa6ba04 100644 --- a/apps/desktop/src/snapShot/ActiveWindow.ts +++ b/apps/desktop/src/snapShot/ActiveWindow.ts @@ -30,23 +30,32 @@ export type ActiveWindow = { const MAC_LOOKUP_TIMEOUT_MS = 5_000; -const MacActiveWindow = Schema.Struct({ - id: Schema.Number, - title: Schema.String, - bounds: Schema.Struct({ - x: Schema.Number, - y: Schema.Number, - width: Schema.Number, - height: Schema.Number, - }), +const MAC_HELPER_STRIP_MAX_THICKNESS_RATIO = 0.25; + +const MacActiveWindowLookup = Schema.Struct({ owner: Schema.Struct({ name: Schema.String, processId: Schema.Number, path: Schema.String, bundleId: Schema.String, }), + windows: Schema.Array( + Schema.Struct({ + id: Schema.Number, + title: Schema.String, + bounds: Schema.Struct({ + x: Schema.Number, + y: Schema.Number, + width: Schema.Number, + height: Schema.Number, + }), + alpha: Schema.Number, + }), + ), }); -const decodeMacActiveWindow = Schema.decodeUnknownSync(Schema.fromJsonString(MacActiveWindow)); +const decodeMacActiveWindowLookup = Schema.decodeUnknownSync( + Schema.fromJsonString(MacActiveWindowLookup), +); const MAC_LOOKUP_SCRIPT = ` ObjC.import("CoreGraphics"); @@ -55,30 +64,38 @@ function run() { const app = $.NSWorkspace.sharedWorkspace.frontmostApplication; if (app.isNil()) return ""; const pid = app.processIdentifier; + let ownerName = String(app.localizedName.js || ""); const list = $.CGWindowListCopyWindowInfo( $.kCGWindowListOptionOnScreenOnly | $.kCGWindowListExcludeDesktopElements, $.kCGNullWindowID, ); $.CFMakeCollectable(list); const count = $.CFArrayGetCount(list); + const windows = []; for (let i = 0; i < count; i++) { const w = ObjC.castRefToObject($.CFArrayGetValueAtIndex(list, i)); if (w.objectForKey("kCGWindowOwnerPID").js !== pid) continue; if (w.objectForKey("kCGWindowLayer").js !== 0) continue; + if (!ownerName) { + ownerName = String(ObjC.unwrap(w.objectForKey("kCGWindowOwnerName")) || ""); + } const b = ObjC.deepUnwrap(w.objectForKey("kCGWindowBounds")); - return JSON.stringify({ + windows.push({ id: w.objectForKey("kCGWindowNumber").js, title: String(ObjC.unwrap(w.objectForKey("kCGWindowName")) || ""), bounds: { x: b.X, y: b.Y, width: b.Width, height: b.Height }, - owner: { - name: String(app.localizedName.js || ObjC.unwrap(w.objectForKey("kCGWindowOwnerName")) || ""), - processId: pid, - path: String(app.bundleURL.path.js || ""), - bundleId: String(app.bundleIdentifier.js || ""), - }, + alpha: w.objectForKey("kCGWindowAlpha").js, }); } - return ""; + return JSON.stringify({ + owner: { + name: ownerName, + processId: pid, + path: String(app.bundleURL.path.js || ""), + bundleId: String(app.bundleIdentifier.js || ""), + }, + windows, + }); }`; function runMacLookup(): Promise { @@ -86,7 +103,7 @@ function runMacLookup(): Promise { NodeChildProcess.execFile( "/usr/bin/osascript", ["-l", "JavaScript", "-e", MAC_LOOKUP_SCRIPT], - { timeout: MAC_LOOKUP_TIMEOUT_MS, maxBuffer: 64 * 1024 }, + { timeout: MAC_LOOKUP_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, (error, stdout) => { if (error) reject(error); else resolve(stdout); @@ -98,17 +115,39 @@ function runMacLookup(): Promise { async function macActiveWindow(): Promise { const output = (await runMacLookup()).trim(); if (!output) return undefined; - const window = decodeMacActiveWindow(output); + const lookup = decodeMacActiveWindowLookup(output); + const visible = lookup.windows.filter( + (window) => window.alpha > 0 && window.bounds.width > 0 && window.bounds.height > 0, + ); + // Helpers belong to the content behind them, which may not be the app's largest window. + const window = visible.find((candidate, index) => { + if (candidate.title.trim() !== "") return true; + const bounds = candidate.bounds; + return !visible.some((reference, referenceIndex) => { + if (referenceIndex <= index) return false; + return ( + (bounds.x === reference.bounds.x && + bounds.width === reference.bounds.width && + bounds.height <= bounds.width * MAC_HELPER_STRIP_MAX_THICKNESS_RATIO && + bounds.height <= reference.bounds.height * MAC_HELPER_STRIP_MAX_THICKNESS_RATIO) || + (bounds.y === reference.bounds.y && + bounds.height === reference.bounds.height && + bounds.width <= bounds.height * MAC_HELPER_STRIP_MAX_THICKNESS_RATIO && + bounds.width <= reference.bounds.width * MAC_HELPER_STRIP_MAX_THICKNESS_RATIO) + ); + }); + }); + if (!window) return undefined; return { platform: "macos", id: window.id, title: window.title, bounds: window.bounds, owner: { - name: window.owner.name, - processId: window.owner.processId, - path: window.owner.path, - ...(window.owner.bundleId ? { bundleId: window.owner.bundleId } : {}), + name: lookup.owner.name, + processId: lookup.owner.processId, + path: lookup.owner.path, + ...(lookup.owner.bundleId ? { bundleId: lookup.owner.bundleId } : {}), }, }; }