From fe56fa2bc2c7b9dc5afc6592fddedddef96cced9 Mon Sep 17 00:00:00 2001 From: Aarohan Niraula Date: Sat, 8 Aug 2026 13:25:03 +0545 Subject: [PATCH 1/9] fix(macos): raise the Screen Recording prompt on first run `requestScreenAccess` only raised the TCC prompt when `getMediaAccessStatus("screen")` returned "not-determined", which macOS never reports. Chromium resolves that permission through `CGPreflightScreenCaptureAccess()`, a bool, so a machine that has never been asked is indistinguishable from an explicit refusal and both arrive as "denied". A first run therefore fell straight through to the "open System Settings" dialog without macOS ever being asked, leaving a manual toggle as the only way to grant. The renderer's retry loop in openSourceSelectorFlow arms on the same status and never ran either. Decide from whether this launch has already asked instead. The first ask raises the prompt and reports "not-determined" so the retry loop arms; later asks report the real status so the Settings dialog still reaches a user who genuinely refused, without re-prompting on every click. Verified on macOS 26.2: with no TCC screen-capture row for the bundle, getMediaAccessStatus("screen") returns "denied" while getMediaAccessStatus("camera") returns "not-determined". Co-Authored-By: Claude Opus 5 (1M context) --- electron/ipc/handlers.ts | 12 ++++++++++- electron/ipc/screenAccessPrompt.test.ts | 27 ++++++++++++++++++++++++ electron/ipc/screenAccessPrompt.ts | 28 +++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 electron/ipc/screenAccessPrompt.test.ts create mode 100644 electron/ipc/screenAccessPrompt.ts diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 7d63b934b..0c622e1f9 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -79,6 +79,7 @@ import { patchWebmDurationOnDisk } from "../recording/webm-duration"; import { reindexRecordingOnDisk } from "../recording/webm-seek-index"; import { registerNativeBridgeHandlers } from "./nativeBridge"; import { RecordingStreamRegistry, registerRecordingStreamHandlers } from "./recordingStream"; +import { shouldPromptForScreenAccess } from "./screenAccessPrompt"; const PROJECT_FILE_EXTENSION = "openscreen"; export const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json"); @@ -455,6 +456,8 @@ type AttachNativeMacWebcamRecordingInput = { let selectedSource: SelectedSource | null = null; let selectedDesktopSource: DesktopCapturerSource | null = null; +/** macOS raises its Screen Recording prompt once per launch. */ +let hasPromptedForScreenAccess = false; let lastEnumeratedSources = new Map(); let currentProjectPath: string | null = null; let currentRecordingSession: RecordingSession | null = null; @@ -1674,7 +1677,14 @@ export function registerIpcHandlers( // Screen recording has no askForMediaAccess equivalent, so trigger the // TCC prompt without opening OpenScreen's source selector above it. - if (status === "not-determined") { + // macOS reports a never-asked machine as "denied", so the decision has to + // come from shouldPromptForScreenAccess rather than the status alone. + // + // Report "not-determined" while that prompt is up: it is the status the + // renderer's retry loop polls on, and macOS keeps answering "denied" + // until the user actually accepts. + if (shouldPromptForScreenAccess(status, hasPromptedForScreenAccess)) { + hasPromptedForScreenAccess = true; const mainWin = getMainWindow(); if (mainWin && !mainWin.isDestroyed()) { if (!mainWin.isVisible()) { diff --git a/electron/ipc/screenAccessPrompt.test.ts b/electron/ipc/screenAccessPrompt.test.ts new file mode 100644 index 000000000..f527ca91f --- /dev/null +++ b/electron/ipc/screenAccessPrompt.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { shouldPromptForScreenAccess } from "./screenAccessPrompt"; + +describe("shouldPromptForScreenAccess", () => { + it("never prompts once the permission is granted", () => { + expect(shouldPromptForScreenAccess("granted", false)).toBe(false); + expect(shouldPromptForScreenAccess("granted", true)).toBe(false); + }); + + it("prompts on the first ask of a launch even though macOS reports denied", () => { + // The regression this guards: macOS collapses "never asked" into "denied", + // so a first run used to skip the prompt entirely. + expect(shouldPromptForScreenAccess("denied", false)).toBe(true); + }); + + it("stops prompting after this launch has already asked", () => { + // Lets the caller report the real status so the Settings dialog takes over + // instead of re-prompting on every click. + expect(shouldPromptForScreenAccess("denied", true)).toBe(false); + expect(shouldPromptForScreenAccess("restricted", true)).toBe(false); + }); + + it("still prompts on not-determined, whatever this launch has already asked", () => { + expect(shouldPromptForScreenAccess("not-determined", false)).toBe(true); + expect(shouldPromptForScreenAccess("not-determined", true)).toBe(true); + }); +}); diff --git a/electron/ipc/screenAccessPrompt.ts b/electron/ipc/screenAccessPrompt.ts new file mode 100644 index 000000000..f2e737b04 --- /dev/null +++ b/electron/ipc/screenAccessPrompt.ts @@ -0,0 +1,28 @@ +/** + * Decides whether to raise macOS' own Screen Recording prompt. + * + * `systemPreferences.getMediaAccessStatus("screen")` cannot answer + * "not-determined" on macOS. Chromium resolves that permission through + * `CGPreflightScreenCaptureAccess()`, a bool, so a machine that has never been + * asked is reported exactly like an explicit refusal — both arrive as "denied". + * Gating the prompt on `status === "not-determined"` therefore never fires: a + * fresh install falls straight through to the "open System Settings" dialog and + * macOS is never given the chance to ask, so the only way to grant is a manual + * toggle. The renderer's permission-retry loop arms on the same status and is + * dead for the same reason. + * + * Drive the first prompt off whether this launch has already asked instead. + * Asking once per launch keeps a genuine refusal from re-prompting on every + * click, and lets the next call report the real status so the Settings dialog + * still reaches a user who said no. + */ +export function shouldPromptForScreenAccess( + status: string, + hasPromptedThisLaunch: boolean, +): boolean { + if (status === "granted") { + return false; + } + + return status === "not-determined" || !hasPromptedThisLaunch; +} From 028de736d70a258fadadf02725f48769cb8151a3 Mon Sep 17 00:00:00 2001 From: Aarohan Niraula Date: Sat, 8 Aug 2026 13:35:24 +0545 Subject: [PATCH 2/9] fix(macos): hold the real status while the prompt is unanswered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raising the prompt and immediately reporting the real status let open-source-selector show the Settings dialog over the native prompt, and stopped the renderer's retry loop on its first poll. An in-flight flag around getSources() does not cover this. Measured on macOS 26.2, that call settles in 4ms whether or not the prompt is still on screen (it rejects outright when access is denied), and the status stays "denied" for as long as the prompt is up — it only flips once the user accepts. So there is nothing observable to wait on. Time-box it instead: keep reporting "not-determined" for the renderer's retry budget after asking, then let the real status through so the Settings dialog still reaches a user who refused. Co-Authored-By: Claude Opus 5 (1M context) --- electron/ipc/handlers.ts | 18 ++++++++---- electron/ipc/screenAccessPrompt.test.ts | 39 ++++++++++++++++++++----- electron/ipc/screenAccessPrompt.ts | 37 +++++++++++++++++++---- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 0c622e1f9..37fb4e79c 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -79,7 +79,7 @@ import { patchWebmDurationOnDisk } from "../recording/webm-duration"; import { reindexRecordingOnDisk } from "../recording/webm-seek-index"; import { registerNativeBridgeHandlers } from "./nativeBridge"; import { RecordingStreamRegistry, registerRecordingStreamHandlers } from "./recordingStream"; -import { shouldPromptForScreenAccess } from "./screenAccessPrompt"; +import { isAwaitingScreenPromptAnswer, shouldPromptForScreenAccess } from "./screenAccessPrompt"; const PROJECT_FILE_EXTENSION = "openscreen"; export const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json"); @@ -456,8 +456,8 @@ type AttachNativeMacWebcamRecordingInput = { let selectedSource: SelectedSource | null = null; let selectedDesktopSource: DesktopCapturerSource | null = null; -/** macOS raises its Screen Recording prompt once per launch. */ -let hasPromptedForScreenAccess = false; +/** When macOS was asked for Screen Recording this launch, if it has been. */ +let screenAccessPromptedAt: number | null = null; let lastEnumeratedSources = new Map(); let currentProjectPath: string | null = null; let currentRecordingSession: RecordingSession | null = null; @@ -1683,8 +1683,8 @@ export function registerIpcHandlers( // Report "not-determined" while that prompt is up: it is the status the // renderer's retry loop polls on, and macOS keeps answering "denied" // until the user actually accepts. - if (shouldPromptForScreenAccess(status, hasPromptedForScreenAccess)) { - hasPromptedForScreenAccess = true; + if (shouldPromptForScreenAccess(status, screenAccessPromptedAt)) { + screenAccessPromptedAt = Date.now(); const mainWin = getMainWindow(); if (mainWin && !mainWin.isDestroyed()) { if (!mainWin.isVisible()) { @@ -1701,6 +1701,14 @@ export function registerIpcHandlers( return { success: true, granted: false, status: "not-determined" }; } + // Keep reporting "not-determined" while that prompt may still be up. + // macOS answers "denied" the whole time it is on screen, so returning the + // real status here would open System Settings over the prompt and stop + // the renderer's retry loop on its first poll. + if (isAwaitingScreenPromptAnswer(screenAccessPromptedAt, Date.now())) { + return { success: true, granted: false, status: "not-determined" }; + } + return { success: true, granted: false, status }; } catch (error) { console.error("Failed to request screen access:", error); diff --git a/electron/ipc/screenAccessPrompt.test.ts b/electron/ipc/screenAccessPrompt.test.ts index f527ca91f..c23375272 100644 --- a/electron/ipc/screenAccessPrompt.test.ts +++ b/electron/ipc/screenAccessPrompt.test.ts @@ -1,27 +1,50 @@ import { describe, expect, it } from "vitest"; -import { shouldPromptForScreenAccess } from "./screenAccessPrompt"; +import { + isAwaitingScreenPromptAnswer, + SCREEN_PROMPT_GRACE_MS, + shouldPromptForScreenAccess, +} from "./screenAccessPrompt"; describe("shouldPromptForScreenAccess", () => { it("never prompts once the permission is granted", () => { - expect(shouldPromptForScreenAccess("granted", false)).toBe(false); - expect(shouldPromptForScreenAccess("granted", true)).toBe(false); + expect(shouldPromptForScreenAccess("granted", null)).toBe(false); + expect(shouldPromptForScreenAccess("granted", 1_000)).toBe(false); }); it("prompts on the first ask of a launch even though macOS reports denied", () => { // The regression this guards: macOS collapses "never asked" into "denied", // so a first run used to skip the prompt entirely. - expect(shouldPromptForScreenAccess("denied", false)).toBe(true); + expect(shouldPromptForScreenAccess("denied", null)).toBe(true); }); it("stops prompting after this launch has already asked", () => { // Lets the caller report the real status so the Settings dialog takes over // instead of re-prompting on every click. - expect(shouldPromptForScreenAccess("denied", true)).toBe(false); - expect(shouldPromptForScreenAccess("restricted", true)).toBe(false); + expect(shouldPromptForScreenAccess("denied", 1_000)).toBe(false); + expect(shouldPromptForScreenAccess("restricted", 1_000)).toBe(false); }); it("still prompts on not-determined, whatever this launch has already asked", () => { - expect(shouldPromptForScreenAccess("not-determined", false)).toBe(true); - expect(shouldPromptForScreenAccess("not-determined", true)).toBe(true); + expect(shouldPromptForScreenAccess("not-determined", null)).toBe(true); + expect(shouldPromptForScreenAccess("not-determined", 1_000)).toBe(true); + }); +}); + +describe("isAwaitingScreenPromptAnswer", () => { + it("is not awaiting anything before the prompt has been raised", () => { + expect(isAwaitingScreenPromptAnswer(null, 10_000)).toBe(false); + }); + + it("holds the real status back while the prompt may still be on screen", () => { + // Without this the Settings dialog opens over the native prompt and the + // renderer's retry loop aborts on its first poll, because macOS keeps + // answering "denied" until the user actually accepts. + expect(isAwaitingScreenPromptAnswer(1_000, 1_000)).toBe(true); + expect(isAwaitingScreenPromptAnswer(1_000, 1_000 + SCREEN_PROMPT_GRACE_MS - 1)).toBe(true); + }); + + it("releases the real status once the grace window lapses", () => { + expect(isAwaitingScreenPromptAnswer(1_000, 1_000 + SCREEN_PROMPT_GRACE_MS)).toBe(false); + expect(isAwaitingScreenPromptAnswer(1_000, 60_000)).toBe(false); }); }); diff --git a/electron/ipc/screenAccessPrompt.ts b/electron/ipc/screenAccessPrompt.ts index f2e737b04..76fa4f93a 100644 --- a/electron/ipc/screenAccessPrompt.ts +++ b/electron/ipc/screenAccessPrompt.ts @@ -1,3 +1,12 @@ +/** + * How long after raising the native prompt to keep reporting "not-determined". + * + * Matches the renderer's retry budget in `openSourceSelectorFlow` (8 attempts, + * 750ms apart), which is the window the user has to answer the prompt before + * the Settings dialog takes over. + */ +export const SCREEN_PROMPT_GRACE_MS = 6_000; + /** * Decides whether to raise macOS' own Screen Recording prompt. * @@ -13,16 +22,32 @@ * * Drive the first prompt off whether this launch has already asked instead. * Asking once per launch keeps a genuine refusal from re-prompting on every - * click, and lets the next call report the real status so the Settings dialog + * click, and lets a later call report the real status so the Settings dialog * still reaches a user who said no. */ -export function shouldPromptForScreenAccess( - status: string, - hasPromptedThisLaunch: boolean, -): boolean { +export function shouldPromptForScreenAccess(status: string, promptedAt: number | null): boolean { if (status === "granted") { return false; } - return status === "not-determined" || !hasPromptedThisLaunch; + return status === "not-determined" || promptedAt === null; +} + +/** + * Whether the native prompt raised at `promptedAt` may still be waiting for an + * answer, and the real status should be withheld until it is. + * + * macOS gives us nothing to observe here. `desktopCapturer.getSources()` settles + * in a few milliseconds whether or not the prompt is still on screen (measured + * at 4ms on macOS 26.2), and the status stays "denied" for the whole time the + * prompt is up — it only ever flips once the user accepts. So an in-flight flag + * around that call covers nothing, and reporting "denied" straight away would + * open System Settings over the prompt and abort the renderer's retry loop on + * its first poll. + * + * Treating the grace window as "still asking" keeps the loop polling long enough + * to notice an accept, and lets the Settings dialog through once it lapses. + */ +export function isAwaitingScreenPromptAnswer(promptedAt: number | null, now: number): boolean { + return promptedAt !== null && now - promptedAt < SCREEN_PROMPT_GRACE_MS; } From 433b30c1980cfec5b67cfb69115946c3aac3958e Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Mon, 31 Aug 2026 12:09:31 +0200 Subject: [PATCH 3/9] feat(macos): read the screen recording grant from a fresh process `CGPreflightScreenCaptureAccess()` caches its answer for the life of the calling process, and Electron's `getMediaAccessStatus("screen")` is that same function. A long-lived app therefore cannot observe its own Screen Recording permission being granted: once it has read false it reads false until relaunch, whatever the user does in System Settings. That is why polling the app's own status after raising the prompt could never succeed, and no grace window would have made it succeed. The helper gains a `--screen-access-status` mode that answers the question and exits, so every read comes from a process with no cache to be stale. It is deliberately handled before the macOS 13 guard and the recording request decode: the question is asked on every macOS the app supports. The bridge mirrors the cursor helper's split between "the user said no" and "the helper never got to answer", so a build without the binary falls back to the old behaviour rather than accusing the user of a refusal. --- .../screen/macScreenAccess.test.ts | 174 ++++++++++++++++ .../native-bridge/screen/macScreenAccess.ts | 187 ++++++++++++++++++ .../ScreenCaptureRecorder.swift | 25 +++ 3 files changed, 386 insertions(+) create mode 100644 electron/native-bridge/screen/macScreenAccess.test.ts create mode 100644 electron/native-bridge/screen/macScreenAccess.ts diff --git a/electron/native-bridge/screen/macScreenAccess.test.ts b/electron/native-bridge/screen/macScreenAccess.test.ts new file mode 100644 index 000000000..c3441c9af --- /dev/null +++ b/electron/native-bridge/screen/macScreenAccess.test.ts @@ -0,0 +1,174 @@ +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * The cast on `actual` is written out in each factory rather than shared in a helper: + * `vi.mock` calls are HOISTED above every top-level statement, so a module-scope helper + * is still in its temporal dead zone when the factory runs. + */ +type WithDefault = { default?: Record }; + +vi.mock("node:child_process", async (importOriginal) => { + const actual = await importOriginal(); + const spawn = vi.fn(); + return { ...actual, spawn, default: { ...((actual as WithDefault).default ?? {}), spawn } }; +}); + +const mocks = vi.hoisted(() => ({ accessSync: vi.fn() })); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + // No helper binary exists in a test checkout; by default pretend the first candidate + // path is executable so path resolution is not what is under test. + return { + ...actual, + accessSync: mocks.accessSync, + default: { ...((actual as WithDefault).default ?? {}), accessSync: mocks.accessSync }, + }; +}); + +import { spawn } from "node:child_process"; +import { isMacScreenProbeUnavailable, readMacScreenCaptureAccess } from "./macScreenAccess"; + +/** Minimal stand-in for the helper: stdio pipes plus kill bookkeeping. */ +class FakeHelper extends EventEmitter { + stdout = new PassThrough(); + stderr = new PassThrough(); + killed = false; + + kill() { + this.killed = true; + return true; + } + + /** Feeds one NDJSON line, the way the real helper emits them. */ + emitEvent(event: Record) { + this.stdout.write(`${JSON.stringify(event)}\n`); + } +} + +const spawnMock = vi.mocked(spawn); +let helper: FakeHelper; +let originalPlatform: PropertyDescriptor | undefined; + +beforeEach(() => { + originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value: "darwin", configurable: true }); + helper = new FakeHelper(); + spawnMock.mockReset(); + spawnMock.mockReturnValue(helper as unknown as ReturnType); + mocks.accessSync.mockReset(); +}); + +afterEach(() => { + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform); + } + vi.restoreAllMocks(); +}); + +/** Lets the spawn listeners attach before the fake helper speaks. */ +async function settle(pending: Promise, act: () => void): Promise { + await Promise.resolve(); + act(); + return pending; +} + +describe("readMacScreenCaptureAccess", () => { + it("grants when the helper reports the permission", async () => { + const access = await settle(readMacScreenCaptureAccess(), () => + helper.emitEvent({ event: "screen-access", granted: true }), + ); + + expect(access).toMatchObject({ success: true, granted: true, status: "granted" }); + }); + + it("denies when the helper reports the permission is absent", async () => { + const access = await settle(readMacScreenCaptureAccess(), () => + helper.emitEvent({ event: "screen-access", granted: false }), + ); + + expect(access).toMatchObject({ success: true, granted: false, status: "denied" }); + }); + + it("spawns the probe flag and never a recording request", async () => { + await settle(readMacScreenCaptureAccess(), () => + helper.emitEvent({ event: "screen-access", granted: true }), + ); + + expect(spawnMock).toHaveBeenCalledTimes(1); + expect(spawnMock.mock.calls[0]?.[1]).toEqual(["--screen-access-status"]); + }); + + it("reads a fresh answer per call, which is the point of the child process", async () => { + // The main process cannot do this: CGPreflightScreenCaptureAccess caches its + // result for the life of the caller, so a grant made while the app runs is + // invisible to it. Every call here is a new process, so the second read sees + // the grant the first one missed. + const first = await settle(readMacScreenCaptureAccess(), () => + helper.emitEvent({ event: "screen-access", granted: false }), + ); + helper = new FakeHelper(); + spawnMock.mockReturnValue(helper as unknown as ReturnType); + const second = await settle(readMacScreenCaptureAccess(), () => + helper.emitEvent({ event: "screen-access", granted: true }), + ); + + expect(first.granted).toBe(false); + expect(second.granted).toBe(true); + expect(spawnMock).toHaveBeenCalledTimes(2); + }); + + it("reports missing-helper rather than a refusal when no binary is installed", async () => { + mocks.accessSync.mockImplementation(() => { + throw new Error("ENOENT"); + }); + + const access = await readMacScreenCaptureAccess(); + + expect(access).toMatchObject({ granted: false, status: "missing-helper" }); + expect(isMacScreenProbeUnavailable(access.status)).toBe(true); + expect(spawnMock).not.toHaveBeenCalled(); + }); + + it("reads the answer even when the helper dies in the same tick", async () => { + // The helper prints one line and exits immediately, so the process-death event can + // land before stdout has been drained. Racing it away would report a good answer + // as a dead helper on a machine that is merely fast. + const access = await settle(readMacScreenCaptureAccess(), () => { + helper.emitEvent({ event: "screen-access", granted: true }); + helper.emit("close", 0, null); + }); + + expect(access).toMatchObject({ granted: true, status: "granted" }); + }); + + it("reports exited rather than a refusal when an older helper rejects the flag", async () => { + // A build predating the probe mode answers `invalidArguments` and exits 1. + // Calling that a denial would tell a user with a working grant to go and + // re-grant it. + const access = await settle(readMacScreenCaptureAccess(), () => helper.emit("close", 1, null)); + + expect(access).toMatchObject({ granted: false, status: "exited" }); + expect(isMacScreenProbeUnavailable(access.status)).toBe(true); + }); + + it("reports error when the helper cannot be launched at all", async () => { + const access = await settle(readMacScreenCaptureAccess(), () => + helper.emit("error", new Error("EACCES")), + ); + + expect(access).toMatchObject({ granted: false, status: "error", error: "EACCES" }); + expect(isMacScreenProbeUnavailable(access.status)).toBe(true); + }); + + it("answers granted off-darwin without spawning anything", async () => { + Object.defineProperty(process, "platform", { value: "win32", configurable: true }); + + const access = await readMacScreenCaptureAccess(); + + expect(access).toMatchObject({ granted: true, status: "granted" }); + expect(spawnMock).not.toHaveBeenCalled(); + }); +}); diff --git a/electron/native-bridge/screen/macScreenAccess.ts b/electron/native-bridge/screen/macScreenAccess.ts new file mode 100644 index 000000000..f167048a3 --- /dev/null +++ b/electron/native-bridge/screen/macScreenAccess.ts @@ -0,0 +1,187 @@ +import { spawn } from "node:child_process"; +import { accessSync, constants as fsConstants } from "node:fs"; +import path from "node:path"; + +/** + * Reading macOS' Screen Recording grant from a short-lived child process. + * + * `CGPreflightScreenCaptureAccess()` caches its answer for the life of the process + * that calls it. Once it has answered false it answers false forever, whatever the + * user does in System Settings afterwards -- Apple's own guidance is to relaunch. + * Electron's `systemPreferences.getMediaAccessStatus("screen")` is that same + * function (Chromium's `IsScreenCaptureAllowed()` in `ui/base/cocoa/permissions_utils.mm`), + * so the app's main process holds one stale bool for its entire run. + * + * That is the whole reason this module exists. The helper is spawned fresh for every + * read, so every read is the current answer, and a grant the user makes while the app + * is running becomes observable without a restart. + * + * The prompt is NOT raised here. It stays in the main process, where Chromium raises + * it through the app bundle, so TCC records the grant against the app's designated + * requirement rather than against a bare child binary. + */ + +const HELPER_NAME = "openscreen-screencapturekit-helper"; + +/** Kept in step with `screenAccessStatusFlag` in ScreenCaptureRecorder.swift. */ +const SCREEN_ACCESS_STATUS_FLAG = "--screen-access-status"; + +/** + * The helper prints one line and exits, so this bounds a hung spawn rather than a + * slow answer. Shorter than the cursor helper's budget because nothing here waits + * on a window server handshake. + */ +const PROBE_TIMEOUT_MS = 3_000; + +/** + * Why `denied` is the only status that means "the user said no". + * + * The other four mean the helper never got to answer -- absent from the build, killed + * by the loader, crashed, or hung. Treating those as a refusal is what would put the + * "grant Screen Recording" dialog in front of a user whose permission is fine, which is + * the same failure the cursor helper's `missing-helper` split exists to prevent (#515). + */ +export type MacScreenAccessStatus = + | "granted" + | "denied" + | "missing-helper" + | "error" + | "exited" + | "timeout"; + +export interface MacScreenAccessResult { + success: boolean; + granted: boolean; + status: MacScreenAccessStatus; + error?: string; +} + +/** True when the probe never got far enough to answer the permission question. */ +export function isMacScreenProbeUnavailable(status: MacScreenAccessStatus) { + return ( + status === "missing-helper" || status === "error" || status === "exited" || status === "timeout" + ); +} + +function helperCandidates() { + const envPath = process.env.OPENSCREEN_SCK_CAPTURE_EXE?.trim(); + const appRoot = process.env.APP_ROOT ? path.resolve(process.env.APP_ROOT) : process.cwd(); + const archTag = process.arch === "arm64" ? "darwin-arm64" : "darwin-x64"; + const resourceRoot = + typeof process.resourcesPath === "string" + ? process.resourcesPath + : path.join(appRoot, "resources"); + + return [ + envPath, + path.join(appRoot, "electron", "native", "screencapturekit", "build", HELPER_NAME), + path.join(appRoot, "electron", "native", "bin", archTag, HELPER_NAME), + path.join(resourceRoot, "electron", "native", "bin", archTag, HELPER_NAME), + ].filter((candidate): candidate is string => Boolean(candidate)); +} + +export function findMacScreenAccessHelperPath() { + for (const candidate of helperCandidates()) { + try { + accessSync(candidate, fsConstants.X_OK); + return candidate; + } catch { + // Try the next helper location. + } + } + + return null; +} + +/** + * Reads the current Screen Recording grant, uncached. + * + * Never prompts and never blocks on the user: the helper calls the preflight function + * only, so this is safe to poll while macOS' own prompt is on screen. + */ +export async function readMacScreenCaptureAccess(): Promise { + if (process.platform !== "darwin") { + return { success: true, granted: true, status: "granted" }; + } + + const helperPath = findMacScreenAccessHelperPath(); + if (!helperPath) { + return { success: true, granted: false, status: "missing-helper" }; + } + + return new Promise((resolve) => { + const child = spawn(helperPath, [SCREEN_ACCESS_STATUS_FLAG], { + stdio: ["ignore", "pipe", "pipe"], + }); + let settled = false; + let lineBuffer = ""; + + const finish = (result: MacScreenAccessResult) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + if (!child.killed) { + child.kill("SIGTERM"); + } + resolve(result); + }; + + const timer = setTimeout(() => { + finish({ + success: false, + granted: false, + status: "timeout", + error: "Timed out reading the macOS screen recording permission", + }); + }, PROBE_TIMEOUT_MS); + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + lineBuffer += chunk; + const lines = lineBuffer.split(/\r?\n/); + lineBuffer = lines.pop() ?? ""; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) { + continue; + } + try { + const event = JSON.parse(trimmed) as { event?: string; granted?: boolean }; + if (event.event === "screen-access") { + finish({ + success: true, + granted: event.granted === true, + status: event.granted === true ? "granted" : "denied", + }); + return; + } + } catch { + // Ignore non-JSON helper output. + } + } + }); + + child.once("error", (error) => { + finish({ success: false, granted: false, status: "error", error: error.message }); + }); + + // `close`, not `exit`. This helper prints one line and dies, and `exit` can fire + // before stdout has been drained to the listener above -- which would report a + // perfectly good answer as a dead helper. `close` waits for the stdio streams. + // + // Reaching it at all means the helper ran and said nothing: an older build without + // the flag, which answers `invalidArguments` and exits 1. Reported as `exited` + // rather than a refusal, so the caller falls back to the app's own status instead + // of accusing the user of denying a permission they may well hold. + child.once("close", (code, signal) => { + finish({ + success: false, + granted: false, + status: "exited", + error: `macOS screen access probe exited (code=${code}, signal=${signal})`, + }); + }); + }); +} diff --git a/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift b/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift index 5add8074d..9f3cdf8ca 100644 --- a/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift +++ b/electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/ScreenCaptureRecorder.swift @@ -814,10 +814,35 @@ struct OpenScreenScreenCaptureKitHelper { _ = CGMainDisplayID() } + /// The flag that turns this helper into a one-shot answer to "may we record the + /// screen", printed as the usual single JSON line and nothing else. + private static let screenAccessStatusFlag = "--screen-access-status" + static func main() async { do { initializeCoreGraphicsWindowServerConnection() + // Answered from a process that exists for one read and then dies, because a FRESH + // PROCESS is the only place the answer can be trusted. + // `CGPreflightScreenCaptureAccess()` caches its result for the life of the calling + // process: once it has answered false it answers false forever, whatever the user + // does in System Settings afterwards. The app is long-lived, and Chromium's + // `getMediaAccessStatus("screen")` goes through that same function, so from the + // first miss until the next relaunch the app cannot observe its own permission + // being granted. That staleness -- not the missing prompt alone -- is what left + // the permission unreachable without a restart. + // + // Deliberately BEFORE the macOS 13 guard and the request decode below: the question + // is asked on every macOS the app supports, and answering it needs neither + // ScreenCaptureKit nor a recording request. + if CommandLine.arguments.count == 2, CommandLine.arguments[1] == screenAccessStatusFlag { + emit([ + "event": "screen-access", + "granted": CGPreflightScreenCaptureAccess(), + ]) + exit(0) + } + guard CommandLine.arguments.count == 2 else { throw HelperError.invalidArguments } From da5205512770021f3a64eb3970e81a487ab5d16b Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Mon, 31 Aug 2026 12:09:51 +0200 Subject: [PATCH 4/9] fix(macos): raise the screen recording prompt, and stop lying about the status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS collapses "never asked" into "denied": Chromium resolves the screen permission through a bool, so the `status === "not-determined"` guard never fired and a fresh install went straight to the "open System Settings" dialog with the OS never given the chance to ask. The decision now comes from whether the app has ever raised the prompt on this machine, which is the only honest way to tell a first run from a refusal — macOS will not tell us. It is an allowlist, not "anything but granted": a policy-restricted Mac can never grant the permission, so it keeps its actionable message instead of waiting for a prompt that cannot appear. `status` now always reports what the OS said. Whether to keep waiting rides on a separate `promptRaised` field, so no branch has to misreport a permission to keep the renderer's retry loop alive — and a user who refused on an earlier launch gets the dialog immediately, with no wait. Where the two reads disagree, the permission was granted after this process started and its cached read cannot see it. That divergence is the only reliable signal macOS leaves that a relaunch is needed, so the selector offers one instead of a picker it cannot fill. The prompt is still raised in-process, through `desktopCapturer`, so TCC records the grant against the app bundle rather than a bare child binary. It now respects the HEADLESS guard every other window show honours. --- electron/ipc/handlers.ts | 236 +++++++++++++++++++----- electron/ipc/screenAccessPrompt.test.ts | 143 ++++++++++---- electron/ipc/screenAccessPrompt.ts | 182 ++++++++++++++---- 3 files changed, 449 insertions(+), 112 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 085c08487..1d9b887a3 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -76,6 +76,10 @@ import { import { findPipeWireCursorHelperPath } from "../native-bridge/cursor/recording/pipeWireCursorRecordingSession"; import type { CursorRecordingSession } from "../native-bridge/cursor/recording/session"; import { toHelperRect } from "../native-bridge/helperCoordinates"; +import { + isMacScreenProbeUnavailable, + readMacScreenCaptureAccess, +} from "../native-bridge/screen/macScreenAccess"; import { scoreDeviceNameMatch } from "../recording/deviceNameMatching"; import { isSalvageableFragmentedCapture, @@ -90,7 +94,11 @@ import { patchWebmDurationOnDisk } from "../recording/webm-duration"; import { reindexRecordingOnDisk } from "../recording/webm-seek-index"; import { registerNativeBridgeHandlers } from "./nativeBridge"; import { RecordingStreamRegistry, registerRecordingStreamHandlers } from "./recordingStream"; -import { isAwaitingScreenPromptAnswer, shouldPromptForScreenAccess } from "./screenAccessPrompt"; +import { + resolveScreenAccessStatus, + ScreenPromptMarker, + shouldRaiseScreenPrompt, +} from "./screenAccessPrompt"; const PROJECT_FILE_EXTENSION = "openscreen"; export const SHORTCUTS_FILE = path.join(app.getPath("userData"), "shortcuts.json"); @@ -492,8 +500,49 @@ type AttachNativeMacWebcamRecordingInput = { let selectedSource: SelectedSource | null = null; let selectedDesktopSource: DesktopCapturerSource | null = null; -/** When macOS was asked for Screen Recording this launch, if it has been. */ -let screenAccessPromptedAt: number | null = null; +/** Whether this launch has already raised macOS' Screen Recording prompt. */ +let screenPromptRaisedThisLaunch = false; +/** Lazily opened so the userData path is only touched once the app needs it. */ +let screenPromptMarkerInstance: ScreenPromptMarker | null = null; + +/** Same flag windows.ts gates every show() on: no window server in the e2e runs. */ +const IS_HEADLESS = process.env.HEADLESS === "true"; + +function getScreenPromptMarker(): ScreenPromptMarker { + screenPromptMarkerInstance ??= new ScreenPromptMarker(app.getPath("userData")); + return screenPromptMarkerInstance; +} + +/** Options the renderer passes to `open-source-selector`. */ +export interface SourceSelectorOptions { + /** + * The renderer has finished waiting on macOS' prompt, so the "permission is required" + * dialog may open. Held back until then only because System Settings opening over that + * prompt is the bug this path exists to fix -- and the renderer, which owns the retry + * budget, is the only side that knows when the wait is over. + */ + screenPromptWaitElapsed?: boolean; +} + +export interface ScreenAccessResult { + success: boolean; + granted: boolean; + /** What the OS actually said. Never bent to steer the caller. */ + status: string; + /** + * Whether macOS' own prompt has been raised this launch and may still be unanswered. + * This, and not `status`, is what tells a caller to keep polling: macOS keeps + * reporting the permission as absent for the whole time its prompt is on screen. + */ + promptRaised: boolean; + /** + * The permission is held, but this process cannot see it -- its cached read predates + * the grant, so capture in this process would still fail. Only ever true on macOS. + */ + requiresRelaunch?: boolean; + error?: string; +} + let lastEnumeratedSources = new Map(); let currentProjectPath: string | null = null; let currentRecordingSession: RecordingSession | null = null; @@ -1742,55 +1791,123 @@ export function registerIpcHandlers( onRecordingStateChange?: (recording: boolean, sourceName: string) => void, _switchToHud?: () => void, ) { - async function requestScreenAccess() { + /** + * Raises macOS' Screen Recording prompt, and records that it was raised. + * + * The recording path warms the same `getSources` call before starting the native + * capture helper, and deliberately still does: routing it through here would steal + * window focus at the instant a recording begins. It can therefore raise the prompt + * without leaving a mark, which costs at most one wait on a prompt macOS declines to + * redraw -- and it is only reachable after the selector, so this function has almost + * always run first. + * + * Raised from THIS process rather than from the capture helper. `desktopCapturer` is + * the app's own call into Chromium, which calls `CGRequestScreenCaptureAccess()` under + * it, so TCC records the grant against the app bundle's designated requirement. A bare + * child binary asking on its own behalf is what pins a row to a raw cdhash, which + * survives reinstalls and reads to the user as a permission that is on and does not work. + */ + function raiseScreenRecordingPrompt() { + const mainWin = getMainWindow(); + if (mainWin && !mainWin.isDestroyed()) { + if (mainWin.isMinimized()) { + mainWin.restore(); + } + // Same HEADLESS guard every other show() in the app respects: the macOS + // Playwright specs run with no window server to steal activation from. + if (!IS_HEADLESS) { + if (!mainWin.isVisible()) { + mainWin.show(); + } + mainWin.focus(); + } + } + if (!IS_HEADLESS) { + app.focus({ steal: true }); + } + + // The only call that raises the prompt. It settles in milliseconds whether or not + // the prompt is still on screen, and rejects outright where the permission is + // missing, so nothing can be learned from awaiting it -- the answer is read back + // from a fresh process instead. + desktopCapturer + .getSources({ types: ["screen"], thumbnailSize: { width: 1, height: 1 } }) + .catch(() => { + // Permission probing failure is reported by the explicit status read. + }); + + screenPromptRaisedThisLaunch = true; + getScreenPromptMarker().recordRaised(new Date().toISOString()); + } + + async function requestScreenAccess(): Promise { if (process.platform !== "darwin") { - return { success: true, granted: true, status: "granted" }; + return { success: true, granted: true, status: "granted", promptRaised: false }; } try { - const status = systemPreferences.getMediaAccessStatus("screen"); - if (status === "granted") { - return { success: true, granted: true, status }; + // Two reads, and the difference between them is load bearing. + // + // `getMediaAccessStatus("screen")` is Chromium's `CGPreflightScreenCaptureAccess()`, + // which caches its answer for the life of this process: once it has said no it + // says no until the app is relaunched, whatever the user does in System Settings. + // The probe spawns a fresh process for every read, so it reports the permission as + // it stands now. See screenAccessPrompt.ts for why this is the whole bug. + const probe = await readMacScreenCaptureAccess(); + const appStatus = systemPreferences.getMediaAccessStatus("screen"); + const status = resolveScreenAccessStatus(probe, appStatus); + + if (isMacScreenProbeUnavailable(probe.status)) { + console.warn( + `[screen-access] permission probe unavailable (status=${probe.status}${ + probe.error ? `, error=${probe.error}` : "" + }); falling back to the app's own status=${appStatus}.`, + ); } - // Screen recording has no askForMediaAccess equivalent, so trigger the - // TCC prompt without opening OpenScreen's source selector above it. - // macOS reports a never-asked machine as "denied", so the decision has to - // come from shouldPromptForScreenAccess rather than the status alone. - // - // Report "not-determined" while that prompt is up: it is the status the - // renderer's retry loop polls on, and macOS keeps answering "denied" - // until the user actually accepts. - if (shouldPromptForScreenAccess(status, screenAccessPromptedAt)) { - screenAccessPromptedAt = Date.now(); - const mainWin = getMainWindow(); - if (mainWin && !mainWin.isDestroyed()) { - if (!mainWin.isVisible()) { - mainWin.show(); - } - mainWin.focus(); - } - app.focus({ steal: true }); - desktopCapturer - .getSources({ types: ["screen"], thumbnailSize: { width: 1, height: 1 } }) - .catch(() => { - // Permission probing failure is reported by the explicit status check below. - }); - return { success: true, granted: false, status: "not-determined" }; + if (status === "granted") { + // Held, but this process may not be able to use it: a grant made after the app + // started is invisible to the cached read the capture stack goes through. The + // disagreement between the two reads is exactly that situation, and it is the + // only reliable signal macOS leaves us that a relaunch is needed. + return { + success: true, + granted: true, + status, + promptRaised: false, + requiresRelaunch: appStatus !== "granted", + }; } - // Keep reporting "not-determined" while that prompt may still be up. - // macOS answers "denied" the whole time it is on screen, so returning the - // real status here would open System Settings over the prompt and stop - // the renderer's retry loop on its first poll. - if (isAwaitingScreenPromptAnswer(screenAccessPromptedAt, Date.now())) { - return { success: true, granted: false, status: "not-determined" }; + if ( + shouldRaiseScreenPrompt({ + status, + raisedThisLaunch: screenPromptRaisedThisLaunch, + raisedBefore: getScreenPromptMarker().hasRaisedBefore(), + }) + ) { + raiseScreenRecordingPrompt(); } - return { success: true, granted: false, status }; + // `status` is what the OS actually said, always. Whether the caller should keep + // polling rides on `promptRaised` instead -- a separate field for a separate + // question, so no branch here has to misreport the permission to keep the + // renderer's retry loop alive. + return { + success: true, + granted: false, + status, + promptRaised: screenPromptRaisedThisLaunch, + }; } catch (error) { console.error("Failed to request screen access:", error); - return { success: false, granted: false, status: "unknown", error: String(error) }; + return { + success: false, + granted: false, + status: "unknown", + promptRaised: screenPromptRaisedThisLaunch, + error: String(error), + }; } } @@ -1977,7 +2094,7 @@ export function registerIpcHandlers( return access; }); - ipcMain.handle("open-source-selector", async () => { + ipcMain.handle("open-source-selector", async (_event, options?: SourceSelectorOptions) => { // Nothing to open on Linux WHEN THE NATIVE HELPER IS THERE. The selector's // own `desktopCapturer.getSources()` raises a portal dialog — a SECOND // one, for a session that is thrown away — and whatever it returns cannot @@ -1992,8 +2109,43 @@ export function registerIpcHandlers( } const access = await requestScreenAccess(); + + // Held, but not by this process. Chromium's capture stack reads the permission + // through a value cached before the grant existed, so opening the picker here + // produces a source list it cannot fill and no way out of it. Apple's guidance -- + // and the warning System Settings prints beside the toggle -- is to relaunch, so + // offer exactly that instead of a picker that cannot work. + if (access.granted && access.requiresRelaunch) { + const mainWin = getMainWindow(); + const messageOptions = { + type: "info", + buttons: ["Restart OpenScreen", "Later"], + defaultId: 0, + cancelId: 1, + message: "Screen Recording permission granted", + detail: + "macOS applies this permission when OpenScreen restarts. Restart now to choose a screen or window.", + } satisfies Electron.MessageBoxOptions; + const result = + mainWin && !mainWin.isDestroyed() + ? await dialog.showMessageBox(mainWin, messageOptions) + : await dialog.showMessageBox(messageOptions); + if (result.response === 0) { + app.relaunch(); + app.quit(); + } + return { opened: false, reason: "screen-access-relaunch-required", access }; + } + if (!access.granted) { - if (process.platform === "darwin" && access.status !== "not-determined") { + // Withheld only while macOS' own prompt from this launch may still be on screen: + // opening System Settings over it is the bug this whole path exists to fix. Every + // other refusal gets the dialog immediately, including the second click of a + // launch and every launch after the first. + if ( + process.platform === "darwin" && + (!access.promptRaised || options?.screenPromptWaitElapsed === true) + ) { const mainWin = getMainWindow(); const messageOptions = { type: "warning", diff --git a/electron/ipc/screenAccessPrompt.test.ts b/electron/ipc/screenAccessPrompt.test.ts index c23375272..77fe8bc32 100644 --- a/electron/ipc/screenAccessPrompt.test.ts +++ b/electron/ipc/screenAccessPrompt.test.ts @@ -1,50 +1,129 @@ -import { describe, expect, it } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { - isAwaitingScreenPromptAnswer, - SCREEN_PROMPT_GRACE_MS, - shouldPromptForScreenAccess, + resolveScreenAccessStatus, + ScreenPromptMarker, + shouldRaiseScreenPrompt, } from "./screenAccessPrompt"; -describe("shouldPromptForScreenAccess", () => { - it("never prompts once the permission is granted", () => { - expect(shouldPromptForScreenAccess("granted", null)).toBe(false); - expect(shouldPromptForScreenAccess("granted", 1_000)).toBe(false); +describe("resolveScreenAccessStatus", () => { + it("reports what the fresh probe read, not the app's cached status", () => { + // The regression this guards: the app's own read is frozen at whatever it saw + // first, so a grant made while the app is running is invisible to it. + expect(resolveScreenAccessStatus({ granted: true, status: "granted" }, "denied")).toBe( + "granted", + ); + expect(resolveScreenAccessStatus({ granted: false, status: "denied" }, "granted")).toBe( + "denied", + ); }); - it("prompts on the first ask of a launch even though macOS reports denied", () => { - // The regression this guards: macOS collapses "never asked" into "denied", - // so a first run used to skip the prompt entirely. - expect(shouldPromptForScreenAccess("denied", null)).toBe(true); + it("falls back to the app's status when the probe could not answer", () => { + // A build without the helper, or a helper that crashed, must land on the old + // behaviour rather than accusing the user of refusing the permission. + for (const status of ["missing-helper", "error", "exited", "timeout"] as const) { + expect(resolveScreenAccessStatus({ granted: false, status }, "restricted")).toBe( + "restricted", + ); + } + }); +}); + +describe("shouldRaiseScreenPrompt", () => { + it("raises the prompt on the first ask, even though macOS reports denied", () => { + // The bug: macOS collapses "never asked" into "denied", so a first run skipped + // the prompt entirely and offered only a manual toggle in System Settings. + expect( + shouldRaiseScreenPrompt({ status: "denied", raisedThisLaunch: false, raisedBefore: false }), + ).toBe(true); + }); + + it("never prompts once the permission is held", () => { + expect( + shouldRaiseScreenPrompt({ status: "granted", raisedThisLaunch: false, raisedBefore: false }), + ).toBe(false); }); - it("stops prompting after this launch has already asked", () => { - // Lets the caller report the real status so the Settings dialog takes over - // instead of re-prompting on every click. - expect(shouldPromptForScreenAccess("denied", 1_000)).toBe(false); - expect(shouldPromptForScreenAccess("restricted", 1_000)).toBe(false); + it("does not re-prompt within a launch", () => { + expect( + shouldRaiseScreenPrompt({ status: "denied", raisedThisLaunch: true, raisedBefore: false }), + ).toBe(false); }); - it("still prompts on not-determined, whatever this launch has already asked", () => { - expect(shouldPromptForScreenAccess("not-determined", null)).toBe(true); - expect(shouldPromptForScreenAccess("not-determined", 1_000)).toBe(true); + it("does not prompt a user who has already been asked on this machine", () => { + // macOS shows the prompt once per TCC decision and ignores every later request. + // Asking again would cost a refusing user the Settings dialog -- the one message + // they can act on -- in exchange for a prompt that never appears. + expect( + shouldRaiseScreenPrompt({ status: "denied", raisedThisLaunch: false, raisedBefore: true }), + ).toBe(false); + }); + + it("leaves a policy-restricted Mac on its actionable message", () => { + // The cell that a status-blind "anything but granted" rule gets wrong: an MDM + // machine can never grant the permission, so a prompt cannot appear and the wait + // for one costs the user the only message that explains the refusal. + expect( + shouldRaiseScreenPrompt({ + status: "restricted", + raisedThisLaunch: false, + raisedBefore: false, + }), + ).toBe(false); + expect( + shouldRaiseScreenPrompt({ status: "unknown", raisedThisLaunch: false, raisedBefore: false }), + ).toBe(false); + }); + + it("still prompts on not-determined, for a platform that can report it", () => { + expect( + shouldRaiseScreenPrompt({ + status: "not-determined", + raisedThisLaunch: false, + raisedBefore: false, + }), + ).toBe(true); }); }); -describe("isAwaitingScreenPromptAnswer", () => { - it("is not awaiting anything before the prompt has been raised", () => { - expect(isAwaitingScreenPromptAnswer(null, 10_000)).toBe(false); +describe("ScreenPromptMarker", () => { + let userData: string; + + beforeEach(() => { + userData = mkdtempSync(path.join(tmpdir(), "openscreen-screen-access-")); }); - it("holds the real status back while the prompt may still be on screen", () => { - // Without this the Settings dialog opens over the native prompt and the - // renderer's retry loop aborts on its first poll, because macOS keeps - // answering "denied" until the user actually accepts. - expect(isAwaitingScreenPromptAnswer(1_000, 1_000)).toBe(true); - expect(isAwaitingScreenPromptAnswer(1_000, 1_000 + SCREEN_PROMPT_GRACE_MS - 1)).toBe(true); + afterEach(() => { + rmSync(userData, { recursive: true, force: true }); + vi.restoreAllMocks(); }); - it("releases the real status once the grace window lapses", () => { - expect(isAwaitingScreenPromptAnswer(1_000, 1_000 + SCREEN_PROMPT_GRACE_MS)).toBe(false); - expect(isAwaitingScreenPromptAnswer(1_000, 60_000)).toBe(false); + it("starts unmarked on a machine that has never been asked", () => { + expect(new ScreenPromptMarker(userData).hasRaisedBefore()).toBe(false); + }); + + it("remembers across launches that the prompt was raised", () => { + new ScreenPromptMarker(userData).recordRaised("2026-08-31T00:00:00.000Z"); + + expect(new ScreenPromptMarker(userData).hasRaisedBefore()).toBe(true); + }); + + it("treats an unreadable or malformed marker as never asked", () => { + writeFileSync(path.join(userData, "screen-access.json"), "{ not json", "utf8"); + + expect(new ScreenPromptMarker(userData).hasRaisedBefore()).toBe(false); + }); + + it("still gets the rest of the launch right when the marker cannot be written", () => { + // An unwritable userData directory must not stop the prompt the user is waiting on. + const marker = new ScreenPromptMarker(path.join(userData, "does", "not", "exist")); + vi.spyOn(console, "warn").mockImplementation(() => { + // The failed write logs; keep the test output readable. + }); + + expect(() => marker.recordRaised("2026-08-31T00:00:00.000Z")).not.toThrow(); + expect(marker.hasRaisedBefore()).toBe(true); }); }); diff --git a/electron/ipc/screenAccessPrompt.ts b/electron/ipc/screenAccessPrompt.ts index 76fa4f93a..37f0958dc 100644 --- a/electron/ipc/screenAccessPrompt.ts +++ b/electron/ipc/screenAccessPrompt.ts @@ -1,53 +1,159 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + /** - * How long after raising the native prompt to keep reporting "not-determined". + * Deciding when to raise macOS' own Screen Recording prompt, and when to stop. + * + * Two macOS facts drive everything here, and both are the opposite of what the old + * code assumed: + * + * 1. `getMediaAccessStatus("screen")` cannot report "not-determined". Chromium resolves + * it through `CGPreflightScreenCaptureAccess()`, a bool, so a machine that has never + * been asked is reported exactly like an explicit refusal. Gating the prompt on + * "not-determined" therefore never fired, and a fresh install fell straight through + * to the "open System Settings" dialog with macOS never given the chance to ask. * - * Matches the renderer's retry budget in `openSourceSelectorFlow` (8 attempts, - * 750ms apart), which is the window the user has to answer the prompt before - * the Settings dialog takes over. + * 2. That same preflight caches its answer for the life of the calling process. The app + * is long-lived, so once it has read false it reads false until relaunch, whatever the + * user does in System Settings. This is why polling the app's own status after raising + * the prompt can never observe a grant -- no delay tunes into correctness -- and why + * the real answer is read from a fresh child process instead + * (`native-bridge/screen/macScreenAccess.ts`). + * + * What is left is one thing macOS genuinely will not tell us: whether a TCC decision + * already exists. "Never asked" and "refused" are the same bool. So the app records for + * itself whether it has ever raised the prompt on this machine, which is the only honest + * way to tell a first run from a user who said no -- and it is what keeps a refusing user + * on the immediate, actionable Settings dialog instead of a wait for a prompt that macOS + * will never show them again. */ -export const SCREEN_PROMPT_GRACE_MS = 6_000; + +/** Statuses the fresh-process probe can report; mirrors MacScreenAccessStatus. */ +export type ScreenAccessProbeStatus = + | "granted" + | "denied" + | "missing-helper" + | "error" + | "exited" + | "timeout"; + +export interface ScreenAccessProbe { + granted: boolean; + status: ScreenAccessProbeStatus; +} + +/** True when the probe never got far enough to answer the permission question. */ +function probeAnswered(status: ScreenAccessProbeStatus) { + return status === "granted" || status === "denied"; +} /** - * Decides whether to raise macOS' own Screen Recording prompt. - * - * `systemPreferences.getMediaAccessStatus("screen")` cannot answer - * "not-determined" on macOS. Chromium resolves that permission through - * `CGPreflightScreenCaptureAccess()`, a bool, so a machine that has never been - * asked is reported exactly like an explicit refusal — both arrive as "denied". - * Gating the prompt on `status === "not-determined"` therefore never fires: a - * fresh install falls straight through to the "open System Settings" dialog and - * macOS is never given the chance to ask, so the only way to grant is a manual - * toggle. The renderer's permission-retry loop arms on the same status and is - * dead for the same reason. - * - * Drive the first prompt off whether this launch has already asked instead. - * Asking once per launch keeps a genuine refusal from re-prompting on every - * click, and lets a later call report the real status so the Settings dialog - * still reaches a user who said no. + * The status to report, preferring the uncached probe over the app's own stale read. + * + * Falls back to whatever `getMediaAccessStatus` said when the probe could not answer -- + * no helper in the build, a loader failure, a crash, a hang. That fallback is the old + * behaviour exactly, so a build without the helper is no worse off than before, and a + * broken helper never becomes a permission refusal in the user's face. + */ +export function resolveScreenAccessStatus( + probe: ScreenAccessProbe, + fallbackStatus: string, +): string { + return probeAnswered(probe.status) ? probe.status : fallbackStatus; +} + +export interface ScreenPromptDecisionInput { + /** The resolved permission status, preferring the fresh probe over the app's own read. */ + status: string; + /** Whether this launch has already raised the prompt. */ + raisedThisLaunch: boolean; + /** Whether this app has ever raised the prompt on this machine. */ + raisedBefore: boolean; +} + +/** + * Whether to raise macOS' prompt now. + * + * Only on the first ask ever, and only where a grant is actually reachable. macOS shows + * the prompt once per TCC decision and silently ignores every later request, so asking + * again buys nothing -- while the window-focus steal that goes with it, and the wait for + * an answer that cannot come, cost a user who has already refused the one message they + * can act on. */ -export function shouldPromptForScreenAccess(status: string, promptedAt: number | null): boolean { +export function shouldRaiseScreenPrompt({ + status, + raisedThisLaunch, + raisedBefore, +}: ScreenPromptDecisionInput): boolean { if (status === "granted") { return false; } - return status === "not-determined" || promptedAt === null; + // An allowlist, not "anything that is not granted". `restricted` means policy + // forbids the grant -- an MDM-managed Mac -- so no prompt can appear and the user + // cannot act on one. Sending those machines down the prompt path would swap their + // only actionable message for a wait on an answer that is never coming. + if (status !== "denied" && status !== "not-determined") { + return false; + } + + return !raisedThisLaunch && !raisedBefore; +} + +const MARKER_FILE = "screen-access.json"; + +interface ScreenPromptMarkerFile { + /** ISO timestamp of the first time this app raised the macOS prompt here. */ + promptRaisedAt?: string; } /** - * Whether the native prompt raised at `promptedAt` may still be waiting for an - * answer, and the real status should be withheld until it is. - * - * macOS gives us nothing to observe here. `desktopCapturer.getSources()` settles - * in a few milliseconds whether or not the prompt is still on screen (measured - * at 4ms on macOS 26.2), and the status stays "denied" for the whole time the - * prompt is up — it only ever flips once the user accepts. So an in-flight flag - * around that call covers nothing, and reporting "denied" straight away would - * open System Settings over the prompt and abort the renderer's retry loop on - * its first poll. - * - * Treating the grace window as "still asking" keeps the loop polling long enough - * to notice an accept, and lets the Settings dialog through once it lapses. + * Remembers, across launches, that the macOS prompt has been raised on this machine. + * + * Deliberately a plain file rather than anything derived from TCC: the TCC database is + * unreadable without disabling SIP, and its "is there a decision" bit is exactly what + * the OS refuses to expose. This is the app's own note to itself, and it is only ever + * used to choose between "raise the prompt" and "show the dialog" -- never as an answer + * to whether the permission is held, which always comes from a live read. + * + * A stale marker (the user reset TCC with `tccutil`) costs the prompt on the next run and + * leaves the Settings dialog, which still works. A missing one costs a duplicate prompt + * that macOS discards. Both fail towards a message the user can act on. */ -export function isAwaitingScreenPromptAnswer(promptedAt: number | null, now: number): boolean { - return promptedAt !== null && now - promptedAt < SCREEN_PROMPT_GRACE_MS; +export class ScreenPromptMarker { + private readonly markerPath: string; + private raised: boolean; + + constructor(userDataPath: string) { + this.markerPath = path.join(userDataPath, MARKER_FILE); + this.raised = this.loadSync(); + } + + private loadSync(): boolean { + try { + const parsed = JSON.parse(readFileSync(this.markerPath, "utf8")) as ScreenPromptMarkerFile; + return typeof parsed.promptRaisedAt === "string"; + } catch { + return false; + } + } + + hasRaisedBefore(): boolean { + return this.raised; + } + + /** + * Records that the prompt has been raised. Best-effort on disk: an unwritable + * userData directory must not stop the prompt the user is waiting on, and the + * in-memory flag still gets the rest of this launch right. + */ + recordRaised(nowIso: string): void { + this.raised = true; + try { + const payload: ScreenPromptMarkerFile = { promptRaisedAt: nowIso }; + writeFileSync(this.markerPath, JSON.stringify(payload), "utf8"); + } catch (error) { + console.warn("Failed to persist the screen prompt marker:", error); + } + } } From b424ee0dc083a7d7d62eda9a268f79223b952694 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Mon, 31 Aug 2026 12:10:05 +0200 Subject: [PATCH 5/9] fix(launch): let the permission wait end in a dialog instead of silence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retry loop exited by synthesizing `{opened:false, reason:"screen-access-required"}`, which `LaunchWindow` discards — it only handles `opened` and `portal-owns-selection`. So running the wait out left the user with nothing at all: the "Screen Recording permission is required" dialog lives in `open-source-selector`, and the loop polled `request-screen-access`, which has none. It now goes back through the main process and says the wait is over, which is the handshake that releases the dialog. The renderer owns the retry budget, so it is the only side that knows when to release it — and holding the dialog back at all is only to keep System Settings from opening over the native prompt, which is the bug this path exists to fix. The loop arms on `promptRaised` rather than a status macOS cannot report, and its success exit is reachable for the first time: the main process now reads the permission from a fresh process, so a grant made while the app is running is visible to it. --- electron/electron-env.d.ts | 27 ++++- electron/preload.ts | 4 +- src/components/launch/LaunchWindow.test.tsx | 1 + src/components/launch/LaunchWindow.tsx | 2 +- .../launch/openSourceSelectorFlow.test.ts | 104 +++++++++++++----- .../launch/openSourceSelectorFlow.ts | 58 ++++++++-- .../architecture/decisions.md | 3 + tsconfig.node.tsbuildinfo | 1 + 8 files changed, 159 insertions(+), 41 deletions(-) create mode 100644 tsconfig.node.tsbuildinfo diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index e140a4e37..681b69c64 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -37,13 +37,29 @@ interface Window { switchToEditor: () => Promise; switchToHud: () => Promise; startNewRecording: () => Promise<{ success: boolean; error?: string }>; - openSourceSelector: () => Promise<{ + openSourceSelector: (options?: { + /** + * The renderer has finished waiting on macOS' Screen Recording prompt, so the + * "permission is required" dialog may open. Held back until then only because + * System Settings opening over that prompt is the bug this path exists to fix. + */ + screenPromptWaitElapsed?: boolean; + }) => Promise<{ opened: boolean; reason?: string; access?: { success: boolean; granted: boolean; + /** What the OS actually said. Never bent to steer the caller. */ status: string; + /** + * macOS' own prompt was raised this launch and may still be unanswered. This, and + * not `status`, is what tells the renderer to keep polling: macOS reports the + * permission as absent for the whole time its prompt is on screen. + */ + promptRaised: boolean; + /** Granted, but this process cannot see it until the app is relaunched. */ + requiresRelaunch?: boolean; error?: string; }; }>; @@ -75,7 +91,16 @@ interface Window { requestScreenAccess: () => Promise<{ success: boolean; granted: boolean; + /** What the OS actually said. Never bent to steer the caller. */ status: string; + /** + * macOS' own prompt was raised this launch and may still be unanswered. This, and + * not `status`, is what tells the renderer to keep polling: macOS reports the + * permission as absent for the whole time its prompt is on screen. + */ + promptRaised: boolean; + /** Granted, but this process cannot see it until the app is relaunched. */ + requiresRelaunch?: boolean; error?: string; }>; requestNativeMacCursorAccess: () => Promise<{ diff --git a/electron/preload.ts b/electron/preload.ts index 6aff16407..bab24a150 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -109,8 +109,8 @@ contextBridge.exposeInMainWorld("electronAPI", { startNewRecording: () => { return ipcRenderer.invoke("start-new-recording"); }, - openSourceSelector: () => { - return ipcRenderer.invoke("open-source-selector"); + openSourceSelector: (options?: { screenPromptWaitElapsed?: boolean }) => { + return ipcRenderer.invoke("open-source-selector", options); }, openNotes: () => { return ipcRenderer.invoke("open-notes"); diff --git a/src/components/launch/LaunchWindow.test.tsx b/src/components/launch/LaunchWindow.test.tsx index 60894f9e0..2438e15c5 100644 --- a/src/components/launch/LaunchWindow.test.tsx +++ b/src/components/launch/LaunchWindow.test.tsx @@ -211,6 +211,7 @@ function stubElectronAPI(getSelectedSource: Window["electronAPI"]["getSelectedSo success: true, granted: true, status: "granted", + promptRaised: false, })), // Follows the platform under test. Pinned to "darwin" before, which was // invisible while only `nativeBridgeClient` was consulted for it — and diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 8a2427fc1..3a1d897cf 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -589,7 +589,7 @@ export function LaunchWindow() { const openSourceSelector = useCallback(async () => { if (window.electronAPI) { return await openSourceSelectorWithPermissionRetry({ - openSourceSelector: () => window.electronAPI.openSourceSelector(), + openSourceSelector: (options) => window.electronAPI.openSourceSelector(options), requestScreenAccess: () => window.electronAPI.requestScreenAccess(), }); } diff --git a/src/components/launch/openSourceSelectorFlow.test.ts b/src/components/launch/openSourceSelectorFlow.test.ts index ac80f1adc..63ca04543 100644 --- a/src/components/launch/openSourceSelectorFlow.test.ts +++ b/src/components/launch/openSourceSelectorFlow.test.ts @@ -1,6 +1,13 @@ import { describe, expect, it, vi } from "vitest"; import { openSourceSelectorWithPermissionRetry } from "./openSourceSelectorFlow"; +/** What the main process returns when it has just raised macOS' prompt. */ +const promptRaised = { + opened: false, + reason: "screen-access-required", + access: { success: true, granted: false, status: "denied", promptRaised: true }, +} as const; + describe("openSourceSelectorWithPermissionRetry", () => { it("returns immediately when the source selector opens on the first attempt", async () => { const openSourceSelector = vi.fn().mockResolvedValue({ opened: true }); @@ -17,19 +24,49 @@ describe("openSourceSelectorWithPermissionRetry", () => { expect(requestScreenAccess).not.toHaveBeenCalled(); }); - it("retries opening after macOS screen permission becomes granted", async () => { + it("does not wait when no prompt was raised, so the dialog is not delayed", async () => { + // A user who refused on an earlier launch: macOS will not show them the prompt + // again, so there is nothing to wait for and the Settings dialog has already been + // shown by the main process. + const openSourceSelector = vi.fn().mockResolvedValue({ + opened: false, + reason: "screen-access-required", + access: { success: true, granted: false, status: "denied", promptRaised: false }, + }); + const requestScreenAccess = vi.fn(); + + const result = await openSourceSelectorWithPermissionRetry({ + openSourceSelector, + requestScreenAccess, + wait: vi.fn(), + }); + + expect(result.opened).toBe(false); + expect(requestScreenAccess).not.toHaveBeenCalled(); + expect(openSourceSelector).toHaveBeenCalledTimes(1); + }); + + it("reopens the selector once the permission is granted while waiting", async () => { + // Reachable only because the main process reads the permission from a fresh + // process. Through its own cached status it could never observe the grant. const openSourceSelector = vi .fn() - .mockResolvedValueOnce({ - opened: false, - reason: "screen-access-required", - access: { success: true, granted: false, status: "not-determined" }, - }) + .mockResolvedValueOnce(promptRaised) .mockResolvedValueOnce({ opened: true }); const requestScreenAccess = vi .fn() - .mockResolvedValueOnce({ success: true, granted: false, status: "not-determined" }) - .mockResolvedValueOnce({ success: true, granted: true, status: "granted" }); + .mockResolvedValueOnce({ + success: true, + granted: false, + status: "denied", + promptRaised: true, + }) + .mockResolvedValueOnce({ + success: true, + granted: true, + status: "granted", + promptRaised: true, + }); const wait = vi.fn().mockResolvedValue(undefined); const result = await openSourceSelectorWithPermissionRetry({ @@ -41,33 +78,48 @@ describe("openSourceSelectorWithPermissionRetry", () => { expect(result).toEqual({ opened: true }); expect(wait).toHaveBeenCalledTimes(2); - expect(requestScreenAccess).toHaveBeenCalledTimes(2); + expect(openSourceSelector).toHaveBeenLastCalledWith(); + }); + + it("asks the main process for its dialog once the wait is spent", async () => { + // The regression this guards: running the budget out used to return a synthesized + // result that LaunchWindow discards, so a user who refused got six seconds of + // nothing followed by nothing at all. + const denied = { success: true, granted: false, status: "denied", promptRaised: true }; + const openSourceSelector = vi.fn().mockResolvedValue(promptRaised); + const requestScreenAccess = vi.fn().mockResolvedValue(denied); + + await openSourceSelectorWithPermissionRetry({ + openSourceSelector, + requestScreenAccess, + wait: vi.fn().mockResolvedValue(undefined), + maxAttempts: 3, + }); + + expect(requestScreenAccess).toHaveBeenCalledTimes(3); expect(openSourceSelector).toHaveBeenCalledTimes(2); + expect(openSourceSelector).toHaveBeenLastCalledWith({ screenPromptWaitElapsed: true }); }); - it("stops retrying once macOS permission is explicitly denied", async () => { - const openSourceSelector = vi.fn().mockResolvedValue({ - opened: false, - reason: "screen-access-required", - access: { success: true, granted: false, status: "not-determined" }, + it("stops waiting immediately on a status no prompt can resolve", async () => { + // An MDM-managed Mac cannot grant the permission at all. Waiting it out would cost + // the user the one message that explains why. + const openSourceSelector = vi.fn().mockResolvedValue(promptRaised); + const requestScreenAccess = vi.fn().mockResolvedValue({ + success: true, + granted: false, + status: "restricted", + promptRaised: true, }); - const requestScreenAccess = vi - .fn() - .mockResolvedValueOnce({ success: true, granted: false, status: "denied" }); - const result = await openSourceSelectorWithPermissionRetry({ + await openSourceSelectorWithPermissionRetry({ openSourceSelector, requestScreenAccess, - wait: vi.fn(), - maxAttempts: 4, + wait: vi.fn().mockResolvedValue(undefined), + maxAttempts: 8, }); - expect(result).toEqual({ - opened: false, - reason: "screen-access-required", - access: { success: true, granted: false, status: "denied" }, - }); expect(requestScreenAccess).toHaveBeenCalledTimes(1); - expect(openSourceSelector).toHaveBeenCalledTimes(1); + expect(openSourceSelector).toHaveBeenLastCalledWith({ screenPromptWaitElapsed: true }); }); }); diff --git a/src/components/launch/openSourceSelectorFlow.ts b/src/components/launch/openSourceSelectorFlow.ts index 540f1fcef..962892e07 100644 --- a/src/components/launch/openSourceSelectorFlow.ts +++ b/src/components/launch/openSourceSelectorFlow.ts @@ -1,7 +1,18 @@ export type ScreenAccessResult = { success: boolean; granted: boolean; + /** What the OS actually said. Never bent to steer this loop. */ status: string; + /** + * macOS' own prompt was raised this launch and may still be unanswered. + * + * This, and not `status`, is what arms the wait below. macOS reports the permission + * as absent for the whole time its prompt is on screen -- it only flips once the user + * accepts -- so a loop keyed on the status would abort on its first poll, every time. + */ + promptRaised?: boolean; + /** Granted, but the main process cannot use it until the app is relaunched. */ + requiresRelaunch?: boolean; error?: string; }; @@ -11,8 +22,13 @@ export type OpenSourceSelectorResult = { access?: ScreenAccessResult; }; +export type OpenSourceSelectorOptions = { + /** Tells the main process this loop has stopped waiting on macOS' prompt. */ + screenPromptWaitElapsed?: boolean; +}; + type OpenSourceSelectorFlowOptions = { - openSourceSelector: () => Promise; + openSourceSelector: (options?: OpenSourceSelectorOptions) => Promise; requestScreenAccess: () => Promise; wait?: (ms: number) => Promise; retryDelayMs?: number; @@ -21,14 +37,31 @@ type OpenSourceSelectorFlowOptions = { const defaultWait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); -function shouldRetryAfterPermissionPrompt(result: OpenSourceSelectorResult): boolean { +/** + * Whether macOS' Screen Recording prompt is up and worth waiting on. + * + * The main process holds its "permission is required" dialog back for exactly as long as + * this is true, because opening System Settings over the native prompt is the bug the + * whole path exists to fix. Which makes this loop the owner of that wait -- and the only + * side that can say when it is over. + */ +function shouldWaitForPermissionPrompt(result: OpenSourceSelectorResult): boolean { return ( result.opened === false && result.reason === "screen-access-required" && - result.access?.status === "not-determined" + result.access?.promptRaised === true ); } +/** + * True for a status no prompt can resolve -- policy-restricted by an MDM profile, or a + * permission read that failed outright. Waiting those out costs the user the message that + * explains the refusal and buys nothing: no answer is coming. + */ +function isUnanswerableStatus(status: string): boolean { + return status !== "denied" && status !== "not-determined"; +} + export async function openSourceSelectorWithPermissionRetry({ openSourceSelector, requestScreenAccess, @@ -37,7 +70,7 @@ export async function openSourceSelectorWithPermissionRetry({ maxAttempts = 8, }: OpenSourceSelectorFlowOptions): Promise { const initialResult = await openSourceSelector(); - if (!shouldRetryAfterPermissionPrompt(initialResult)) { + if (!shouldWaitForPermissionPrompt(initialResult)) { return initialResult; } @@ -45,18 +78,21 @@ export async function openSourceSelectorWithPermissionRetry({ await wait(retryDelayMs); const access = await requestScreenAccess(); + // Reachable now: the main process reads the permission from a fresh process, so a + // grant made while the app is running is visible to it. Read through the app's own + // cached status -- as this loop used to be -- this branch could never be taken. if (access.granted) { return openSourceSelector(); } - if (access.status !== "not-determined") { - return { - opened: false, - reason: "screen-access-required", - access, - }; + if (isUnanswerableStatus(access.status)) { + return openSourceSelector({ screenPromptWaitElapsed: true }); } } - return initialResult; + // The budget is spent, so the prompt has been answered, dismissed, or was never shown. + // Going back through the main process -- rather than synthesizing a result here, which + // LaunchWindow discards -- is what puts the "permission is required" dialog in front of + // a user who refused. Without this, running the wait out ended in silence. + return openSourceSelector({ screenPromptWaitElapsed: true }); } diff --git a/technical-documentation/architecture/decisions.md b/technical-documentation/architecture/decisions.md index ac249fe6c..581356c9d 100644 --- a/technical-documentation/architecture/decisions.md +++ b/technical-documentation/architecture/decisions.md @@ -25,6 +25,7 @@ A decision leaves this list only when the code stops honouring it. | **The project file extension is `.openscreen`.** Builds that wrote `.axcut` are read and renamed forward on first open. | Users already recognise the extension; `electron/ai-edition/document-service.ts:23` holds both. | | **Migrations are forward-only.** A document is migrated up to the current `schemaVersion` on open and never written back down. | Round-tripping through an older schema loses fields silently. | | **Captions are derived from the transcript, not injected as annotations.** | The earlier design generated annotation objects from captions, which then drifted from the transcript the moment either was edited. The transcript is the SSOT for spoken words. | +| **macOS' Screen Recording permission is read from a fresh child process, never from the app's own status.** The prompt is still raised in-process, through `desktopCapturer`, so TCC attributes the grant to the app bundle. `electron/native-bridge/screen/macScreenAccess.ts`. | `CGPreflightScreenCaptureAccess()` caches its answer for the life of the calling process, and Electron's `getMediaAccessStatus("screen")` is that same function. A long-lived app therefore cannot observe its own permission being granted, which is why polling it after the prompt could never succeed. A process spawned per read has no cache to be stale. See [`screenAccessPrompt.ts`](../../electron/ipc/screenAccessPrompt.ts). | | **One package, one repository.** No sidecar process, no local HTTP server, no monorepo. | The editing engine was adopted from a project that had a Python worker and a Fastify server; both were replaced by in-process TypeScript and Electron IPC. Adding a second runtime back is a large, permanent cost. | ## Rejected, with the reason @@ -41,6 +42,8 @@ contradicts the reason given. | **Proxy MP4 files for scrubbing** | Dropped in favour of streaming decode. If long-recording scrub latency becomes the top complaint again, the revival path is a per-asset "generate proxy" action, not a background pass over every import. | | **Server-sent events for project changes** | Meaningless in a single-user desktop app; the document store already notifies every subscriber. | | **Auto-generating annotations from captions** | See "captions are derived" above. | +| **An in-flight flag around `desktopCapturer.getSources()`** to hold the Settings dialog back while macOS' prompt is up | Measured on macOS 26.2 / Electron 41.2.1 ([PR #302](https://github.com/getopenscreen/openscreen/pull/302)): the call settles in 4ms whether or not the prompt is still on screen, so the flag would cover four milliseconds of a wait that lasts as long as the user takes. Note the measurement was taken on a machine that had already refused, where the call short-circuits. | +| **Reporting `not-determined` for a grace window after prompting**, to keep the renderer's retry loop alive | Built, then rejected: it cannot be tuned into correctness. The loop it kept alive polled the app's own cached status, which never flips, so no window length would have let it succeed. It also cost a user who had genuinely refused their only actionable message, and made `status` mean two things at once. Replaced by an honest `promptRaised` field plus a fresh-process read. | | **React Query for the agent layer** | Planned during the merge, never adopted; the dependency is not in `package.json`. Plain IPC plus the document store covers it. | ## Surfaces that were removed diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo new file mode 100644 index 000000000..100624c00 --- /dev/null +++ b/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["../../../node_modules/typescript/lib/lib.d.ts","../../../node_modules/typescript/lib/lib.es5.d.ts","../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../node_modules/typescript/lib/lib.dom.d.ts","../../../node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../../node_modules/typescript/lib/lib.scripthost.d.ts","../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../node_modules/typescript/lib/lib.decorators.d.ts","../../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../../node_modules/@types/node/compatibility/disposable.d.ts","../../../node_modules/@types/node/compatibility/indexable.d.ts","../../../node_modules/@types/node/compatibility/iterators.d.ts","../../../node_modules/@types/node/compatibility/index.d.ts","../../../node_modules/@types/node/globals.typedarray.d.ts","../../../node_modules/@types/node/buffer.buffer.d.ts","../../../node_modules/@types/node/globals.d.ts","../../../node_modules/@types/node/web-globals/abortcontroller.d.ts","../../../node_modules/@types/node/web-globals/domexception.d.ts","../../../node_modules/@types/node/web-globals/events.d.ts","../../../node_modules/undici-types/header.d.ts","../../../node_modules/undici-types/readable.d.ts","../../../node_modules/undici-types/file.d.ts","../../../node_modules/undici-types/fetch.d.ts","../../../node_modules/undici-types/formdata.d.ts","../../../node_modules/undici-types/connector.d.ts","../../../node_modules/undici-types/client.d.ts","../../../node_modules/undici-types/errors.d.ts","../../../node_modules/undici-types/dispatcher.d.ts","../../../node_modules/undici-types/global-dispatcher.d.ts","../../../node_modules/undici-types/global-origin.d.ts","../../../node_modules/undici-types/pool-stats.d.ts","../../../node_modules/undici-types/pool.d.ts","../../../node_modules/undici-types/handlers.d.ts","../../../node_modules/undici-types/balanced-pool.d.ts","../../../node_modules/undici-types/agent.d.ts","../../../node_modules/undici-types/mock-interceptor.d.ts","../../../node_modules/undici-types/mock-agent.d.ts","../../../node_modules/undici-types/mock-client.d.ts","../../../node_modules/undici-types/mock-pool.d.ts","../../../node_modules/undici-types/mock-errors.d.ts","../../../node_modules/undici-types/proxy-agent.d.ts","../../../node_modules/undici-types/env-http-proxy-agent.d.ts","../../../node_modules/undici-types/retry-handler.d.ts","../../../node_modules/undici-types/retry-agent.d.ts","../../../node_modules/undici-types/api.d.ts","../../../node_modules/undici-types/interceptors.d.ts","../../../node_modules/undici-types/util.d.ts","../../../node_modules/undici-types/cookies.d.ts","../../../node_modules/undici-types/patch.d.ts","../../../node_modules/undici-types/websocket.d.ts","../../../node_modules/undici-types/eventsource.d.ts","../../../node_modules/undici-types/filereader.d.ts","../../../node_modules/undici-types/diagnostics-channel.d.ts","../../../node_modules/undici-types/content-type.d.ts","../../../node_modules/undici-types/cache.d.ts","../../../node_modules/undici-types/index.d.ts","../../../node_modules/@types/node/web-globals/fetch.d.ts","../../../node_modules/@types/node/web-globals/navigator.d.ts","../../../node_modules/@types/node/web-globals/storage.d.ts","../../../node_modules/@types/node/assert.d.ts","../../../node_modules/@types/node/assert/strict.d.ts","../../../node_modules/@types/node/async_hooks.d.ts","../../../node_modules/@types/node/buffer.d.ts","../../../node_modules/@types/node/child_process.d.ts","../../../node_modules/@types/node/cluster.d.ts","../../../node_modules/@types/node/console.d.ts","../../../node_modules/@types/node/constants.d.ts","../../../node_modules/@types/node/crypto.d.ts","../../../node_modules/@types/node/dgram.d.ts","../../../node_modules/@types/node/diagnostics_channel.d.ts","../../../node_modules/@types/node/dns.d.ts","../../../node_modules/@types/node/dns/promises.d.ts","../../../node_modules/@types/node/domain.d.ts","../../../node_modules/@types/node/events.d.ts","../../../node_modules/@types/node/fs.d.ts","../../../node_modules/@types/node/fs/promises.d.ts","../../../node_modules/@types/node/http.d.ts","../../../node_modules/@types/node/http2.d.ts","../../../node_modules/@types/node/https.d.ts","../../../node_modules/@types/node/inspector.d.ts","../../../node_modules/@types/node/inspector.generated.d.ts","../../../node_modules/@types/node/module.d.ts","../../../node_modules/@types/node/net.d.ts","../../../node_modules/@types/node/os.d.ts","../../../node_modules/@types/node/path.d.ts","../../../node_modules/@types/node/perf_hooks.d.ts","../../../node_modules/@types/node/process.d.ts","../../../node_modules/@types/node/punycode.d.ts","../../../node_modules/@types/node/querystring.d.ts","../../../node_modules/@types/node/readline.d.ts","../../../node_modules/@types/node/readline/promises.d.ts","../../../node_modules/@types/node/repl.d.ts","../../../node_modules/@types/node/sea.d.ts","../../../node_modules/@types/node/sqlite.d.ts","../../../node_modules/@types/node/stream.d.ts","../../../node_modules/@types/node/stream/promises.d.ts","../../../node_modules/@types/node/stream/consumers.d.ts","../../../node_modules/@types/node/stream/web.d.ts","../../../node_modules/@types/node/string_decoder.d.ts","../../../node_modules/@types/node/test.d.ts","../../../node_modules/@types/node/timers.d.ts","../../../node_modules/@types/node/timers/promises.d.ts","../../../node_modules/@types/node/tls.d.ts","../../../node_modules/@types/node/trace_events.d.ts","../../../node_modules/@types/node/tty.d.ts","../../../node_modules/@types/node/url.d.ts","../../../node_modules/@types/node/util.d.ts","../../../node_modules/@types/node/v8.d.ts","../../../node_modules/@types/node/vm.d.ts","../../../node_modules/@types/node/wasi.d.ts","../../../node_modules/@types/node/worker_threads.d.ts","../../../node_modules/@types/node/zlib.d.ts","../../../node_modules/@types/node/index.d.ts","../../../node_modules/vite/types/hmrpayload.d.ts","../../../node_modules/vite/dist/node/chunks/modulerunnertransport.d.ts","../../../node_modules/vite/types/customevent.d.ts","../../../node_modules/@types/estree/index.d.ts","../../../node_modules/rollup/dist/rollup.d.ts","../../../node_modules/rollup/dist/parseast.d.ts","../../../node_modules/vite/types/hot.d.ts","../../../node_modules/vite/dist/node/module-runner.d.ts","../../../node_modules/esbuild/lib/main.d.ts","../../../node_modules/@jridgewell/trace-mapping/types/sourcemap-segment.d.mts","../../../node_modules/@jridgewell/trace-mapping/types/types.d.mts","../../../node_modules/@jridgewell/trace-mapping/types/flatten-map.d.mts","../../../node_modules/@jridgewell/trace-mapping/types/trace-mapping.d.mts","../../../node_modules/@jridgewell/gen-mapping/types/sourcemap-segment.d.mts","../../../node_modules/@jridgewell/gen-mapping/types/types.d.mts","../../../node_modules/@jridgewell/gen-mapping/types/gen-mapping.d.mts","../../../node_modules/@jridgewell/source-map/types/source-map.d.mts","../../../node_modules/terser/tools/terser.d.ts","../../../node_modules/vite/types/internal/terseroptions.d.ts","../../../node_modules/source-map-js/source-map.d.ts","../../../node_modules/postcss/lib/previous-map.d.ts","../../../node_modules/postcss/lib/input.d.ts","../../../node_modules/postcss/lib/css-syntax-error.d.ts","../../../node_modules/postcss/lib/declaration.d.ts","../../../node_modules/postcss/lib/root.d.ts","../../../node_modules/postcss/lib/warning.d.ts","../../../node_modules/postcss/lib/lazy-result.d.ts","../../../node_modules/postcss/lib/no-work-result.d.ts","../../../node_modules/postcss/lib/processor.d.ts","../../../node_modules/postcss/lib/result.d.ts","../../../node_modules/postcss/lib/document.d.ts","../../../node_modules/postcss/lib/rule.d.ts","../../../node_modules/postcss/lib/node.d.ts","../../../node_modules/postcss/lib/comment.d.ts","../../../node_modules/postcss/lib/container.d.ts","../../../node_modules/postcss/lib/at-rule.d.ts","../../../node_modules/postcss/lib/list.d.ts","../../../node_modules/postcss/lib/postcss.d.ts","../../../node_modules/postcss/lib/postcss.d.mts","../../../node_modules/vite/types/internal/csspreprocessoroptions.d.ts","../../../node_modules/vite/types/internal/lightningcssoptions.d.ts","../../../node_modules/vite/types/importglob.d.ts","../../../node_modules/vite/types/metadata.d.ts","../../../node_modules/vite/dist/node/index.d.ts","../../../node_modules/@babel/types/lib/index.d.ts","../../../node_modules/@types/babel__generator/index.d.ts","../../../node_modules/@babel/parser/typings/babel-parser.d.ts","../../../node_modules/@types/babel__template/index.d.ts","../../../node_modules/@types/babel__traverse/index.d.ts","../../../node_modules/@types/babel__core/index.d.ts","../../../node_modules/@vitejs/plugin-react/dist/index.d.ts","../../../node_modules/vite-plugin-electron/dist/utils.d.ts","../../../node_modules/vite-plugin-electron/dist/index.d.ts","../../../node_modules/vite-plugin-electron-renderer/dist/index.d.ts","../../../node_modules/vite-plugin-electron/dist/simple.d.ts","./vite.config.ts","../../../node_modules/playwright-core/types/protocol.d.ts","../../../node_modules/playwright-core/types/structs.d.ts","../../../node_modules/zod/v4/core/json-schema.d.cts","../../../node_modules/zod/v4/core/standard-schema.d.cts","../../../node_modules/zod/v4/core/registries.d.cts","../../../node_modules/zod/v4/core/to-json-schema.d.cts","../../../node_modules/zod/v4/core/util.d.cts","../../../node_modules/zod/v4/core/versions.d.cts","../../../node_modules/zod/v4/core/schemas.d.cts","../../../node_modules/zod/v4/core/checks.d.cts","../../../node_modules/zod/v4/core/errors.d.cts","../../../node_modules/zod/v4/core/core.d.cts","../../../node_modules/zod/v4/core/parse.d.cts","../../../node_modules/zod/v4/core/regexes.d.cts","../../../node_modules/zod/v4/locales/ar.d.cts","../../../node_modules/zod/v4/locales/az.d.cts","../../../node_modules/zod/v4/locales/be.d.cts","../../../node_modules/zod/v4/locales/bg.d.cts","../../../node_modules/zod/v4/locales/ca.d.cts","../../../node_modules/zod/v4/locales/cs.d.cts","../../../node_modules/zod/v4/locales/da.d.cts","../../../node_modules/zod/v4/locales/de.d.cts","../../../node_modules/zod/v4/locales/el.d.cts","../../../node_modules/zod/v4/locales/en.d.cts","../../../node_modules/zod/v4/locales/eo.d.cts","../../../node_modules/zod/v4/locales/es.d.cts","../../../node_modules/zod/v4/locales/fa.d.cts","../../../node_modules/zod/v4/locales/fi.d.cts","../../../node_modules/zod/v4/locales/fr.d.cts","../../../node_modules/zod/v4/locales/fr-ca.d.cts","../../../node_modules/zod/v4/locales/he.d.cts","../../../node_modules/zod/v4/locales/hr.d.cts","../../../node_modules/zod/v4/locales/hu.d.cts","../../../node_modules/zod/v4/locales/hy.d.cts","../../../node_modules/zod/v4/locales/id.d.cts","../../../node_modules/zod/v4/locales/is.d.cts","../../../node_modules/zod/v4/locales/it.d.cts","../../../node_modules/zod/v4/locales/ja.d.cts","../../../node_modules/zod/v4/locales/ka.d.cts","../../../node_modules/zod/v4/locales/kh.d.cts","../../../node_modules/zod/v4/locales/km.d.cts","../../../node_modules/zod/v4/locales/ko.d.cts","../../../node_modules/zod/v4/locales/lt.d.cts","../../../node_modules/zod/v4/locales/mk.d.cts","../../../node_modules/zod/v4/locales/ms.d.cts","../../../node_modules/zod/v4/locales/nl.d.cts","../../../node_modules/zod/v4/locales/no.d.cts","../../../node_modules/zod/v4/locales/ota.d.cts","../../../node_modules/zod/v4/locales/ps.d.cts","../../../node_modules/zod/v4/locales/pl.d.cts","../../../node_modules/zod/v4/locales/pt.d.cts","../../../node_modules/zod/v4/locales/ro.d.cts","../../../node_modules/zod/v4/locales/ru.d.cts","../../../node_modules/zod/v4/locales/sl.d.cts","../../../node_modules/zod/v4/locales/sv.d.cts","../../../node_modules/zod/v4/locales/ta.d.cts","../../../node_modules/zod/v4/locales/th.d.cts","../../../node_modules/zod/v4/locales/tr.d.cts","../../../node_modules/zod/v4/locales/ua.d.cts","../../../node_modules/zod/v4/locales/uk.d.cts","../../../node_modules/zod/v4/locales/ur.d.cts","../../../node_modules/zod/v4/locales/uz.d.cts","../../../node_modules/zod/v4/locales/vi.d.cts","../../../node_modules/zod/v4/locales/zh-cn.d.cts","../../../node_modules/zod/v4/locales/zh-tw.d.cts","../../../node_modules/zod/v4/locales/yo.d.cts","../../../node_modules/zod/v4/locales/index.d.cts","../../../node_modules/zod/v4/core/doc.d.cts","../../../node_modules/zod/v4/core/api.d.cts","../../../node_modules/zod/v4/core/json-schema-processors.d.cts","../../../node_modules/zod/v4/core/json-schema-generator.d.cts","../../../node_modules/zod/v4/core/index.d.cts","../../../node_modules/zod/v4/classic/errors.d.cts","../../../node_modules/zod/v4/classic/parse.d.cts","../../../node_modules/zod/v4/classic/schemas.d.cts","../../../node_modules/zod/v4/classic/checks.d.cts","../../../node_modules/zod/v4/classic/compat.d.cts","../../../node_modules/zod/v4/classic/from-json-schema.d.cts","../../../node_modules/zod/v4/classic/iso.d.cts","../../../node_modules/zod/v4/classic/coerce.d.cts","../../../node_modules/zod/v4/classic/external.d.cts","../../../node_modules/zod/index.d.cts","../../../node_modules/zod/v3/helpers/typealiases.d.cts","../../../node_modules/zod/v3/helpers/util.d.cts","../../../node_modules/zod/v3/zoderror.d.cts","../../../node_modules/zod/v3/locales/en.d.cts","../../../node_modules/zod/v3/errors.d.cts","../../../node_modules/zod/v3/helpers/parseutil.d.cts","../../../node_modules/zod/v3/helpers/enumutil.d.cts","../../../node_modules/zod/v3/helpers/errorutil.d.cts","../../../node_modules/zod/v3/helpers/partialutil.d.cts","../../../node_modules/zod/v3/standard-schema.d.cts","../../../node_modules/zod/v3/types.d.cts","../../../node_modules/zod/v3/external.d.cts","../../../node_modules/zod/v3/index.d.cts","../../../node_modules/electron/electron.d.ts","../../../node_modules/playwright-core/types/types.d.ts","../../../node_modules/playwright-core/index.d.ts","../../../node_modules/playwright/types/test.d.ts","../../../node_modules/playwright/test.d.ts","../../../node_modules/@playwright/test/index.d.ts","./playwright.config.ts","../../../node_modules/@vitest/spy/optional-types.d.ts","../../../node_modules/@vitest/spy/dist/index.d.ts","../../../node_modules/tinyrainbow/dist/index.d.ts","../../../node_modules/@standard-schema/spec/dist/index.d.ts","../../../node_modules/@vitest/pretty-format/dist/index.d.ts","../../../node_modules/@vitest/utils/dist/types.d-bcelap-c.d.ts","../../../node_modules/@vitest/utils/dist/diff.d.ts","../../../node_modules/@vitest/utils/dist/display.d.ts","../../../node_modules/@types/deep-eql/index.d.ts","../../../node_modules/assertion-error/index.d.ts","../../../node_modules/@types/chai/index.d.ts","../../../node_modules/@vitest/expect/dist/index.d.ts","../../../node_modules/@vitest/utils/dist/types.d.ts","../../../node_modules/@vitest/utils/dist/helpers.d.ts","../../../node_modules/@vitest/utils/dist/timers.d.ts","../../../node_modules/@vitest/utils/dist/index.d.ts","../../../node_modules/@vitest/runner/dist/tasks.d-deyaimiu.d.ts","../../../node_modules/@vitest/runner/dist/index.d.ts","../../../node_modules/vitest/dist/chunks/traces.d.d2t_r8rx.d.ts","../../../node_modules/@vitest/snapshot/dist/environment.d-dojxxzv9.d.ts","../../../node_modules/@vitest/snapshot/dist/rawsnapshot.d-d_x3-62x.d.ts","../../../node_modules/@vitest/snapshot/dist/index.d.ts","../../../node_modules/vitest/dist/chunks/config.d.a1h_y6jt.d.ts","../../../node_modules/vitest/dist/chunks/environment.d.crsxczp1.d.ts","../../../node_modules/vitest/dist/chunks/rpc.d.b_8spu0w.d.ts","../../../node_modules/vitest/dist/chunks/worker.d.zphpo4yb.d.ts","../../../node_modules/vitest/dist/chunks/browser.d.bcoexmfg.d.ts","../../../node_modules/vitest/optional-types.d.ts","../../../node_modules/@vitest/runner/dist/utils.d.ts","../../../node_modules/tinybench/dist/index.d.ts","../../../node_modules/vitest/dist/chunks/benchmark.d.daahlpsq.d.ts","../../../node_modules/@vitest/mocker/dist/types.d-bji5eawu.d.ts","../../../node_modules/@vitest/mocker/dist/index.d-b41z0auw.d.ts","../../../node_modules/@vitest/mocker/dist/index.d.ts","../../../node_modules/@vitest/utils/dist/source-map.d.ts","../../../node_modules/vitest/dist/chunks/coverage.d.bztk59wp.d.ts","../../../node_modules/@vitest/utils/dist/serialize.d.ts","../../../node_modules/@vitest/utils/dist/error.d.ts","../../../node_modules/vitest/dist/browser.d.ts","../../../node_modules/vitest/browser/context.d.ts","../../../node_modules/@vitest/snapshot/dist/manager.d.ts","../../../node_modules/vitest/dist/chunks/reporters.d.dtokvv2s.d.ts","../../../node_modules/vitest/dist/chunks/plugin.d.dwfiij7i.d.ts","../../../node_modules/vitest/dist/config.d.ts","../../../node_modules/vitest/config.d.ts","./vitest.config.ts","../../../node_modules/@types/aria-query/index.d.ts","../../../node_modules/keyv/src/index.d.ts","../../../node_modules/@types/http-cache-semantics/index.d.ts","../../../node_modules/@types/responselike/index.d.ts","../../../node_modules/@types/cacheable-request/index.d.ts","../../../node_modules/@types/ms/index.d.ts","../../../node_modules/@types/debug/index.d.ts","../../../node_modules/@types/dom-webcodecs/webcodecs.generated.d.ts","../../../node_modules/@types/dom-webcodecs/index.d.ts","../../../node_modules/@types/dom-mediacapture-transform/index.d.ts","../../../node_modules/@types/fs-extra/index.d.ts","../../../node_modules/@types/json-schema/index.d.ts","../../../node_modules/@types/keyv/index.d.ts","../../../node_modules/@types/prop-types/index.d.ts","../../../node_modules/@types/react/global.d.ts","../../../node_modules/csstype/index.d.ts","../../../node_modules/@types/react/index.d.ts","../../../node_modules/@types/react-dom/index.d.ts","../../../node_modules/@types/use-sync-external-store/index.d.ts","../../../node_modules/@types/yauzl/index.d.ts"],"fileIdsList":[[55,103,120,121,310],[55,103,120,121,125,197,204,208,355],[55,103,120,121,125,356],[55,103,120,121,198],[55,103,120,121],[55,103,120,121,166,168],[55,103,120,121,167],[55,103,120,121,166,169],[55,103,120,121,164,166],[55,103,120,121,163,164,165],[55,103,120,121,163,166],[55,103,120,121,309],[55,103,120,121,198,199,200,201,202],[55,103,120,121,198,200],[55,103,114,117,120,121,146,153,359,360,361],[55,103,120,121,320,321],[55,103,120,121,363],[55,103,120,121,366],[55,103,120,121,365],[55,103,115,120,121,153],[55,103,114,120,121,153],[55,100,101,103,120,121],[55,102,103,120,121],[103,120,121],[55,103,108,120,121,138],[55,103,104,109,114,120,121,123,135,146],[55,103,104,105,114,120,121,123],[50,51,52,55,103,120,121],[55,103,106,120,121,147],[55,103,107,108,115,120,121,124],[55,103,108,120,121,135,143],[55,103,109,111,114,120,121,123],[55,102,103,110,120,121],[55,103,111,112,120,121],[55,103,113,114,120,121],[55,102,103,114,120,121],[55,103,114,115,116,120,121,135,146],[55,103,114,115,116,120,121,130,135,138],[55,96,103,111,114,117,120,121,123,135,146],[55,103,114,115,117,118,120,121,123,135,143,146],[55,103,117,119,120,121,135,143,146],[53,54,55,56,57,58,59,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152],[55,103,114,120,121],[55,103,120,121,122,146],[55,103,111,114,120,121,123,135],[55,103,120,121,124],[55,103,120,121,125],[55,102,103,120,121,126],[55,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152],[55,103,120,121,128],[55,103,120,121,129],[55,103,114,120,121,130,131],[55,103,120,121,130,132,147,149],[55,103,115,120,121],[55,103,114,120,121,135,136,138],[55,103,120,121,137,138],[55,103,120,121,135,136],[55,103,120,121,138],[55,103,120,121,139],[55,100,103,120,121,135,140,146],[55,103,114,120,121,141,142],[55,103,120,121,141,142],[55,103,108,120,121,123,135,143],[55,103,120,121,144],[55,103,120,121,123,145],[55,103,117,120,121,129,146],[55,103,108,120,121,147],[55,103,120,121,135,148],[55,103,120,121,122,149],[55,103,120,121,150],[55,96,103,120,121],[55,96,103,114,116,120,121,126,135,138,146,148,149,151],[55,103,120,121,135,152],[55,103,120,121,374],[55,103,120,121,371,372,373],[55,103,117,120,121,135,153],[55,103,114,120,121,135,153],[55,103,120,121,197,203,355],[55,103,120,121,313,314,315,318,319,322],[55,103,120,121,343],[55,103,120,121,343,344],[55,103,120,121,318,327,328],[55,103,120,121,318,327],[55,103,120,121,327],[55,103,120,121,316,327,331,332],[55,103,120,121,316,327,331],[55,103,120,121,312],[55,103,120,121,316,317],[55,103,120,121,316],[55,103,120,121,316,317,324,348],[55,103,120,121,324],[55,103,120,121,316,319,324,325,326],[55,103,114,115,120,121,153],[55,103,120,121,306],[55,103,104,115,120,121,135,210,211,291,304,305],[55,103,120,121,308],[55,103,120,121,307],[55,103,120,121,188],[55,103,120,121,186,188],[55,103,120,121,177,185,186,187,189,191],[55,103,120,121,175],[55,103,120,121,178,183,188,191],[55,103,120,121,174,191],[55,103,120,121,178,179,182,183,184,191],[55,103,120,121,178,179,180,182,183,191],[55,103,120,121,175,176,177,178,179,183,184,185,187,188,189,191],[55,103,120,121,191],[55,103,120,121,173,175,176,177,178,179,180,182,183,184,185,186,187,188,189,190],[55,103,120,121,173,191],[55,103,120,121,178,180,181,183,184,191],[55,103,120,121,182,191],[55,103,120,121,183,184,188,191],[55,103,120,121,176,186],[55,103,120,121,158,196,197],[55,103,120,121,157,158],[55,103,120,121,170],[55,68,72,103,120,121,146],[55,68,103,120,121,135,146],[55,63,103,120,121],[55,65,68,103,120,121,143,146],[55,103,120,121,123,143],[55,103,120,121,153],[55,63,103,120,121,153],[55,65,68,103,120,121,123,146],[55,60,61,64,67,103,114,120,121,135,146],[55,68,75,103,120,121],[55,60,66,103,120,121],[55,68,89,90,103,120,121],[55,64,68,103,120,121,138,146,153],[55,89,103,120,121,153],[55,62,63,103,120,121,153],[55,68,103,120,121],[55,62,63,64,65,66,67,68,69,70,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,90,91,92,93,94,95,103,120,121],[55,68,83,103,120,121],[55,68,75,76,103,120,121],[55,66,68,76,77,103,120,121],[55,67,103,120,121],[55,60,63,68,103,120,121],[55,68,72,76,77,103,120,121],[55,72,103,120,121],[55,66,68,71,103,120,121,146],[55,60,65,68,75,103,120,121],[55,103,120,121,135],[55,63,68,89,103,120,121,151,153],[55,103,120,121,162,197,355],[55,103,104,120,121,158,196,197,205,355],[55,103,120,121,158,196,197,206,207,355],[55,103,120,121,158,196,197,206,355],[55,103,120,121,154],[55,103,114,115,117,118,119,120,121,123,135,143,146,152,153,154,155,156,158,159,161,162,172,192,193,194,195,196,197],[55,103,120,121,154,155,156,160],[55,103,120,121,156],[55,103,120,121,171],[55,103,120,121,158,197],[55,103,120,121,350],[55,103,120,121,323,355],[55,103,120,121,313,316,318,319,325,326,327,329,330,333,334,346,347,349],[55,103,120,121,329,340,341],[55,103,120,121,329,330,337],[55,103,120,121,316,318,329,330,333],[55,103,120,121,197,353,355],[55,103,106,115,120,121,135,197,316,318,323,327,329,330,333,334,337,338,339,342,345,346,347,351,352,355],[55,103,120,121,161,329,330,333],[55,103,120,121,161,329,334,335,336],[55,103,106,115,120,121,135,161,197,316,318,323,327,329,330,333,334,335,336,337,338,339,340,341,342,345,346,347,351,352,353,354,355],[55,103,120,121,290],[55,103,120,121,294,295],[55,103,120,121,292,293,294,296,297,302],[55,103,120,121,293,294],[55,103,120,121,302],[55,103,120,121,303],[55,103,120,121,294],[55,103,120,121,292,293,294,297,298,299,300,301],[55,103,120,121,292,293,304],[55,103,120,121,281],[55,103,120,121,281,284],[55,103,120,121,216,276,279,281,282,283,284,285,286,287,288,289],[55,103,120,121,212,214,284],[55,103,120,121,281,282],[55,103,120,121,213,281,283],[55,103,120,121,214,216,218,219,220,221],[55,103,120,121,216,218,220,221],[55,103,120,121,216,218,220],[55,103,120,121,213,216,218,219,221],[55,103,120,121,212,214,215,216,217,218,219,220,221,222,223,276,277,278,279,280],[55,103,120,121,212,214,215,218],[55,103,120,121,214,215,218],[55,103,120,121,218,221],[55,103,120,121,212,213,215,216,217,219,220,221],[55,103,120,121,212,213,214,218,281],[55,103,120,121,218,219,220,221],[55,103,120,121,220],[55,103,120,121,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"6b4e081d55ac24fc8a4631d5dd77fe249fa25900abd7d046abb87d90e3b45645","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"80523c00b8544a2000ae0143e4a90a00b47f99823eb7926c1e03c494216fc363","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"746911b62b329587939560deb5c036aca48aece03147b021fa680223255d5183","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"10073cdcf56982064c5337787cc59b79586131e1b28c106ede5bff362f912b70","impliedFormat":99},{"version":"72950913f4900b680f44d8cab6dd1ea0311698fc1eefb014eb9cdfc37ac4a734","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"ff0a83c9a0489a627e264ffcb63f2264b935b20a502afa3a018848139e3d8575","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"971f12a5fc236419ced0b7b9f23a53c1758233713f565635bbf4b85e2b23f55a","impliedFormat":99},{"version":"76de3321ce519928f1ff7d7a30391c0dc7374af20f81d9167919f038895b5cb0","impliedFormat":99},{"version":"094b9210da23b8711709b0535c59841186267bf6b83c1609aa9b515f830ab274","impliedFormat":99},{"version":"fbfbb4e99c6259ff5ccc4a5a62b3b63c0c8cae6e84737786c4a4c761c9a9de91","impliedFormat":99},{"version":"604887bbd5b0a93234ce882543a465f008636185c52e0f0353330e2bc38b03b6","impliedFormat":99},{"version":"32bf912173e8a9533631f9e9d8dc90a2ac7b52c2355611ddd886beab24dfd182","impliedFormat":99},{"version":"82695324abf7f3278b6d9f0582f4a544e8f7055c8cbe1065ab5cbacde1719c4c","impliedFormat":99},{"version":"43bba542e50e19241ec64bc13cfc0d9273e6198f36563cecad1f4e4b78ad47f3","impliedFormat":99},{"version":"b8cb3b69c0e8114f758bb8ef8efeef1cc80f8911bfd21126def73d2174ce479e","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"3cc8a3d123b6b232d48d34b51b785f9da8d193f5b5817fa521fcd2f3b9315c55","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"354a7f8e1287d9d6b7561bc97fdd8cbc2f7c1dd79e4cb37b942e8a5cfaff1085","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"59f8dc89b9e724a6a667f52cdf4b90b6816ae6c9842ce176d38fcc973669009e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e4af494f7a14b226bbe732e9c130d8811f8c7025911d7c58dd97121a85519715","impliedFormat":1},{"version":"cbb1c5ba5dbabe42c19ca31b83e48fec95895484fe1d1a8fb649b69ea224c5b8","impliedFormat":99},{"version":"556ccd493ec36c7d7cb130d51be66e147b91cc1415be383d71da0f1e49f742a9","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"ef94438f848be3a3b0033013bf64753f771f983c1e205e4a06675eb253ca7cd2","impliedFormat":99},{"version":"d920c349abc38c151f86499ae2c16d07862cc103e8dd3c781710aabbdb667e61","impliedFormat":1},{"version":"9fe2a0b69d0d9cf90e3d0e7dffc9fa080194a8fedbe191dcece5cf8449452f4d","impliedFormat":1},{"version":"c400678110f688feba4d6d3f93269fca02834bb3d6a1ecd99b28b3f69f7d23f4","impliedFormat":1},{"version":"0531ff3bd78d643b584e02909c5fdb86938169d818afbbef1bc3a2a768ab32ec","impliedFormat":1},"233c18ea181b6edf1129b693329a5763449a931bd8ce24d41135f4c6d52834fb",{"version":"e08660f21d0e8b367414e78706ae69a19b078fb67b0fe8c818ccaeeeedc00272","impliedFormat":1},{"version":"32727845ab5bd8a9ef3e4844c567c09f6d418fcf0f90d381c00652a6f23e7f6e","impliedFormat":1},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"40ba6c32eb732a09e4446ade5cb6ad0c147f186f9c9dc6878b90b4418ad9f6ea","impliedFormat":1},{"version":"fdd814741843f85c98281522c58f5a646590ba9019fad2efaa95987655e0611b","impliedFormat":1},{"version":"c78aff4fb58b28b8f642d5095fc7eeb79f00e652a67caa19693af1adabb833c9","impliedFormat":1},{"version":"f80a08ced8818dc99359c0acd5b3f12762e1ce53758007759b0d4e503cbf4a5e","impliedFormat":1},{"version":"37935fa7564bcc6e0bc845b766a24391098d26f7c8245d6e8ab37bc016816e94","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"0f8a263f4c8595c8a07de52e3f3927640c44386c1aa2984de9eae50d75e613b2","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"346fffde7c32da87c2196eb7494422449dc2ca82d3b4e6bf55be1d1a33ffc2b0","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8b5875e4958528042103fdd775e106a7f76bafc29709f0690df9a7d2241d52a7","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"aef26cf95593c8ace1c62c4724f9afac77bdfa756fb8a00613cd152117cb2f43","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"268fd6d9f2e807a39a6c5aa654b00f949feb63d3faa7dd0f9bba7dde9172159c","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"36eb5babc665b890786550d4a8cb20ef7105673a6d5551fbdd7012877bb26942","impliedFormat":1},{"version":"fec412ded391a7239ef58f455278154b62939370309c1fed322293d98c8796a6","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"93c3e73824ad57f98fd23b39335dbdae2db0bd98199b0dc0b9ccc60bf3c5134a","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"5d22f19e0756e9fc11fde5bb67eee993c289fc371b8ad3fc63629bfaadff79a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"8324f3861a7a8db0f9d294f6a189182b2d231840cebb7f3ea5f4635773cdaf41","impliedFormat":1},{"version":"7a8ec10b0834eb7183e4bfcd929838ac77583828e343211bb73676d1e47f6f01","impliedFormat":1},{"version":"be57def447f85b42c8f509a8ce4125c1af5f26597c4a93ef617aae5e0b81fa02","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f00324f263189b385c3a9383b1f4dae6237697bcf0801f96aa35c340512d79c","impliedFormat":1},{"version":"ec8997c2e5cea26befc76e7bf990750e96babb16977673a9ff3b5c0575d01e48","impliedFormat":1},"6cf13b54411f9888a1aa5e200b871e56f0406f3f9a84d16f7b0b861fe231ddb4",{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"4ead13a482c539b77394b2a97e3b877b809eac596390371cea490286f53b996a","impliedFormat":99},{"version":"12405c748be62bbefb6df7515956b0921f84d7b444d1f6c3e00af1980cc3af17","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"3a582c6e8906f5b094ccf0de6cc6f4f8a54b05a34f52517aba5c9c7f704f6b28","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"49ab4f1d153a252779958fc87b700743d32b5ffa42addd70ae23ad3f429daa5c","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"5c935b7fc4ddc1410ea1cd7cd4e35ed106a6e4920dd27a9480a40fd224359dc3","affectsGlobalScope":true,"impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"72ce5b734c05da85c85a6f6dc05823b051d6aa41acaedeeb1d17c72f3b4efa72","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"53cf4076f42b29b8d411259d168d51b3a0274c42c8814e5b44dfa8803a35d4fc","impliedFormat":99},{"version":"a39461ee1f27cf3e6cfd63d21045713d26d521da55ea4d8efccb705f689e6dbb","impliedFormat":99},{"version":"61bb64660ee150f3ab618340e15cca0a81664801bede7c966ca0eca3a952fe63","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"ec0c42bb0f465e4993f2bc68a6ce9df9a2dcbc7b83e21748f82f1b69561938e3","impliedFormat":99},{"version":"f50ff37a9cbbe74475f426474d9827083c7c2c138a954d28f1690df338f69291","impliedFormat":99},{"version":"61fd6c17235d530c40f543dd7c40afab091d91c1ef890baeed30db6d82b04b28","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"091767bc841f937654ed597d49e023ed59850355e746ae1a6f20ab31076ee1fb","impliedFormat":99},{"version":"19c6d6135af59693698d384050b45a8a049493500add442f58e4bd7c8a255ab6","impliedFormat":99},{"version":"6a0dba12d55314638a8c51108b20fe2f68f1364a619d098918bda91c22dec154","impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"2b39c6cf59088713babbfc3e20ee85f1375d40e66953156fa658346b8346f24f","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"437b7613a30a2fcde463f7b707c6d5567a8823fbc51de50b8641bf5b1d126fad","impliedFormat":99},{"version":"63ea959e28c110923f495576e614fb8b36c09b6828b467b2c7cd7f03b03ccf9f","impliedFormat":99},{"version":"1601a95dbb33059fc3d12638ed2a9aecff899e339c5c0f3a0b28768866d385b4","impliedFormat":99},{"version":"56fc978580577d30f4c2cdb5b1eb9217b66ed66537dd27141256f426e4b8dd68","impliedFormat":99},{"version":"2c5413050a2580becf9d82dd7e3006b95623e96f145356bf73230cd635352f70","impliedFormat":99},{"version":"860bedc71ead192ea4a0ea5ef4686e65724d14b391ebd1a6671a7044e6bd8e15","impliedFormat":99},{"version":"7c0a845bee4a084cbb8654709f48e5f13e2f6d45e5e2dde7c57cadf79fd9e3d5","impliedFormat":99},{"version":"7fa6fdc6eeeb3850e6b3166836f8c9199594c4f4bed3cee02cac0ca33d71894d","impliedFormat":99},{"version":"e53757f3e6688e8be16b36ccbeedc85637e607353fc67ff7b6665d6005aeaa12","impliedFormat":99},{"version":"88ba2660f5b024c7afd02ea700bc0c677687e17e34f0a94c3256ddb55e0f1c5d","impliedFormat":99},{"version":"7bbff6783e96c691a41a7cf12dd5486b8166a01b0c57d071dbcfca55c9525ec4","impliedFormat":99},"9c00dd8f0d59eb05b152984d3d393295a9eafc76c37272092581253396a21b7b",{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"42baf4ca38c38deaf411ea73f37bc39ff56c6e5c761a968b64ac1b25c92b5cd8","impliedFormat":1},{"version":"4f6ae308c5f2901f2988c817e1511520619e9025b9b12cc7cce2ab2e6ffed78a","impliedFormat":1},{"version":"8718fa41d7cf4aa91de4e8f164c90f88e0bf343aa92a1b9b725a9c675c64e16b","impliedFormat":1},{"version":"f992cd6cc0bcbaa4e6c810468c90f2d8595f8c6c3cf050c806397d3de8585562","impliedFormat":1},{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"89e326922cadcc2331d7e851011cf9f0456a681aaf3c95b48b81f8d80e8cdfba","impliedFormat":1},{"version":"d44c53a5185ae285987268f3f30cdadf3dc7bff19d230bfcb5c6505d4f268299","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7f161bf747d17d98f49d7c2a9e67da87031da2c2cbcf356698fbfc184788b20","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d9ff2131e0cd0d00541853f45cf91d93570f4657717daee949596e828dfecd5","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed19da84b7dbf00952ad0b98ce5c194f1903bcf7c94d8103e8e0d63b271543ae","impliedFormat":1},{"version":"f3d8c757e148ad968f0d98697987db363070abada5f503da3c06aefd9d4248c1","impliedFormat":1},{"version":"fec943fdb3275eb6e006b35e04a8e2e99e9adf3f4b969ddf15315ac7575a93e4","impliedFormat":1},{"version":"87d9d29dbc745f182683f63187bf3d53fd8673e5fca38ad5eaab69798ed29fbc","impliedFormat":1},{"version":"eb5b19b86227ace1d29ea4cf81387279d04bb34051e944bc53df69f58914b788","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"035312d4945d13efa134ae482f6dc56a1a9346f7ac3be7ccbad5741058ce87f3","affectsGlobalScope":true,"impliedFormat":1},{"version":"17ed71200119e86ccef2d96b73b02ce8854b76ad6bd21b5021d4269bec527b5f","impliedFormat":1},{"version":"7fa8d75d229eeaee235a801758d9c694e94405013fe77d5d1dd8e3201fc414f1","impliedFormat":1},{"version":"74d5a87c3616cd5d8691059d531504403aa857e09cbaecb1c64dfb9ace0db185","impliedFormat":1}],"root":[209,311,357],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true,"strict":true},"referencedMap":[[311,1],[209,2],[357,3],[200,4],[198,5],[169,6],[167,5],[168,7],[170,8],[165,9],[163,5],[166,10],[164,11],[310,12],[315,5],[358,5],[203,13],[199,4],[201,14],[202,4],[362,15],[322,16],[364,17],[320,5],[367,18],[366,19],[365,5],[157,5],[368,20],[360,5],[369,5],[370,21],[363,5],[100,22],[101,22],[102,23],[55,24],[103,25],[104,26],[105,27],[50,5],[53,28],[51,5],[52,5],[106,29],[107,30],[108,31],[109,32],[110,33],[111,34],[112,34],[113,35],[114,36],[115,37],[116,38],[56,5],[54,5],[117,39],[118,40],[119,41],[153,42],[120,43],[121,5],[122,44],[123,45],[124,46],[125,47],[126,48],[127,49],[128,50],[129,51],[130,52],[131,52],[132,53],[133,5],[134,54],[135,55],[137,56],[136,57],[138,58],[139,59],[140,60],[141,61],[142,62],[143,63],[144,64],[145,65],[146,66],[147,67],[148,68],[149,69],[150,70],[57,5],[58,5],[59,5],[97,71],[98,5],[99,5],[151,72],[152,73],[371,5],[375,74],[372,5],[374,75],[361,76],[376,5],[377,77],[204,78],[323,79],[344,80],[345,81],[343,5],[316,5],[329,82],[328,83],[340,82],[331,84],[333,85],[352,85],[332,86],[313,87],[312,5],[318,88],[319,89],[349,90],[325,91],[327,92],[348,5],[346,91],[326,5],[317,89],[324,5],[321,5],[373,5],[305,93],[162,5],[359,43],[307,94],[210,5],[211,94],[306,95],[309,96],[308,97],[189,98],[187,99],[188,100],[176,101],[177,99],[184,102],[175,103],[180,104],[190,5],[181,105],[186,106],[192,107],[191,108],[174,109],[182,110],[183,111],[178,112],[185,98],[179,113],[159,114],[158,115],[173,5],[171,116],[341,5],[314,5],[1,5],[48,5],[49,5],[9,5],[13,5],[12,5],[3,5],[14,5],[15,5],[16,5],[17,5],[18,5],[19,5],[20,5],[21,5],[4,5],[22,5],[23,5],[5,5],[24,5],[28,5],[25,5],[26,5],[27,5],[29,5],[30,5],[31,5],[6,5],[32,5],[33,5],[34,5],[35,5],[7,5],[39,5],[36,5],[37,5],[38,5],[40,5],[8,5],[41,5],[46,5],[47,5],[42,5],[43,5],[44,5],[45,5],[2,5],[11,5],[10,5],[75,117],[85,118],[74,117],[95,119],[66,120],[65,121],[94,122],[88,123],[93,124],[68,125],[82,126],[67,127],[91,128],[63,129],[62,122],[92,130],[64,131],[69,132],[70,5],[73,132],[60,5],[96,133],[86,134],[77,135],[78,136],[80,137],[76,138],[79,139],[89,122],[71,140],[72,141],[81,142],[61,143],[84,134],[83,132],[87,5],[90,144],[207,145],[206,146],[208,147],[205,148],[155,149],[197,150],[161,151],[156,149],[154,5],[160,152],[195,5],[193,5],[194,5],[172,153],[196,154],[351,155],[356,156],[350,157],[342,158],[338,159],[334,160],[347,5],[335,84],[354,161],[353,162],[336,163],[330,5],[337,164],[355,165],[339,5],[291,166],[296,167],[303,168],[298,5],[299,5],[297,169],[300,170],[292,5],[293,5],[304,171],[295,172],[301,5],[302,173],[294,174],[285,175],[289,176],[286,176],[282,175],[290,177],[287,178],[288,176],[283,179],[284,180],[278,181],[219,182],[221,183],[277,5],[220,184],[281,185],[280,186],[279,187],[212,5],[222,182],[223,5],[214,188],[218,189],[213,5],[215,190],[216,191],[217,5],[224,192],[225,192],[226,192],[227,192],[228,192],[229,192],[230,192],[231,192],[232,192],[233,192],[234,192],[235,192],[236,192],[237,192],[239,192],[238,192],[240,192],[241,192],[242,192],[243,192],[244,192],[276,193],[245,192],[246,192],[247,192],[248,192],[249,192],[250,192],[251,192],[252,192],[253,192],[254,192],[255,192],[256,192],[257,192],[259,192],[258,192],[260,192],[261,192],[262,192],[263,192],[264,192],[265,192],[266,192],[267,192],[268,192],[269,192],[270,192],[271,192],[272,192],[275,192],[273,192],[274,192]],"affectedFilesPendingEmit":[[311,17],[209,17],[357,17]],"emitSignatures":[209,311,357],"version":"5.9.3"} \ No newline at end of file From 4db035e7a6adcaff1d1056a9e483a019a5c0e8ea Mon Sep 17 00:00:00 2001 From: Aarohan Niraula Date: Mon, 31 Aug 2026 16:01:22 +0545 Subject: [PATCH 6/9] fix(macos): wait out the startup microphone alert before the screen prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS will not stack a second permission alert. On first launch the mic ask from main.ts is still on screen when the user reaches Record, so the Screen Recording prompt raised there was silently dropped — while app.focus({steal:true}) yanked activation from the alert the user was reading. Worse than the lost prompt: the marker recorded an ask that never appeared, which writes off this machine's one prompt for good, since macOS never redraws it once the marker says it was raised. Raising now waits on a gate main.ts arms with the mic ask's promise: the launch's single raise is claimed synchronously (so a concurrent request cannot start a second one), but the focus steal, the probe, and the marker write all happen after the mic alert settles. On every launch where the mic is already decided the gate is a resolved promise and nothing changes. Claude-Session: https://claude.ai/code/session_01AeoFwWz1hQWoXeccpud6wo --- electron/ipc/handlers.ts | 25 +++++++++++++++++++++++++ electron/main.ts | 14 ++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 1d9b887a3..08c900420 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -502,6 +502,23 @@ let selectedSource: SelectedSource | null = null; let selectedDesktopSource: DesktopCapturerSource | null = null; /** Whether this launch has already raised macOS' Screen Recording prompt. */ let screenPromptRaisedThisLaunch = false; + +/** + * Settles once the startup TCC ask (the microphone prompt in main.ts) has been + * answered. macOS will not stack a second permission alert: a Screen Recording + * prompt raised while the mic alert is still up is silently dropped, and + * `app.focus({ steal: true })` yanks activation from the alert the user is + * reading. First launch hits exactly this — mic prompt at startup, Record + * clicked moments later — and it is the one launch whose prompt the marker + * would then write off for good. + */ +let startupMediaPromptGate: Promise = Promise.resolve(); + +export function setStartupMediaPromptGate(gate: Promise) { + startupMediaPromptGate = gate.catch(() => { + // A failed ask still settles the gate; the screen prompt proceeds. + }); +} /** Lazily opened so the userData path is only touched once the app needs it. */ let screenPromptMarkerInstance: ScreenPromptMarker | null = null; @@ -1886,6 +1903,14 @@ export function registerIpcHandlers( raisedBefore: getScreenPromptMarker().hasRaisedBefore(), }) ) { + // Claim the launch's single raise before awaiting anything, so a + // concurrent request cannot start a second one while this waits. + screenPromptRaisedThisLaunch = true; + // Queue behind the startup mic alert (see startupMediaPromptGate). + // The marker is recorded inside the raise, on the other side of this + // await — recording it here would burn the machine's one prompt on + // an ask macOS never drew. + await startupMediaPromptGate; raiseScreenRecordingPrompt(); } diff --git a/electron/main.ts b/electron/main.ts index a85629bf3..56f0d2c12 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -45,6 +45,7 @@ import { exportDiagnosticFile, getSelectedDesktopSource, registerIpcHandlers, + setStartupMediaPromptGate, } from "./ipc/handlers"; import { installMainProcessErrorGuards } from "./main-process-errors"; import { registerSttIpc, shutdownStt } from "./stt"; @@ -1065,10 +1066,15 @@ appReady?.then(async () => { if (process.platform === "darwin") { const micStatus = systemPreferences.getMediaAccessStatus("microphone"); if (micStatus !== "granted") { - systemPreferences - .askForMediaAccess("microphone") - .then((granted) => console.info(`[permissions] microphone granted=${granted}`)) - .catch((error) => console.warn("[permissions] microphone request failed:", error)); + // The Screen Recording prompt queues behind this ask — macOS will not + // stack a second permission alert, so raising it mid-mic-prompt shows + // nothing and (worse) records a prompt that never appeared. + setStartupMediaPromptGate( + systemPreferences + .askForMediaAccess("microphone") + .then((granted) => console.info(`[permissions] microphone granted=${granted}`)) + .catch((error) => console.warn("[permissions] microphone request failed:", error)), + ); } } From 074aa8a657c61e8fce52c14cc37e37891b8efefd Mon Sep 17 00:00:00 2001 From: Aarohan Niraula Date: Mon, 31 Aug 2026 16:02:17 +0545 Subject: [PATCH 7/9] fix(macos): scope promptRaised to the call that raised the prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The field was returned as the sticky per-launch flag, so every request after the first reported promptRaised: true for the rest of the launch. That armed the renderer's wait on every later Record click too: a user who had already refused sat out the full retry budget before each dialog, when the handler's own contract says every refusal after the raising call gets it immediately — "including the second click of a launch". promptRaised now answers "did THIS call raise the prompt". The raising call still withholds the dialog and arms the wait; every other path — later clicks, the loop's re-entry, the error branch — reports false and reaches the dialog at once. The renderer's screenPromptWaitElapsed handshake keeps its meaning as the loop's explicit release signal. Claude-Session: https://claude.ai/code/session_01AeoFwWz1hQWoXeccpud6wo --- electron/electron-env.d.ts | 4 +-- electron/ipc/handlers.ts | 25 +++++++++++-------- .../launch/openSourceSelectorFlow.ts | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 681b69c64..517cb10ac 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -53,7 +53,7 @@ interface Window { /** What the OS actually said. Never bent to steer the caller. */ status: string; /** - * macOS' own prompt was raised this launch and may still be unanswered. This, and + * macOS' own prompt was raised by this very call and may still be unanswered. This, and * not `status`, is what tells the renderer to keep polling: macOS reports the * permission as absent for the whole time its prompt is on screen. */ @@ -94,7 +94,7 @@ interface Window { /** What the OS actually said. Never bent to steer the caller. */ status: string; /** - * macOS' own prompt was raised this launch and may still be unanswered. This, and + * macOS' own prompt was raised by this very call and may still be unanswered. This, and * not `status`, is what tells the renderer to keep polling: macOS reports the * permission as absent for the whole time its prompt is on screen. */ diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 08c900420..72499d604 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -547,9 +547,11 @@ export interface ScreenAccessResult { /** What the OS actually said. Never bent to steer the caller. */ status: string; /** - * Whether macOS' own prompt has been raised this launch and may still be unanswered. + * Whether THIS call raised macOS' own prompt, which may still be unanswered. * This, and not `status`, is what tells a caller to keep polling: macOS keeps * reporting the permission as absent for the whole time its prompt is on screen. + * Scoped to the raising call so every other request — the second click of a + * launch included — reaches the Settings dialog without sitting out the wait. */ promptRaised: boolean; /** @@ -1896,13 +1898,12 @@ export function registerIpcHandlers( }; } - if ( - shouldRaiseScreenPrompt({ - status, - raisedThisLaunch: screenPromptRaisedThisLaunch, - raisedBefore: getScreenPromptMarker().hasRaisedBefore(), - }) - ) { + const raiseNow = shouldRaiseScreenPrompt({ + status, + raisedThisLaunch: screenPromptRaisedThisLaunch, + raisedBefore: getScreenPromptMarker().hasRaisedBefore(), + }); + if (raiseNow) { // Claim the launch's single raise before awaiting anything, so a // concurrent request cannot start a second one while this waits. screenPromptRaisedThisLaunch = true; @@ -1917,12 +1918,14 @@ export function registerIpcHandlers( // `status` is what the OS actually said, always. Whether the caller should keep // polling rides on `promptRaised` instead -- a separate field for a separate // question, so no branch here has to misreport the permission to keep the - // renderer's retry loop alive. + // renderer's retry loop alive. Scoped to the call that raised the prompt: a + // sticky per-launch value would arm the wait on every later click too, making + // each one sit out the full retry budget before the dialog it was owed at once. return { success: true, granted: false, status, - promptRaised: screenPromptRaisedThisLaunch, + promptRaised: raiseNow, }; } catch (error) { console.error("Failed to request screen access:", error); @@ -1930,7 +1933,7 @@ export function registerIpcHandlers( success: false, granted: false, status: "unknown", - promptRaised: screenPromptRaisedThisLaunch, + promptRaised: false, error: String(error), }; } diff --git a/src/components/launch/openSourceSelectorFlow.ts b/src/components/launch/openSourceSelectorFlow.ts index 962892e07..e8fe0c1f9 100644 --- a/src/components/launch/openSourceSelectorFlow.ts +++ b/src/components/launch/openSourceSelectorFlow.ts @@ -4,7 +4,7 @@ export type ScreenAccessResult = { /** What the OS actually said. Never bent to steer this loop. */ status: string; /** - * macOS' own prompt was raised this launch and may still be unanswered. + * macOS' own prompt was raised by this very call and may still be unanswered. * * This, and not `status`, is what arms the wait below. macOS reports the permission * as absent for the whole time its prompt is on screen -- it only flips once the user From b1fa941fab1ed97568a5ca62073c08481835b63a Mon Sep 17 00:00:00 2001 From: Aarohan Niraula Date: Mon, 31 Aug 2026 16:02:59 +0545 Subject: [PATCH 8/9] fix(launch): share one selector flow between the Record button and the chip The permission wait keeps openSourceSelector's promise pending for seconds, and nothing locks the HUD while it runs: controlsLocked is recording || saving, both still false. Every extra Record click started another concurrent retry loop, each one toggling recordAfterSourceSelectionRef under the others, and the source chip was a second entry point doing the same. Both entry points now share a single in-flight flow: a click during the wait joins the running promise instead of starting a rival. The ref clears when the flow settles, so the next click after it starts fresh. Claude-Session: https://claude.ai/code/session_01AeoFwWz1hQWoXeccpud6wo --- src/components/launch/LaunchWindow.tsx | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 3a1d897cf..da4db3689 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -39,7 +39,10 @@ import { HUD_STACK_GAP, } from "./hudGeometry"; import styles from "./LaunchWindow.module.css"; -import { openSourceSelectorWithPermissionRetry } from "./openSourceSelectorFlow"; +import { + type OpenSourceSelectorResult, + openSourceSelectorWithPermissionRetry, +} from "./openSourceSelectorFlow"; // Locale list is computed once at module load; keeping the reference stable lets // the language menu sit behind a memo boundary. @@ -586,15 +589,28 @@ export function LaunchWindow() { }; }, [applySelectedSource, recording, toggleRecording]); + // One flow at a time, shared by the Record button and the source chip. The + // permission wait keeps this promise pending for seconds while the button + // stays live, and every extra click used to start a concurrent retry loop — + // all of them fighting over recordAfterSourceSelectionRef. + const sourceSelectorFlowRef = useRef | null>(null); const openSourceSelector = useCallback(async () => { - if (window.electronAPI) { - return await openSourceSelectorWithPermissionRetry({ - openSourceSelector: (options) => window.electronAPI.openSourceSelector(options), - requestScreenAccess: () => window.electronAPI.requestScreenAccess(), - }); + if (!window.electronAPI) { + return { opened: false, reason: "electron-api-unavailable" }; } - return { opened: false, reason: "electron-api-unavailable" }; + if (sourceSelectorFlowRef.current) { + return sourceSelectorFlowRef.current; + } + + const flow = openSourceSelectorWithPermissionRetry({ + openSourceSelector: (options) => window.electronAPI.openSourceSelector(options), + requestScreenAccess: () => window.electronAPI.requestScreenAccess(), + }).finally(() => { + sourceSelectorFlowRef.current = null; + }); + sourceSelectorFlowRef.current = flow; + return flow; }, []); const handleRecordButtonClick = useCallback( From df9d9fa47dacf40a2ec8672ef61b9cfbd6507fcc Mon Sep 17 00:00:00 2001 From: Aarohan Niraula Date: Mon, 31 Aug 2026 16:03:12 +0545 Subject: [PATCH 9/9] docs(native): record the helper's screen-access-status mode in its contract The README documents the helper's whole protocol with the app; the new one-shot permission read was only described at its call sites. Claude-Session: https://claude.ai/code/session_01AeoFwWz1hQWoXeccpud6wo --- electron/native/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/electron/native/README.md b/electron/native/README.md index 9bd69ae81..39058964e 100644 --- a/electron/native/README.md +++ b/electron/native/README.md @@ -9,6 +9,8 @@ macOS native recording will use a ScreenCaptureKit helper with the same process 3. The helper owns ScreenCaptureKit/AVFoundation capture, timing, encoding, and muxing. 4. Electron persists the resulting media/session manifest and reports helper errors explicitly. +The helper has one non-recording mode: `openscreen-screencapturekit-helper --screen-access-status` prints `{"event":"screen-access","granted":}` and exits. `CGPreflightScreenCaptureAccess()` caches its answer for the life of the calling process, so Electron's own read can never observe a Screen Recording grant made after launch — a helper spawned per read has no cache to be stale (`electron/native-bridge/screen/macScreenAccess.ts`). + Helper locations: 1. `OPENSCREEN_SCK_CAPTURE_EXE`, for local development and diagnostics. @@ -89,8 +91,8 @@ Encoder selection: by default the helper keeps the existing sink-writer path fir Frame input path: the helper feeds the encoder from the GPU when it can. On that path it copies the WGC frame across a keyed-mutex bridge to a second D3D11 device, converts BGRA to NV12 with the D3D11 video processor, and submits an allocator-owned DXGI sample to the hardware H.264 encoder, so no frame ever passes through system memory. The alternative is the original path: a staging texture, `Map(D3D11_MAP_READ)`, and a row-by-row copy into an `IMFMediaBuffer` — which is where a driver stall costs a recording (issue #252). The GPU path is a preference, never a requirement: it is skipped outright for `preferSoftwareEncoder` and for inline webcam PiP (both need the frame in system memory), and it degrades to the CPU path on its own if the encoding device, the NV12 video processor, the shared bridge texture, the DXGI sample allocator, or the hardware sink writer is unavailable. The GPU path is OFF by default: it fixed #252 on the machine that reproduces it and broke recording outright in #336, and its fallbacks only cover failures during `initialize()`, not one that appears once frames are flowing. Set `OPENSCREEN_WGC_ENABLE_DXGI_INPUT=1` to turn it on. Because the two paths land on different encoders and hardware MFTs default to constant bitrate, the GPU path asks for VBR through `ICodecAPI`; without it a static screen spends the full configured budget (measured 16.9 Mbps against 1.95 for the same desktop). -The helper reports the outcome through the `encoder-selection` stdout event (`video` is `default`, `software-preferred`, or `software-fallback`; `videoInput` is `dxgi-nv12` or `cpu-rgb32`; `container` is `fragmented-mp4` or `mp4`; all three report what the encoder settled on rather than what was asked for). On the GPU path the helper also prints one `[frame-drops] gpu_bridge_contended=` line to stderr at stop: a frame the bridge was too busy to take is skipped rather than failing the recording, and a large count there is the first thing to look at in a report about missing frames. When the app sees `software-fallback` — the default encoder failed and the helper switched on its own — it shows a small dismissible notice in the recording HUD with a "Don't show again" option, because software encoding can raise CPU usage. An explicit `software-preferred` selection shows no notice, and the event stays available for diagnostics either way. - +The helper reports the outcome through the `encoder-selection` stdout event (`video` is `default`, `software-preferred`, or `software-fallback`; `videoInput` is `dxgi-nv12` or `cpu-rgb32`; `container` is `fragmented-mp4` or `mp4`; all three report what the encoder settled on rather than what was asked for). On the GPU path the helper also prints one `[frame-drops] gpu_bridge_contended=` line to stderr at stop: a frame the bridge was too busy to take is skipped rather than failing the recording, and a large count there is the first thing to look at in a report about missing frames. When the app sees `software-fallback` — the default encoder failed and the helper switched on its own — it shows a small dismissible notice in the recording HUD with a "Don't show again" option, because software encoding can raise CPU usage. An explicit `software-preferred` selection shows no notice, and the event stays available for diagnostics either way. + At startup the helper also emits `capture-adapter`, naming the GPU its D3D device landed on and the one actually driving the captured display, each with its LUID, plus one `[adapters]` line per enumerated adapter on stderr. `createD3DDevice` asks for the *default* adapter and nothing checks that it is the one driving the display; when they differ every frame crosses an adapter boundary before the caller touches it. The LUIDs are there because the descriptions are not enough to tell: an IddCx virtual display driver renders through the physical GPU and inherits its description string while being a separate DXGI adapter, so the configuration this diagnostic exists to catch is precisely the one where both names are identical and only the LUIDs differ (measured: `NVIDIA Quadro RTX 4000` at LUID `0:24084` driving the display, the same string at `0:12889146` for the virtual adapter). `monitorLookup` says which of three things happened: `ok`, `no-output-claims-it` (the enumeration finished and nothing owns the captured monitor, which is what an active virtual display looks like), or `unavailable` (`EnumOutputs` refused, as it does in session 0 — the outputs were never inspected, so the absence means nothing about the hardware). Encoder diagnostic on final sink-writer failure: when the final sink-writer attempt fails (`MFCreateSinkWriterFromMediaSink` on the fragmented container, `MFCreateSinkWriterFromURL` on the plain one; the message names which), the helper logs the registered H.264 video encoder MFT count (via `MFTEnumEx`), the registered AAC encoder count when audio was requested, and the hex HRESULT. If no H.264 encoder is registered, it additionally emits the four-bullet actionable error (missing Media Feature Pack / GPU driver registration / empty `HKLM:\SOFTWARE\Microsoft\Windows Media Foundation\Transforms` / reboot). If an H.264 encoder IS registered but the sink writer still failed, it logs a hint pointing at invalid output path, missing MP4 mux, or GPU driver incompatibility. There is still no fail-fast pre-flight gate because `MFTEnumEx` and the sink writer can disagree about which H.264 encoders are available in non-interactive / Session 0 contexts.