diff --git a/electron/ipc/cursor/interaction.test.ts b/electron/ipc/cursor/interaction.test.ts index 6702f66cd..f126f6e33 100644 --- a/electron/ipc/cursor/interaction.test.ts +++ b/electron/ipc/cursor/interaction.test.ts @@ -11,7 +11,21 @@ vi.mock("electron", () => ({ }, })); -import { repairBundledUiohookBinaryForCurrentArch } from "./interaction"; +import { + repairBundledUiohookBinaryForCurrentArch, + shouldStartGlobalInteractionHook, +} from "./interaction"; + +describe("shouldStartGlobalInteractionHook", () => { + it("does not start the synchronous uiohook event tap on macOS", () => { + expect(shouldStartGlobalInteractionHook("darwin")).toBe(false); + }); + + it("keeps global interaction capture enabled on Windows and Linux", () => { + expect(shouldStartGlobalInteractionHook("win32")).toBe(true); + expect(shouldStartGlobalInteractionHook("linux")).toBe(true); + }); +}); describe("repairBundledUiohookBinaryForCurrentArch", () => { const tempRoots: string[] = []; @@ -68,4 +82,4 @@ describe("repairBundledUiohookBinaryForCurrentArch", () => { expect(repaired).toBe(false); expect(await fs.readFile(buildPath, "utf8")).toBe("existing-build"); }); -}); \ No newline at end of file +}); diff --git a/electron/ipc/cursor/interaction.ts b/electron/ipc/cursor/interaction.ts index 9b6f3ac92..47c42437f 100644 --- a/electron/ipc/cursor/interaction.ts +++ b/electron/ipc/cursor/interaction.ts @@ -1,21 +1,26 @@ import fs from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import type { HookMouseEvent, UiohookLike, UiohookModuleNamespace, CursorInteractionType } from "../types"; import { - isCursorCaptureActive, - interactionCaptureCleanup, - setInteractionCaptureCleanup, hasLoggedInteractionHookFailure, - setHasLoggedInteractionHookFailure, + interactionCaptureCleanup, + isCursorCaptureActive, lastLeftClick, + setHasLoggedInteractionHookFailure, + setInteractionCaptureCleanup, setLastLeftClick, setLinuxCursorScreenPoint, } from "../state"; +import type { + CursorInteractionType, + HookMouseEvent, + UiohookLike, + UiohookModuleNamespace, +} from "../types"; import { - getNormalizedCursorPoint, getCursorCaptureElapsedMs, getHookCursorScreenPoint, + getNormalizedCursorPoint, isCursorCapturePaused, pushCursorSample, } from "./telemetry"; @@ -172,6 +177,62 @@ function loadUiohookModule() { } } +export function shouldStartGlobalInteractionHook(platform: NodeJS.Platform = process.platform) { + // On macOS, uiohook can block forever while its native event tap starts + // (notably when Accessibility permission is unavailable or stale). Because + // start() executes synchronously, that freezes Electron's main thread and + // makes every window, including the recording HUD, unresponsive. Cursor + // position and visual-state telemetry still come from the existing native + // macOS monitor and Electron sampler. + return platform !== "darwin"; +} + +export function recordCursorMouseDown(button: 1 | 2 | 3) { + if (!isCursorCaptureActive || isCursorCapturePaused()) { + return; + } + + const point = getNormalizedCursorPoint(); + if (!point) { + return; + } + + const timeMs = getCursorCaptureElapsedMs(); + let interactionType: CursorInteractionType = "click"; + + if (button === 2) { + interactionType = "right-click"; + } else if (button === 3) { + interactionType = "middle-click"; + } else { + const thresholdMs = 350; + const distance = lastLeftClick + ? Math.hypot(point.cx - lastLeftClick.cx, point.cy - lastLeftClick.cy) + : Number.POSITIVE_INFINITY; + + if (lastLeftClick && timeMs - lastLeftClick.timeMs <= thresholdMs && distance <= 0.04) { + interactionType = "double-click"; + } + + setLastLeftClick({ timeMs, cx: point.cx, cy: point.cy }); + } + + pushCursorSample(point.cx, point.cy, timeMs, interactionType); +} + +export function recordCursorMouseUp() { + if (!isCursorCaptureActive || isCursorCapturePaused()) { + return; + } + + const point = getNormalizedCursorPoint(); + if (!point) { + return; + } + + pushCursorSample(point.cx, point.cy, getCursorCaptureElapsedMs(), "mouseup"); +} + export async function startInteractionCapture() { if (!isCursorCaptureActive) { return; @@ -181,6 +242,11 @@ export async function startInteractionCapture() { return; } + if (!shouldStartGlobalInteractionHook()) { + console.warn("[CursorTelemetry] Skipping the blocking global interaction hook on macOS."); + return; + } + stopInteractionCapture(); try { @@ -203,63 +269,15 @@ export async function startInteractionCapture() { } const onMouseDown = (event: HookMouseEvent) => { - if (!isCursorCaptureActive || isCursorCapturePaused()) { - return; - } - - const point = getNormalizedCursorPoint(); - if (!point) { - return; - } - - const timeMs = getCursorCaptureElapsedMs(); - const button = getHookMouseButton(event); - let interactionType: CursorInteractionType = "click"; - - if (button === 2) { - interactionType = "right-click"; - } else if (button === 3) { - interactionType = "middle-click"; - } else { - const thresholdMs = 350; - const distance = lastLeftClick - ? Math.hypot(point.cx - lastLeftClick.cx, point.cy - lastLeftClick.cy) - : Number.POSITIVE_INFINITY; - - if ( - lastLeftClick && - timeMs - lastLeftClick.timeMs <= thresholdMs && - distance <= 0.04 - ) { - interactionType = "double-click"; - } - - setLastLeftClick({ timeMs, cx: point.cx, cy: point.cy }); - } - - pushCursorSample(point.cx, point.cy, timeMs, interactionType); + recordCursorMouseDown(getHookMouseButton(event)); }; const onMouseUp = () => { - if (!isCursorCaptureActive || isCursorCapturePaused()) { - return; - } - - const point = getNormalizedCursorPoint(); - if (!point) { - return; - } - - const timeMs = getCursorCaptureElapsedMs(); - pushCursorSample(point.cx, point.cy, timeMs, "mouseup"); + recordCursorMouseUp(); }; const onMouseMove = (event: HookMouseEvent) => { - if ( - process.platform !== "linux" || - !isCursorCaptureActive || - isCursorCapturePaused() - ) { + if (process.platform !== "linux" || !isCursorCaptureActive || isCursorCapturePaused()) { return; } diff --git a/electron/ipc/cursor/monitor.ts b/electron/ipc/cursor/monitor.ts index a076713ec..8a507d56d 100644 --- a/electron/ipc/cursor/monitor.ts +++ b/electron/ipc/cursor/monitor.ts @@ -2,7 +2,7 @@ import { spawn } from "node:child_process"; import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import { BrowserWindow } from "electron"; -import type { CursorVisualType } from "../types"; +import { ensureNativeCursorMonitorBinary, getCursorMonitorExePath } from "../paths/binaries"; import { currentCursorVisualType, nativeCursorMonitorOutputBuffer, @@ -11,7 +11,8 @@ import { setNativeCursorMonitorOutputBuffer, setNativeCursorMonitorProcess, } from "../state"; -import { getCursorMonitorExePath, ensureNativeCursorMonitorBinary } from "../paths/binaries"; +import type { CursorVisualType } from "../types"; +import { recordCursorMouseDown, recordCursorMouseUp } from "./interaction"; export function emitCursorStateChanged(cursorType: CursorVisualType) { BrowserWindow.getAllWindows().forEach((window) => { @@ -27,6 +28,17 @@ export function handleCursorMonitorStdout(chunk: Buffer) { setNativeCursorMonitorOutputBuffer(lines.pop() ?? ""); for (const line of lines) { + const interactionMatch = line.match(/^INTERACTION:(mousedown|mouseup)(?::([123]))?$/); + if (interactionMatch) { + if (interactionMatch[1] === "mouseup") { + recordCursorMouseUp(); + } else { + const button = Number(interactionMatch[2]); + recordCursorMouseDown(button === 2 || button === 3 ? button : 1); + } + continue; + } + const match = line.match(/^STATE:(.+)$/); if (!match) continue; const next = match[1].trim() as CursorVisualType; diff --git a/electron/ipc/recording/mac.ts b/electron/ipc/recording/mac.ts index 01951345d..57a9e6881 100644 --- a/electron/ipc/recording/mac.ts +++ b/electron/ipc/recording/mac.ts @@ -77,6 +77,49 @@ export function waitForNativeCaptureStart(process: ChildProcessWithoutNullStream }); } +export function waitForNativeCaptureCommand( + process: ChildProcessWithoutNullStreams, + marker: "Recording paused" | "Recording resumed", +) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timed out waiting for ScreenCaptureKit helper: ${marker}`)); + }, 5000); + + let stdoutBuffer = ""; + const onStdout = (chunk: Buffer) => { + stdoutBuffer += chunk.toString(); + if (stdoutBuffer.includes(marker)) { + cleanup(); + resolve(); + } + }; + const onError = (error: Error) => { + cleanup(); + reject(error); + }; + const onExit = (code: number | null) => { + cleanup(); + reject( + new Error( + `Native capture helper exited before ${marker.toLowerCase()} (code ${code ?? "unknown"})`, + ), + ); + }; + const cleanup = () => { + clearTimeout(timer); + process.stdout.off("data", onStdout); + process.off("error", onError); + process.off("exit", onExit); + }; + + process.stdout.on("data", onStdout); + process.once("error", onError); + process.once("exit", onExit); + }); +} + export function waitForNativeCaptureStop(process: ChildProcessWithoutNullStreams) { return new Promise((resolve, reject) => { const onClose = (code: number | null) => { diff --git a/electron/ipc/register/recording.ts b/electron/ipc/register/recording.ts index 06a33f84f..fa9b32f36 100644 --- a/electron/ipc/register/recording.ts +++ b/electron/ipc/register/recording.ts @@ -65,6 +65,7 @@ import { finalizeStoredVideo, muxNativeMacRecordingWithAudio, recoverNativeMacCaptureOutput, + waitForNativeCaptureCommand, waitForNativeCaptureStart, waitForNativeCaptureStop, } from "../recording/mac"; @@ -1305,7 +1306,12 @@ export function registerRecordingHandlers( } try { + const commandApplied = waitForNativeCaptureCommand( + nativeCaptureProcess, + "Recording paused", + ); nativeCaptureProcess.stdin.write("pause\n"); + await commandApplied; setNativeCapturePaused(true); return { success: true }; } catch (error) { @@ -1356,7 +1362,12 @@ export function registerRecordingHandlers( } try { + const commandApplied = waitForNativeCaptureCommand( + nativeCaptureProcess, + "Recording resumed", + ); nativeCaptureProcess.stdin.write("resume\n"); + await commandApplied; setNativeCapturePaused(false); return { success: true }; } catch (error) { diff --git a/electron/native/NativeCursorMonitor.swift b/electron/native/NativeCursorMonitor.swift index 236f9bb9b..18c644150 100644 --- a/electron/native/NativeCursorMonitor.swift +++ b/electron/native/NativeCursorMonitor.swift @@ -406,6 +406,76 @@ if CommandLine.arguments.contains("--export-images") { exit(0) } +func mouseInteractionCallback( + proxy: CGEventTapProxy, + type: CGEventType, + event: CGEvent, + refcon: UnsafeMutableRawPointer? +) -> Unmanaged? { + let action: String + let button: Int + switch type { + case .leftMouseDown: + action = "mousedown" + button = 1 + case .leftMouseUp: + action = "mouseup" + button = 1 + case .rightMouseDown: + action = "mousedown" + button = 2 + case .rightMouseUp: + action = "mouseup" + button = 2 + case .otherMouseDown: + guard event.getIntegerValueField(.mouseEventButtonNumber) == 2 else { + return Unmanaged.passUnretained(event) + } + action = "mousedown" + button = 3 + case .otherMouseUp: + guard event.getIntegerValueField(.mouseEventButtonNumber) == 2 else { + return Unmanaged.passUnretained(event) + } + action = "mouseup" + button = 3 + default: + return Unmanaged.passUnretained(event) + } + + print("INTERACTION:\(action):\(button)") + fflush(stdout) + return Unmanaged.passUnretained(event) +} + +let mouseEventTypes: [CGEventType] = [ + .leftMouseDown, + .leftMouseUp, + .rightMouseDown, + .rightMouseUp, + .otherMouseDown, + .otherMouseUp, +] +let mouseEventMask = mouseEventTypes.reduce(CGEventMask(0)) { mask, type in + mask | (CGEventMask(1) << type.rawValue) +} +let mouseEventTap = CGEvent.tapCreate( + tap: .cgSessionEventTap, + place: .headInsertEventTap, + options: .listenOnly, + eventsOfInterest: mouseEventMask, + callback: mouseInteractionCallback, + userInfo: nil +) +if let mouseEventTap, + let eventTapSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0) { + CFRunLoopAddSource(CFRunLoopGetMain(), eventTapSource, .commonModes) + CGEvent.tapEnable(tap: mouseEventTap, enable: true) +} else { + fputs("Mouse interaction event tap unavailable; click telemetry disabled\n", stderr) + fflush(stderr) +} + var lastState = "" func emitStateIfNeeded() { let state = currentSystemCursorType() @@ -433,4 +503,3 @@ DispatchQueue.global(qos: .utility).async { } RunLoop.main.run() - diff --git a/electron/native/ScreenCaptureKitRecorder.swift b/electron/native/ScreenCaptureKitRecorder.swift index 1e2a397aa..d4c05a3ae 100644 --- a/electron/native/ScreenCaptureKitRecorder.swift +++ b/electron/native/ScreenCaptureKitRecorder.swift @@ -18,8 +18,17 @@ struct CaptureConfig: Codable { let targetCaptureFPS = 60 let maxInlineAudioTailExtension = CMTime(seconds: 2.0, preferredTimescale: 600) +/// How long finalization waits for a backed-up encoder queue before giving up on +/// the optional tail frame: 100 polls x 10 ms = 1 s. +let writerReadinessPollAttempts = 100 +let writerReadinessPollInterval: UInt64 = 10_000_000 final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { + private struct CaptureFinalizationResult { + let outputResult: Result + let interactiveStopParticipated: Bool + } + private let queue = DispatchQueue(label: "recordly.screencapturekit.video") private var assetWriter: AVAssetWriter? private var videoInput: AVAssetWriterInput? @@ -47,6 +56,9 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { private var microphoneOutputURL: URL? private var trackedWindowId: UInt32? private var windowValidationTask: Task? + private var isFinalizing = false + private var interactiveStopParticipated = false + private var finalizationWaiters: [CheckedContinuation] = [] private var inlineAudioInput: AVAssetWriterInput? private var firstInlineAudioSampleTime: CMTime? private var capturesSystemAudio = false @@ -271,29 +283,40 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { lastVideoPresentationTime = .zero lastVideoDuration = .zero startWindowValidationIfNeeded() - print("Recording started") - fflush(stdout) } func stopCapture() async throws -> String { - guard isRecording else { - throw NSError(domain: "RecordlyCapture", code: 9, userInfo: [NSLocalizedDescriptionKey: "No recording in progress"]) - } - - return try await finishCapture() + let finalization = await finalizeCapture(interactive: true) + return try finalization.outputResult.get() } - func pauseCapture() { - guard isRecording, !isPaused else { return } - isPaused = true - pauseStartedHostTime = CMClockGetTime(CMClockGetHostTimeClock()) - pendingResumeAdjustment = false + func pauseCapture() async -> Bool { + await withCheckedContinuation { continuation in + queue.async { + guard self.isRecording, !self.isPaused else { + continuation.resume(returning: self.isRecording && self.isPaused) + return + } + self.isPaused = true + self.pauseStartedHostTime = CMClockGetTime(CMClockGetHostTimeClock()) + self.pendingResumeAdjustment = false + continuation.resume(returning: true) + } + } } - func resumeCapture() { - guard isRecording, isPaused else { return } - isPaused = false - pendingResumeAdjustment = true + func resumeCapture() async -> Bool { + await withCheckedContinuation { continuation in + queue.async { + guard self.isRecording, self.isPaused else { + continuation.resume(returning: self.isRecording && !self.isPaused) + return + } + self.isPaused = false + self.pendingResumeAdjustment = true + continuation.resume(returning: true) + } + } } func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { @@ -309,7 +332,9 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { return } - guard let videoInput = videoInput, videoInput.isReadyForMoreMediaData else { return } + guard let videoInput = videoInput, + assetWriter?.status == .writing, + videoInput.isReadyForMoreMediaData else { return } if firstSampleTime == .zero { firstSampleTime = sampleBuffer.presentationTimeStamp @@ -318,31 +343,38 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { lastSampleBuffer = sampleBuffer let timing = CMSampleTimingInfo(duration: sampleBuffer.duration, presentationTimeStamp: presentationTime, decodeTimeStamp: sampleBuffer.decodeTimeStamp) if let retimedSampleBuffer = try? CMSampleBuffer(copying: sampleBuffer, withNewTiming: [timing]) { - videoInput.append(retimedSampleBuffer) - lastVideoPresentationTime = presentationTime - lastVideoDuration = sampleBuffer.duration - frameCount += 1 + if videoInput.append(retimedSampleBuffer) { + lastVideoPresentationTime = presentationTime + lastVideoDuration = sampleBuffer.duration + frameCount += 1 + if frameCount == 1 { + // Signal readiness only after AVAssetWriter has accepted a + // real frame, so countdown warm-start cannot pause too early. + print("Recording started") + fflush(stdout) + } + } } return } if outputType == .audio { guard let systemAudioInput else { return } - appendAudioSampleBuffer(sampleBuffer, to: systemAudioInput, firstSampleTime: &firstSystemAudioSampleTime, presentationTime: presentationTime) + appendAudioSampleBuffer(sampleBuffer, to: systemAudioInput, of: systemAudioWriter, firstSampleTime: &firstSystemAudioSampleTime, presentationTime: presentationTime) // Also write system audio to the inline video track if let inlineAudioInput, inlineAudioInput.isReadyForMoreMediaData { - appendAudioSampleBuffer(sampleBuffer, to: inlineAudioInput, firstSampleTime: &firstInlineAudioSampleTime, presentationTime: presentationTime) + appendAudioSampleBuffer(sampleBuffer, to: inlineAudioInput, of: assetWriter, firstSampleTime: &firstInlineAudioSampleTime, presentationTime: presentationTime) } return } if outputType.rawValue == microphoneOutputTypeRawValue { if let microphoneOnlyInput { - appendAudioSampleBuffer(sampleBuffer, to: microphoneOnlyInput, firstSampleTime: &firstMicrophoneSampleTime, presentationTime: presentationTime) + appendAudioSampleBuffer(sampleBuffer, to: microphoneOnlyInput, of: microphoneOnlyWriter, firstSampleTime: &firstMicrophoneSampleTime, presentationTime: presentationTime) } // Write mic to inline video track only if there's no system audio (avoids double-writing) if !capturesSystemAudio, let inlineAudioInput, inlineAudioInput.isReadyForMoreMediaData { - appendAudioSampleBuffer(sampleBuffer, to: inlineAudioInput, firstSampleTime: &firstInlineAudioSampleTime, presentationTime: presentationTime) + appendAudioSampleBuffer(sampleBuffer, to: inlineAudioInput, of: assetWriter, firstSampleTime: &firstInlineAudioSampleTime, presentationTime: presentationTime) } return } @@ -355,10 +387,64 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { fflush(stderr) } + /// Starts one finalization operation after all previously delivered samples on + /// the recorder queue have drained. Manual stop and automatic window-close + /// detection join the same operation instead of racing the asset writers. + private func finalizeCapture(interactive: Bool) async -> CaptureFinalizationResult { + await withCheckedContinuation { continuation in + queue.async { + if self.isFinalizing { + self.interactiveStopParticipated = self.interactiveStopParticipated || interactive + self.finalizationWaiters.append(continuation) + return + } + + guard self.isRecording else { + continuation.resume(returning: CaptureFinalizationResult( + outputResult: .failure(NSError( + domain: "RecordlyCapture", + code: 9, + userInfo: [NSLocalizedDescriptionKey: "No recording in progress"] + )), + interactiveStopParticipated: interactive + )) + return + } + + self.isFinalizing = true + self.interactiveStopParticipated = interactive + self.isRecording = false + self.windowValidationTask = nil + self.trackedWindowId = nil + self.finalizationWaiters.append(continuation) + + Task { + let outputResult: Result + do { + outputResult = .success(try await self.finishCapture()) + } catch { + outputResult = .failure(error) + } + + self.queue.async { + let finalizationResult = CaptureFinalizationResult( + outputResult: outputResult, + interactiveStopParticipated: self.interactiveStopParticipated + ) + let waiters = self.finalizationWaiters + self.finalizationWaiters.removeAll() + self.isFinalizing = false + self.interactiveStopParticipated = false + for waiter in waiters { + waiter.resume(returning: finalizationResult) + } + } + } + } + } + } + private func finishCapture() async throws -> String { - windowValidationTask?.cancel() - windowValidationTask = nil - trackedWindowId = nil if let activeStream = stream { do { @@ -368,9 +454,17 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { } } stream = nil - isRecording = false - if let originalBuffer = lastSampleBuffer, let videoInput = videoInput { + // The tail frame only gives the last captured frame its full duration, so + // it must never put the file at risk. Appending to an input whose encoder + // queue is still backed up — routine after a long high-resolution capture — + // raises an Objective-C exception that Swift cannot catch, aborting the + // helper before `finishWriting()` and leaving an mdat with no moov atom: + // an unplayable recording. Wait briefly for the queue to drain, then skip + // the frame rather than lose the recording. + if let originalBuffer = lastSampleBuffer, + let videoInput = videoInput, + await waitUntilReady(videoInput, of: assetWriter) { let additionalTime = lastVideoPresentationTime + frameDuration(for: originalBuffer) let timing = CMSampleTimingInfo(duration: originalBuffer.duration, presentationTimeStamp: additionalTime, decodeTimeStamp: originalBuffer.decodeTimeStamp) if let additionalSampleBuffer = try? CMSampleBuffer(copying: originalBuffer, withNewTiming: [timing]) { @@ -378,19 +472,36 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { } } + // `endSession`, `markAsFinished` and `finishWriting` all raise when the + // writer is no longer in the `.writing` state (a mid-capture failure, for + // example a full disk), which would abort the helper the same way. let videoEndTime = lastVideoPresentationTime + (lastSampleBuffer.map { frameDuration(for: $0) } ?? .zero) let endTime = resolvedCaptureEndTime(videoEndTime: videoEndTime) - assetWriter?.endSession(atSourceTime: endTime) - videoInput?.markAsFinished() - inlineAudioInput?.markAsFinished() - await assetWriter?.finishWriting() + if let assetWriter, assetWriter.status == .writing { + assetWriter.endSession(atSourceTime: endTime) + videoInput?.markAsFinished() + inlineAudioInput?.markAsFinished() + await assetWriter.finishWriting() + } - systemAudioInput?.markAsFinished() - await systemAudioWriter?.finishWriting() + if let systemAudioWriter, systemAudioWriter.status == .writing { + systemAudioInput?.markAsFinished() + await systemAudioWriter.finishWriting() + } - microphoneOnlyInput?.markAsFinished() - await microphoneOnlyWriter?.finishWriting() + if let microphoneOnlyWriter, microphoneOnlyWriter.status == .writing { + microphoneOnlyInput?.markAsFinished() + await microphoneOnlyWriter.finishWriting() + } + let finalizeFailure: Error? = [assetWriter, systemAudioWriter, microphoneOnlyWriter] + .compactMap { $0 } + .compactMap { writer in + writer.status == .completed + ? nil + : (writer.error ?? unfinalizedWriterError(status: writer.status)) + } + .first let path = outputURL?.path ?? "" assetWriter = nil videoInput = nil @@ -420,9 +531,48 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { capturesMicrophone = false writesSystemAudioToSeparateTrack = false writesMicrophoneToSeparateTrack = false + + // Report a half-written file as a failure instead of handing the editor a + // path it cannot decode. + if let finalizeFailure { + throw finalizeFailure + } + return path } + /// Waits briefly for an input's encoder queue to drain. Returns false when the + /// input stays backed up or its writer is no longer accepting data, in which + /// case the caller must skip the append: `AVAssetWriterInput.append` raises an + /// uncatchable Objective-C exception in both cases. + private func waitUntilReady(_ input: AVAssetWriterInput, of writer: AVAssetWriter?) async -> Bool { + guard let writer else { return false } + + var attemptsRemaining = writerReadinessPollAttempts + while writer.status == .writing { + if input.isReadyForMoreMediaData { + return true + } + guard attemptsRemaining > 0 else { return false } + attemptsRemaining -= 1 + do { + try await Task.sleep(nanoseconds: writerReadinessPollInterval) + } catch is CancellationError { + return false + } catch { + return false + } + } + + return false + } + + private func unfinalizedWriterError(status: AVAssetWriter.Status) -> Error { + NSError(domain: "RecordlyCapture", code: 10, userInfo: [ + NSLocalizedDescriptionKey: "Recording could not be finalized (writer status \(status.rawValue))", + ]) + } + private func adjustedPresentationTime(for sampleBuffer: CMSampleBuffer, outputType: SCStreamOutputType) -> CMTime? { if isPaused { return nil @@ -497,8 +647,10 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { return videoEndTime + CMTimeMinimum(tailExtension, maxInlineAudioTailExtension) } - private func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer, to input: AVAssetWriterInput, firstSampleTime: inout CMTime?, presentationTime: CMTime) { - guard input.isReadyForMoreMediaData else { return } + private func appendAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer, to input: AVAssetWriterInput, of writer: AVAssetWriter?, firstSampleTime: inout CMTime?, presentationTime: CMTime) { + // A writer that failed mid-capture (a full disk, say) raises on every + // further append, which would abort the helper and lose the whole file. + guard writer?.status == .writing, input.isReadyForMoreMediaData else { return } if firstSampleTime == nil { firstSampleTime = presentationTime @@ -565,19 +717,31 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate { if Task.isCancelled { return } guard self.isRecording else { return } + let availableContent: SCShareableContent do { - let availableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) - let windowStillAvailable = availableContent.windows.contains(where: { $0.windowID == trackedWindowId }) - if !windowStillAvailable { - print("WINDOW_UNAVAILABLE") - fflush(stdout) - let outputPath = try await self.finishCapture() + availableContent = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true) + } catch { + continue + } + + let windowStillAvailable = availableContent.windows.contains(where: { $0.windowID == trackedWindowId }) + if !windowStillAvailable { + print("WINDOW_UNAVAILABLE") + fflush(stdout) + let finalization = await self.finalizeCapture(interactive: false) + if finalization.interactiveStopParticipated { + return + } + do { + let outputPath = try finalization.outputResult.get() print("Recording stopped. Output path: \(outputPath)") fflush(stdout) exit(0) + } catch { + fputs("Error stopping capture: \(error.localizedDescription)\n", stderr) + fflush(stderr) + exit(1) } - } catch { - continue } } } @@ -595,53 +759,70 @@ final class RecorderService { private let recorder = ScreenCaptureRecorder() private let queue = DispatchQueue(label: "recordly.screencapturekit.commands") private let completionGroup = DispatchGroup() + private var succeeded = true - func start(configJSON: String) { - completionGroup.enter() + private func enqueue(_ operation: @escaping () async -> Void) { queue.async { + let semaphore = DispatchSemaphore(value: 0) Task { - do { - try await self.recorder.startCapture(configJSON: configJSON) - } catch { - fputs("Error starting capture: \(error.localizedDescription)\n", stderr) - fflush(stderr) - self.completionGroup.leave() - } + await operation() + semaphore.signal() + } + semaphore.wait() + } + } + + func start(configJSON: String) { + completionGroup.enter() + enqueue { + do { + try await self.recorder.startCapture(configJSON: configJSON) + } catch { + self.succeeded = false + fputs("Error starting capture: \(error.localizedDescription)\n", stderr) + fflush(stderr) + self.completionGroup.leave() } } } func stop() { - queue.async { - Task { - do { - let outputPath = try await self.recorder.stopCapture() - print("Recording stopped. Output path: \(outputPath)") - fflush(stdout) - self.completionGroup.leave() - } catch { - fputs("Error stopping capture: \(error.localizedDescription)\n", stderr) - fflush(stderr) - self.completionGroup.leave() - } + enqueue { + do { + let outputPath = try await self.recorder.stopCapture() + print("Recording stopped. Output path: \(outputPath)") + fflush(stdout) + self.completionGroup.leave() + } catch { + self.succeeded = false + fputs("Error stopping capture: \(error.localizedDescription)\n", stderr) + fflush(stderr) + self.completionGroup.leave() } } } func pause() { - queue.async { - self.recorder.pauseCapture() + enqueue { + if await self.recorder.pauseCapture() { + print("Recording paused") + fflush(stdout) + } } } func resume() { - queue.async { - self.recorder.resumeCapture() + enqueue { + if await self.recorder.resumeCapture() { + print("Recording resumed") + fflush(stdout) + } } } - func waitUntilFinished() { + func waitUntilFinished() -> Bool { completionGroup.wait() + return succeeded } } @@ -714,5 +895,6 @@ DispatchQueue.global(qos: .utility).async { } } -service.waitUntilFinished() - +if !service.waitUntilFinished() { + exit(1) +} diff --git a/electron/native/ScreenCaptureKitRecorder.test.ts b/electron/native/ScreenCaptureKitRecorder.test.ts new file mode 100644 index 000000000..0e269bd4f --- /dev/null +++ b/electron/native/ScreenCaptureKitRecorder.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const recorderSource = readFileSync( + fileURLToPath(new URL("./ScreenCaptureKitRecorder.swift", import.meta.url)), + "utf8", +); + +describe("ScreenCaptureKitRecorder finalization coordination", () => { + it("marks manual stops as participants in the shared finalization", () => { + expect(recorderSource).toContain("finalizeCapture(interactive: true)"); + expect(recorderSource).toContain("finalization.outputResult.get()"); + expect(recorderSource).toContain( + "self.interactiveStopParticipated = self.interactiveStopParticipated || interactive", + ); + }); + + it("does not let automatic window-close exit preempt a joined manual stop", () => { + expect(recorderSource).toContain("self.finalizeCapture(interactive: false)"); + expect(recorderSource).toMatch( + /if finalization\.interactiveStopParticipated\s*\{\s*return\s*\}/, + ); + }); +}); diff --git a/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor index e0e478f11..962161c1c 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor and b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper index 259f18848..242bf20ef 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper and b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-x64/recordly-native-cursor-monitor b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor index d577b1a6f..3e55919f1 100755 Binary files a/electron/native/bin/darwin-x64/recordly-native-cursor-monitor and b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-x64/recordly-screencapturekit-helper b/electron/native/bin/darwin-x64/recordly-screencapturekit-helper index 09e0ed5d6..7a4ce3128 100755 Binary files a/electron/native/bin/darwin-x64/recordly-screencapturekit-helper and b/electron/native/bin/darwin-x64/recordly-screencapturekit-helper differ diff --git a/electron/native/bin/win32-x64/cursor-monitor.exe b/electron/native/bin/win32-x64/cursor-monitor.exe index ef71282d9..70738d118 100644 Binary files a/electron/native/bin/win32-x64/cursor-monitor.exe and b/electron/native/bin/win32-x64/cursor-monitor.exe differ diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index c5080b665..c47558692 100644 --- a/electron/native/bin/win32-x64/helpers-manifest.json +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -5,24 +5,24 @@ "helpers": { "wgc-capture": { "binaryName": "wgc-capture.exe", - "binarySha256": "298b41f371c3881046061048b466e12ed70dd93fa761bade2fd57d1ccddf3cb9", + "binarySha256": "4d89fdff8e3343998c7a3b4d75d964c1f75f93594aa097c6681e477d25ec9f01", "sourceDir": "electron/native/wgc-capture", - "sourceFingerprint": "6ee457080c27dc939ff4b61965f86b6d73995e40200440a1dc44865f9708d39f", - "updatedAt": "2026-05-24T19:49:15.077Z" + "sourceFingerprint": "c6dac250c9d16f7aa881998353441b4ae3fb59731b802e3b8491a136e79726b0", + "updatedAt": "2026-07-11T11:58:45.856Z" }, "cursor-monitor": { "binaryName": "cursor-monitor.exe", - "binarySha256": "6ae6d91103b6e891a851e8ea5791e1c1f9aaab700134c18bc4c46cfffd7fdd12", + "binarySha256": "f1d8f30e8d7bee19ecea91c9a90a95ea4824138b8336b18030fa0602a641d70d", "sourceDir": "electron/native/cursor-monitor", - "sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e", - "updatedAt": "2026-05-07T15:22:18.173Z" + "sourceFingerprint": "45bb72e4039e061a354af87317d7c42ec3d2118369b3f4379046ff497b701e72", + "updatedAt": "2026-07-11T11:58:56.534Z" }, "recordly-gpu-export": { "binaryName": "recordly-gpu-export.exe", - "binarySha256": "4cb3a293fd36f718af55906820d9b3fd78babc855888c2e248f0b918ec1aff3c", + "binarySha256": "49a2ac588206305d0129e6263ce4be49780c50a9dc4efc7df63aad09178a919f", "sourceDir": "electron/native/gpu-export-probe", - "sourceFingerprint": "743b386a5f1bbcc99cec5465c3de228d2b045061dead31dfcbf25cf6a1e61de5", - "updatedAt": "2026-05-07T20:13:48.585Z" + "sourceFingerprint": "37a5842eba63cdeccfddd02cc278a98207248fb0aa6b56153fb85ed152c908c1", + "updatedAt": "2026-07-11T11:58:51.659Z" }, "recordly-nvidia-cuda-compositor": { "binaryName": "recordly-nvidia-cuda-compositor.exe", diff --git a/electron/native/bin/win32-x64/recordly-gpu-export.exe b/electron/native/bin/win32-x64/recordly-gpu-export.exe index 3d0f57f58..b9d1efc9e 100644 Binary files a/electron/native/bin/win32-x64/recordly-gpu-export.exe and b/electron/native/bin/win32-x64/recordly-gpu-export.exe differ diff --git a/electron/native/bin/win32-x64/wgc-capture.exe b/electron/native/bin/win32-x64/wgc-capture.exe index 8180edd00..a882ee87a 100644 Binary files a/electron/native/bin/win32-x64/wgc-capture.exe and b/electron/native/bin/win32-x64/wgc-capture.exe differ diff --git a/electron/windows.ts b/electron/windows.ts index 55f6314cd..8a10981f2 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -290,9 +290,7 @@ function setHudOverlayMousePassthrough(ignore: boolean) { hudOverlayIgnoringMouse = hudOverlaySourceSelectionActive && !hudOverlayRecordingActive ? true - : hudOverlayRecordingActive - ? false - : ignore; + : ignore; if (hudOverlayMouseReassertTimer) { clearTimeout(hudOverlayMouseReassertTimer); @@ -306,8 +304,6 @@ function setHudOverlayMousePassthrough(ignore: boolean) { if (hudOverlayRecordingActive) { hudOverlayFallbackExpanded = false; applyHudOverlayBounds(); - hudOverlayWindow.setIgnoreMouseEvents(false); - return; } if (!isHudOverlayMousePassthroughSupported()) { @@ -638,11 +634,6 @@ export function reassertHudOverlayMousePassthrough(): void { return; } - if (hudOverlayRecordingActive) { - hud.setIgnoreMouseEvents(false); - return; - } - // Toggle off then back on so the native WS_EX_TRANSPARENT flag is fully // re-initialised rather than merely re-asserted in a potentially broken state. hud.setIgnoreMouseEvents(false); @@ -661,7 +652,10 @@ export function setHudOverlayRecordingActive(recording: boolean): void { hudOverlayRecordingActive = Boolean(recording); hudOverlayFallbackExpanded = false; applyHudOverlayBounds(); - setHudOverlayMousePassthrough(!hudOverlayRecordingActive); + // Start in passthrough mode. Forwarded pointer movement lets the renderer + // make the visible HUD controls interactive when the pointer reaches them, + // while transparent parts never block the recorded application. + setHudOverlayMousePassthrough(true); } export function createUpdateToastWindow(): BrowserWindow { diff --git a/package-lock.json b/package-lock.json index 71432b2b1..89ab3d499 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "vite": "^5.1.6", "vite-plugin-electron": "^0.28.6", "vite-plugin-electron-renderer": "^0.14.5", - "vitest": "^4.1.10", + "vitest": "^2.1.9", "web-demuxer": "^4.0.0" } }, @@ -1071,40 +1071,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1394,24 +1360,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", @@ -1429,24 +1377,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", @@ -1464,24 +1394,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", @@ -1868,25 +1780,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1925,16 +1818,6 @@ "node": ">= 8" } }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@phosphor-icons/react": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", @@ -2905,27 +2788,45 @@ "dev": true, "license": "MIT" }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", "cpu": [ "arm64" ], @@ -2934,15 +2835,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", "cpu": [ "x64" ], @@ -2951,426 +2849,154 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "freebsd" + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", "cpu": [ - "s390x" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", "cpu": [ - "x64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", "cpu": [ - "x64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "linux" + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", "cpu": [ - "wasm32" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", - "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", - "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", - "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", - "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", - "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", - "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", - "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", - "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", - "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", - "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", - "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", - "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", - "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", - "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", - "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", - "cpu": [ - "s390x" + "s390x" ], "dev": true, "license": "MIT", @@ -3490,13 +3116,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -3510,17 +3129,6 @@ "node": ">=10" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3579,17 +3187,6 @@ "@types/responselike": "^1.0.0" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, "node_modules/@types/css-font-loading-module": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz", @@ -3607,13 +3204,6 @@ "@types/ms": "*" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/dom-mediacapture-transform": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", @@ -3859,86 +3449,113 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" }, "funding": { "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", "dev": true, "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -4734,6 +4351,16 @@ "node": ">= 10.0.0" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -4827,11 +4454,18 @@ "license": "Apache-2.0" }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, "engines": { "node": ">=18" } @@ -4853,6 +4487,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5168,6 +4812,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -5232,6 +4886,8 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -5802,9 +5458,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", - "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -6974,6 +6630,8 @@ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -7011,6 +6669,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7032,6 +6691,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7053,6 +6713,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7074,6 +6735,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7095,6 +6757,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7116,6 +6779,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7137,6 +6801,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7158,6 +6823,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7179,6 +6845,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7200,6 +6867,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7221,6 +6889,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7281,6 +6950,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -7777,17 +7453,6 @@ "node": ">= 0.4" } }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7909,12 +7574,22 @@ "license": "ISC" }, "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", @@ -8711,47 +8386,6 @@ "node": ">=8.0" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, "node_modules/rollup": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", @@ -9071,9 +8705,9 @@ } }, "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "dev": true, "license": "MIT" }, @@ -9518,14 +9152,11 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.17", @@ -9575,10 +9206,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "license": "MIT", "engines": { @@ -9878,6 +9529,29 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-electron": { "version": "0.28.8", "resolved": "https://registry.npmjs.org/vite-plugin-electron/-/vite-plugin-electron-0.28.8.tgz", @@ -9901,79 +9575,58 @@ "license": "MIT" }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@opentelemetry/api": { - "optional": true - }, "@types/node": { "optional": true }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { + "@vitest/browser": { "optional": true }, "@vitest/ui": { @@ -9984,585 +9637,6 @@ }, "jsdom": { "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/vitest/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest/node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, diff --git a/package.json b/package.json index bf17e9ebb..f930a7007 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "vite": "^5.1.6", "vite-plugin-electron": "^0.28.6", "vite-plugin-electron-renderer": "^0.14.5", - "vitest": "^4.1.10", + "vitest": "^2.1.9", "web-demuxer": "^4.0.0" }, "main": "dist-electron/main.cjs" diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09cca63ef..9bc34c85b 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -438,6 +438,7 @@ function LaunchWindowContent() { const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle"; const useNativeHudBarDrag = platform === "linux" || hudOverlayMousePassthroughSupported === false; + const shouldAnimateHudLayout = !recording && !showRecordingWebcamPreview && !isHudDragging; return ( @@ -487,7 +488,7 @@ function LaunchWindowContent() { { }); }); +describe("stopAndDiscardNativeCapture", () => { + it("deletes the partial recording after a successful warm-start stop", async () => { + const deleteRecordingFile = vi.fn().mockResolvedValue(undefined); + + await expect( + stopAndDiscardNativeCapture({ + stopNativeScreenRecording: vi.fn().mockResolvedValue({ + success: true, + path: "C:\\Recordly\\warm-start.mp4", + }), + deleteRecordingFile, + }), + ).resolves.toEqual({ + stopSucceeded: true, + deleteSucceeded: true, + path: "C:\\Recordly\\warm-start.mp4", + }); + expect(deleteRecordingFile).toHaveBeenCalledWith("C:\\Recordly\\warm-start.mp4"); + }); + + it("reports an unsuccessful stop without deleting or confirming cleanup", async () => { + const deleteRecordingFile = vi.fn(); + + await expect( + stopAndDiscardNativeCapture({ + stopNativeScreenRecording: vi.fn().mockResolvedValue({ + success: false, + error: "helper still running", + }), + deleteRecordingFile, + }), + ).resolves.toEqual({ + stopSucceeded: false, + deleteSucceeded: false, + error: "helper still running", + }); + expect(deleteRecordingFile).not.toHaveBeenCalled(); + }); + + it("keeps the stopped path available when deletion fails so cleanup can retry", async () => { + const deleteError = new Error("file locked"); + + await expect( + stopAndDiscardNativeCapture({ + stopNativeScreenRecording: vi.fn().mockResolvedValue({ + success: true, + path: "C:\\Recordly\\warm-start.mp4", + }), + deleteRecordingFile: vi.fn().mockRejectedValue(deleteError), + }), + ).resolves.toEqual({ + stopSucceeded: true, + deleteSucceeded: false, + path: "C:\\Recordly\\warm-start.mp4", + error: deleteError, + }); + }); +}); + function stopRecording( recorder: ReturnType, isNativeRecording: boolean, diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index c5cd70056..a696c4b20 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -213,10 +213,7 @@ export function resolveBrowserCaptureCursorPolicy({ export function shouldUseNativeWindowsCaptureForSource( source: Pick | null | undefined, ): boolean { - return ( - source?.id?.startsWith("screen:") === true || - source?.id?.startsWith("window:") === true - ); + return source?.id?.startsWith("screen:") === true || source?.id?.startsWith("window:") === true; } export function createProcessedMicrophoneConstraints( @@ -265,6 +262,63 @@ export function createBrowserRecordingOptions({ return options; } +type NativeCaptureStopResult = { + success: boolean; + path?: string; + error?: string; + message?: string; +}; + +export type DiscardNativeCaptureResult = { + stopSucceeded: boolean; + deleteSucceeded: boolean; + path?: string; + error?: unknown; +}; + +export async function stopAndDiscardNativeCapture({ + stopNativeScreenRecording, + deleteRecordingFile, +}: { + stopNativeScreenRecording: () => Promise; + deleteRecordingFile: (path: string) => Promise; +}): Promise { + let stoppedResult: NativeCaptureStopResult; + try { + stoppedResult = await stopNativeScreenRecording(); + } catch (error) { + return { stopSucceeded: false, deleteSucceeded: false, error }; + } + + if (!stoppedResult.success) { + return { + stopSucceeded: false, + deleteSucceeded: false, + error: stoppedResult.error ?? stoppedResult.message, + }; + } + + if (!stoppedResult.path) { + return { stopSucceeded: true, deleteSucceeded: true }; + } + + try { + await deleteRecordingFile(stoppedResult.path); + return { + stopSucceeded: true, + deleteSucceeded: true, + path: stoppedResult.path, + }; + } catch (error) { + return { + stopSucceeded: true, + deleteSucceeded: false, + path: stoppedResult.path, + error, + }; + } +} + function createMicrophoneTrackSettingsSnapshot( stream: MediaStream, ): MicrophoneTrackSettingsSnapshot | null { @@ -347,6 +401,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const recordingSessionTimestamp = useRef(null); const nativeScreenRecording = useRef(false); const nativeWindowsRecording = useRef(false); + const nativeWarmStartActive = useRef(false); + const pendingNativeCleanupPath = useRef(null); + const recordingStartGeneration = useRef(0); + const nativeStopRequestInFlight = useRef(false); const startInFlight = useRef(false); const hasPromptedForReselect = useRef(false); const hasShownNativeWindowsFallbackToast = useRef(false); @@ -906,6 +964,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { webcamStopPromise.current = null; pendingWebcamPathPromise.current = null; resolvedWebcamPath.current = result ?? null; + webcamRecorder.current = null; return result ?? null; }, []); @@ -1066,10 +1125,165 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }, []); + const prepareRecordingStart = useCallback(async () => { + const platform = await window.electronAPI.getPlatform(); + hideEditorOverlayCursorByDefault.current = false; + const existingSource = await window.electronAPI.getSelectedSource(); + const selectedSource = + existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); + if (!selectedSource) { + alert("Please select a source to record"); + return null; + } + + if (!existingSource && selectedSource.id === "screen:linux-portal") { + try { + await window.electronAPI.selectSource(selectedSource); + } catch (err) { + console.warn("Failed to persist Linux portal sentinel source:", err); + } + } + + const permissionsReady = await preparePermissions(); + if (!permissionsReady) { + return null; + } + + recordingSessionTimestamp.current = Date.now(); + resetRecordingClock(recordingSessionTimestamp.current); + await prepareWebcamRecorder(); + + const useNativeMacScreenCapture = + platform === "darwin" && + (selectedSource.id?.startsWith("screen:") || + selectedSource.id?.startsWith("window:")) && + typeof window.electronAPI.startNativeScreenRecording === "function"; + + let useNativeWindowsCapture = false; + if ( + platform === "win32" && + shouldUseNativeWindowsCaptureForSource(selectedSource) && + typeof window.electronAPI.isNativeWindowsCaptureAvailable === "function" + ) { + try { + const nativeWindowsResult = + await window.electronAPI.isNativeWindowsCaptureAvailable(); + useNativeWindowsCapture = nativeWindowsResult.available; + if (!useNativeWindowsCapture && !hasShownNativeWindowsFallbackToast.current) { + void logNativeCaptureDiagnostics("is-native-windows-capture-available"); + hasShownNativeWindowsFallbackToast.current = true; + toast.info( + "Native Windows capture is unavailable. Falling back to browser capture.", + ); + } + } catch { + useNativeWindowsCapture = false; + if (!hasShownNativeWindowsFallbackToast.current) { + hasShownNativeWindowsFallbackToast.current = true; + toast.info( + "Unable to check native Windows capture. Falling back to browser capture.", + ); + } + } + } + + let micLabel: string | undefined; + if ((useNativeMacScreenCapture || useNativeWindowsCapture) && microphoneEnabled) { + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + const mic = devices.find( + (d) => d.deviceId === microphoneDeviceId && d.kind === "audioinput", + ); + micLabel = mic?.label || undefined; + } catch { + // Fall through - native process will use the default mic. + } + } + + return { + platform, + selectedSource, + useNativeMacScreenCapture, + useNativeWindowsCapture, + micLabel, + }; + }, [ + logNativeCaptureDiagnostics, + microphoneDeviceId, + microphoneEnabled, + preparePermissions, + prepareWebcamRecorder, + resetRecordingClock, + ]); + + const discardActiveNativeCapture = useCallback(async () => { + const pendingPath = pendingNativeCleanupPath.current; + if (pendingPath) { + try { + await window.electronAPI.deleteRecordingFile(pendingPath); + pendingNativeCleanupPath.current = null; + } catch (error) { + console.warn("Failed to delete pending native capture file:", error); + } + } + + if (!nativeScreenRecording.current) { + return pendingNativeCleanupPath.current === null; + } + if (nativeStopRequestInFlight.current) { + return false; + } + + nativeStopRequestInFlight.current = true; + let result: DiscardNativeCaptureResult; + try { + result = await stopAndDiscardNativeCapture({ + stopNativeScreenRecording: () => window.electronAPI.stopNativeScreenRecording(), + deleteRecordingFile: (path) => window.electronAPI.deleteRecordingFile(path), + }); + } finally { + nativeStopRequestInFlight.current = false; + } + + if (result.stopSucceeded) { + nativeScreenRecording.current = false; + nativeWindowsRecording.current = false; + nativeWarmStartActive.current = false; + } + + if (!result.deleteSucceeded && result.path) { + pendingNativeCleanupPath.current = result.path; + } + + if (!result.stopSucceeded || !result.deleteSucceeded) { + console.warn("Failed to fully discard native capture:", result.error); + return false; + } + + return true; + }, []); + const stopRecording = useRef(() => { + recordingStartGeneration.current += 1; setPaused(false); + if (nativeScreenRecording.current && nativeWarmStartActive.current) { + setRecording(false); + void (async () => { + await discardActiveNativeCapture(); + cleanupCapturedMedia(); + await Promise.allSettled([ + stopMicFallbackRecorder(), + stopWebcamRecorder(), + window.electronAPI?.setRecordingState(false), + ]); + })(); + return; + } if (nativeScreenRecording.current) { - nativeScreenRecording.current = false; + if (nativeStopRequestInFlight.current) { + return; + } + nativeStopRequestInFlight.current = true; setRecording(false); setFinalizing(true); @@ -1085,16 +1299,30 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const micFallbackBlobPromise = stopMicFallbackRecorder(); const webcamPathPromise = stopWebcamRecorder(); const isNativeWindows = nativeWindowsRecording.current; - nativeWindowsRecording.current = false; const ipcStopStart = performance.now(); console.log("[PERF:RENDERER] IPC: stopNativeScreenRecording: STARTED"); - const result = await window.electronAPI.stopNativeScreenRecording(); + let result: NativeCaptureStopResult; + try { + result = await window.electronAPI.stopNativeScreenRecording(); + } catch (error) { + result = { success: false, error: getErrorMessage(error) }; + } + nativeStopRequestInFlight.current = false; console.log( `[PERF:RENDERER] IPC: stopNativeScreenRecording: COMPLETED in ${(performance.now() - ipcStopStart).toFixed(2)}ms`, ); + if (result.success) { + nativeScreenRecording.current = false; + nativeWindowsRecording.current = false; + nativeWarmStartActive.current = false; + } - await window.electronAPI?.setRecordingState(false); + try { + await window.electronAPI?.setRecordingState(false); + } catch (stateError) { + console.warn("Failed to reset main-process recording state:", stateError); + } if (!result.success || !result.path) { console.error( @@ -1304,8 +1532,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const removeRecordingInterruptedListener = window.electronAPI?.onRecordingInterrupted?.( (state) => { void (async () => { + recordingStartGeneration.current += 1; setRecording(false); nativeScreenRecording.current = false; + nativeWindowsRecording.current = false; + nativeWarmStartActive.current = false; cleanupCapturedMedia(); await window.electronAPI.setRecordingState(false); @@ -1336,13 +1567,33 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); return () => { + recordingStartGeneration.current += 1; cleanup?.(); removeRecordingStateListener?.(); removeRecordingInterruptedListener?.(); if (nativeScreenRecording.current) { - nativeScreenRecording.current = false; - void window.electronAPI.stopNativeScreenRecording(); + if (nativeWarmStartActive.current) { + void discardActiveNativeCapture(); + } else if (!nativeStopRequestInFlight.current) { + nativeStopRequestInFlight.current = true; + void window.electronAPI + .stopNativeScreenRecording() + .then((result) => { + if (result.success) { + nativeScreenRecording.current = false; + nativeWindowsRecording.current = false; + } + }) + .catch((error) => { + console.warn("Failed to stop native capture during cleanup:", error); + }) + .finally(() => { + nativeStopRequestInFlight.current = false; + }); + } + } else if (pendingNativeCleanupPath.current) { + void discardActiveNativeCapture(); } const recorder = mediaRecorder.current; @@ -1353,12 +1604,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn { cleanupCapturedMedia(); }; - }, [cleanupCapturedMedia, recoverNativeRecordingSession]); + }, [cleanupCapturedMedia, discardActiveNativeCapture, recoverNativeRecordingSession]); const startRecording = async () => { if (startInFlight.current) { return; } + const startGeneration = recordingStartGeneration.current + 1; + recordingStartGeneration.current = startGeneration; + const startWasCancelled = () => recordingStartGeneration.current !== startGeneration; let hudSourceSelectionActive = false; const setHudSourceSelectionActive = (active: boolean) => { @@ -1375,84 +1629,36 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setStarting(true); try { - const platform = await window.electronAPI.getPlatform(); - hideEditorOverlayCursorByDefault.current = false; - const existingSource = await window.electronAPI.getSelectedSource(); - const selectedSource = - existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); - if (!selectedSource) { - alert("Please select a source to record"); - return; - } - // Persist the synthetic Linux portal sentinel to main so that the - // setDisplayMediaRequestHandler can short-circuit getSources() and - // avoid triggering an extra portal dialog. - if (!existingSource && selectedSource.id === "screen:linux-portal") { - try { - await window.electronAPI.selectSource(selectedSource); - } catch (err) { - console.warn("Failed to persist Linux portal sentinel source:", err); - } - } - - const permissionsReady = await preparePermissions(); - if (!permissionsReady) { + const preparedStart = await prepareRecordingStart(); + if (!preparedStart || startWasCancelled()) { + cleanupCapturedMedia(); + await stopWebcamRecorder(); return; } - recordingSessionTimestamp.current = Date.now(); - resetRecordingClock(recordingSessionTimestamp.current); - await prepareWebcamRecorder(); - const useNativeMacScreenCapture = - platform === "darwin" && - (selectedSource.id?.startsWith("screen:") || - selectedSource.id?.startsWith("window:")) && - typeof window.electronAPI.startNativeScreenRecording === "function"; - - let useNativeWindowsCapture = false; - let nativeWindowsCaptureStartFailed = false; - if ( - platform === "win32" && - shouldUseNativeWindowsCaptureForSource(selectedSource) && - typeof window.electronAPI.isNativeWindowsCaptureAvailable === "function" - ) { + const { selectedSource, useNativeMacScreenCapture, useNativeWindowsCapture, micLabel } = + preparedStart; + const useNativeCapture = useNativeMacScreenCapture || useNativeWindowsCapture; + const shouldWarmStartNativeCapture = useNativeCapture && countdownDelay > 0; + if (countdownDelay > 0 && !shouldWarmStartNativeCapture) { + setCountdownActive(true); try { - const nativeWindowsResult = - await window.electronAPI.isNativeWindowsCaptureAvailable(); - useNativeWindowsCapture = nativeWindowsResult.available; - if (!useNativeWindowsCapture && !hasShownNativeWindowsFallbackToast.current) { - void logNativeCaptureDiagnostics("is-native-windows-capture-available"); - hasShownNativeWindowsFallbackToast.current = true; - toast.info( - "Native Windows capture is unavailable. Falling back to browser capture.", - ); - } - } catch { - useNativeWindowsCapture = false; - if (!hasShownNativeWindowsFallbackToast.current) { - hasShownNativeWindowsFallbackToast.current = true; - toast.info( - "Unable to check native Windows capture. Falling back to browser capture.", - ); + const result = await window.electronAPI.startCountdown(countdownDelay); + if (!result.success || result.cancelled || startWasCancelled()) { + cleanupCapturedMedia(); + await stopWebcamRecorder(); + return; } + } finally { + setCountdownActive(false); } + recordingSessionTimestamp.current = Date.now(); + resetRecordingClock(recordingSessionTimestamp.current); } - if (useNativeMacScreenCapture || useNativeWindowsCapture) { - // Resolve the selected mic label for native capture backends. - let micLabel: string | undefined; - if (microphoneEnabled) { - try { - const devices = await navigator.mediaDevices.enumerateDevices(); - const mic = devices.find( - (d) => d.deviceId === microphoneDeviceId && d.kind === "audioinput", - ); - micLabel = mic?.label || undefined; - } catch { - // Fall through — native process will use the default mic - } - } + let nativeWindowsCaptureStartFailed = false; + if (useNativeCapture) { const nativeResult = await window.electronAPI.startNativeScreenRecording( selectedSource, { @@ -1462,6 +1668,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn { microphoneLabel: micLabel, }, ); + if (nativeResult.success && startWasCancelled()) { + nativeScreenRecording.current = true; + nativeWindowsRecording.current = useNativeWindowsCapture; + nativeWarmStartActive.current = shouldWarmStartNativeCapture; + await discardActiveNativeCapture(); + cleanupCapturedMedia(); + await stopWebcamRecorder(); + return; + } if (!nativeResult.success) { if (useNativeWindowsCapture) { nativeWindowsCaptureStartFailed = true; @@ -1491,11 +1706,62 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } if (nativeResult.success) { + nativeScreenRecording.current = true; + nativeWindowsRecording.current = useNativeWindowsCapture; + if (shouldWarmStartNativeCapture) { + nativeWarmStartActive.current = true; + const pauseResult = await window.electronAPI.pauseNativeScreenRecording(); + if (startWasCancelled()) { + return; + } + if (!pauseResult.success) { + throw new Error( + pauseResult.error ?? + pauseResult.message ?? + "Failed to pause native capture before countdown", + ); + } + + setCountdownActive(true); + try { + const countdownResult = + await window.electronAPI.startCountdown(countdownDelay); + if ( + !countdownResult.success || + countdownResult.cancelled || + startWasCancelled() + ) { + if (!startWasCancelled()) { + await discardActiveNativeCapture(); + } + cleanupCapturedMedia(); + await stopWebcamRecorder(); + return; + } + } finally { + setCountdownActive(false); + } + + const resumeResult = await window.electronAPI.resumeNativeScreenRecording(); + if (startWasCancelled()) { + return; + } + if (!resumeResult.success) { + throw new Error( + resumeResult.error ?? + resumeResult.message ?? + "Failed to resume native capture after countdown", + ); + } + nativeWarmStartActive.current = false; + } + if (startWasCancelled()) { + return; + } + const mainStartedAt = Date.now(); micFallbackStartDelayMs.current = null; beginWebcamCapture(); - nativeScreenRecording.current = true; - nativeWindowsRecording.current = useNativeWindowsCapture; resetRecordingClock(mainStartedAt); webcamTimeOffsetMs.current = webcamStartTime.current === null @@ -1566,6 +1832,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); } } + if (startWasCancelled()) { + await stopMicFallbackRecorder(); + await stopWebcamRecorder(); + cleanupCapturedMedia(); + return; + } setRecording(true); try { @@ -1581,6 +1853,22 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } + if (nativeWindowsCaptureStartFailed && countdownDelay > 0) { + setCountdownActive(true); + try { + const result = await window.electronAPI.startCountdown(countdownDelay); + if (!result.success || result.cancelled) { + cleanupCapturedMedia(); + await stopWebcamRecorder(); + return; + } + } finally { + setCountdownActive(false); + } + recordingSessionTimestamp.current = Date.now(); + resetRecordingClock(recordingSessionTimestamp.current); + } + const browserCursorPolicy = resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed, }); @@ -1921,6 +2209,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { : "Failed to start recording", ); setRecording(false); + if (nativeScreenRecording.current) { + await discardActiveNativeCapture(); + } try { await window.electronAPI?.setRecordingState(false); } catch (stateError) { @@ -2029,6 +2320,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }, [markRecordingResumed, paused, recording, resumeMicFallbackRecorder]); const cancelRecording = useCallback(() => { + recordingStartGeneration.current += 1; if (!recording) return; setPaused(false); markRecordingResumed(Date.now()); @@ -2047,19 +2339,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { resolvedWebcamPath.current = null; if (nativeScreenRecording.current) { - nativeScreenRecording.current = false; - nativeWindowsRecording.current = false; setRecording(false); window.electronAPI?.setRecordingState(false); void (async () => { - try { - const result = await window.electronAPI.stopNativeScreenRecording(); - if (result?.path) { - await window.electronAPI.deleteRecordingFile(result.path); - } - } catch { - // Best-effort cleanup - } + await discardActiveNativeCapture(); })(); return; } @@ -2073,7 +2356,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { setRecording(false); window.electronAPI?.setRecordingState(false); } - }, [cleanupCapturedMedia, markRecordingResumed, recording]); + }, [cleanupCapturedMedia, discardActiveNativeCapture, markRecordingResumed, recording]); const toggleRecording = async () => { if (starting || countdownActive || finalizing) { @@ -2085,19 +2368,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } - // Start recording with optional countdown - if (countdownDelay > 0) { - setCountdownActive(true); - try { - const result = await window.electronAPI.startCountdown(countdownDelay); - if (!result.success || result.cancelled) { - return; - } - } finally { - setCountdownActive(false); - } - } - startRecording(); };