fix: address ipc review follow-ups

This commit is contained in:
webadderall
2026-04-17 21:11:58 +10:00
parent 87ec8fca93
commit 0b3169ffff
13 changed files with 131 additions and 75 deletions
+25 -20
View File
@@ -29,29 +29,33 @@ export async function getNativeMacWindowSources(options?: { maxAgeMs?: number })
return cachedNativeMacWindowSources;
}
const binaryPath = await ensureNativeWindowListBinary();
const { stdout } = await execFileAsync(binaryPath, [], {
timeout: 30000,
maxBuffer: 10 * 1024 * 1024,
});
try {
const binaryPath = await ensureNativeWindowListBinary();
const { stdout } = await execFileAsync(binaryPath, [], {
timeout: 30000,
maxBuffer: 10 * 1024 * 1024,
});
const parsed = JSON.parse(stdout);
if (!Array.isArray(parsed)) {
return [] as NativeMacWindowSource[];
}
const entries = parsed.filter((entry: unknown): entry is NativeMacWindowSource => {
if (!entry || typeof entry !== "object") {
return false;
const parsed = JSON.parse(stdout);
if (!Array.isArray(parsed)) {
return [] as NativeMacWindowSource[];
}
const candidate = entry as Partial<NativeMacWindowSource>;
return typeof candidate.id === "string" && typeof candidate.name === "string";
});
const entries = parsed.filter((entry: unknown): entry is NativeMacWindowSource => {
if (!entry || typeof entry !== "object") {
return false;
}
setCachedNativeMacWindowSources(entries);
setCachedNativeMacWindowSourcesAtMs(now);
return entries;
const candidate = entry as Partial<NativeMacWindowSource>;
return typeof candidate.id === "string" && typeof candidate.name === "string";
});
setCachedNativeMacWindowSources(entries);
setCachedNativeMacWindowSourcesAtMs(now);
return entries;
} catch {
return cachedNativeMacWindowSources ?? ([] as NativeMacWindowSource[]);
}
}
export function getWindowBoundsFromNativeSource(
@@ -180,8 +184,9 @@ export async function resolveWindowsWindowBounds(source: SelectedSource): Promis
"if ($windowId) {",
" $handle = [Int64]$windowId",
"}",
"$escapedWindowTitle = if ($windowTitle) { [WildcardPattern]::Escape($windowTitle) } else { $null }",
"if ($handle -le 0 -and $windowTitle) {",
' $matchingProcess = Get-Process | Where-Object { $_.MainWindowTitle -eq $windowTitle -or $_.MainWindowTitle -like "*$windowTitle*" } | Select-Object -First 1',
' $matchingProcess = Get-Process | Where-Object { $_.MainWindowTitle -eq $windowTitle -or ($escapedWindowTitle -and $_.MainWindowTitle -like "*$escapedWindowTitle*") } | Select-Object -First 1',
" if ($matchingProcess) {",
" $handle = $matchingProcess.MainWindowHandle.ToInt64()",
" }",
+2 -2
View File
@@ -187,8 +187,6 @@ export async function startInteractionCapture() {
hook.on("mousemove", onMouseMove);
}
hook.start();
setInteractionCaptureCleanup(() => {
try {
if (typeof hook.off === "function") {
@@ -216,6 +214,8 @@ export async function startInteractionCapture() {
// ignore hook shutdown errors
}
});
hook.start();
} catch (error) {
if (!hasLoggedInteractionHookFailure) {
setHasLoggedInteractionHookFailure(true);
+24 -9
View File
@@ -112,20 +112,35 @@ export async function startNativeCursorMonitor() {
}
setNativeCursorMonitorProcess(proc as Parameters<typeof setNativeCursorMonitorProcess>[0]);
proc.once("error", (error) => {
console.warn("Native cursor monitor process error:", error);
const spawned = proc;
if (!spawned) {
setNativeCursorMonitorProcess(null);
setNativeCursorMonitorOutputBuffer("");
setCurrentCursorVisualType("arrow");
return;
}
spawned.once("error", (error) => {
console.warn("Native cursor monitor process error:", error);
if (nativeCursorMonitorProcess === spawned) {
setNativeCursorMonitorProcess(null);
setNativeCursorMonitorOutputBuffer("");
setCurrentCursorVisualType("arrow");
}
});
if (proc.stdout) proc.stdout.on("data", handleCursorMonitorStdout);
if (spawned.stdout) spawned.stdout.on("data", handleCursorMonitorStdout);
if (spawned.stderr) {
spawned.stderr.on("data", () => {
// Drain stderr so helper logging cannot block the process.
});
}
proc.once("close", () => {
setNativeCursorMonitorProcess(null);
setNativeCursorMonitorOutputBuffer("");
setCurrentCursorVisualType("arrow");
spawned.once("close", () => {
if (nativeCursorMonitorProcess === spawned) {
setNativeCursorMonitorProcess(null);
setNativeCursorMonitorOutputBuffer("");
setCurrentCursorVisualType("arrow");
}
});
} catch (error) {
console.warn("Failed to start native cursor monitor:", error);
-4
View File
@@ -117,10 +117,6 @@ export function pushCursorSample(
export function sampleCursorPoint() {
const point = getNormalizedCursorPoint();
if (!point) {
return;
}
pushCursorSample(point.cx, point.cy, Date.now() - cursorCaptureStartTimeMs, "move");
}
+5 -3
View File
@@ -326,7 +326,10 @@ export async function resolveNativeVideoEncoder(
ffmpegPath: string,
encodingMode: NativeExportEncodingMode,
) {
if (cachedNativeVideoEncoder?.ffmpegPath === ffmpegPath) {
if (
cachedNativeVideoEncoder?.ffmpegPath === ffmpegPath &&
cachedNativeVideoEncoder?.encodingMode === encodingMode
) {
return cachedNativeVideoEncoder.encoderName;
}
@@ -341,7 +344,7 @@ export async function resolveNativeVideoEncoder(
}
if (await probeNativeVideoEncoder(ffmpegPath, encoderName, encodingMode)) {
setCachedNativeVideoEncoder({ ffmpegPath, encoderName });
setCachedNativeVideoEncoder({ ffmpegPath, encodingMode, encoderName });
return encoderName;
}
}
@@ -451,7 +454,6 @@ export async function muxExportedVideoAudioBuffer(
} finally {
await Promise.allSettled([
removeTemporaryExportFile(tempVideoPath),
removeTemporaryExportFile(`${tempVideoPath}.muxed.mp4`),
removeTemporaryExportFile(
path.join(
path.dirname(tempVideoPath),
+8 -7
View File
@@ -51,11 +51,12 @@ export function isPathInsideDirectory(candidatePath: string, directoryPath: stri
export function isAllowedLocalReadPath(candidatePath: string) {
const allowedPrefixes = [RECORDINGS_DIR, USER_DATA_PATH, getAssetRootPath(), app.getPath("temp")];
const normalizedCandidatePath = normalizePath(candidatePath);
return (
existsSync(candidatePath) ||
allowedPrefixes.some((prefix) => isPathInsideDirectory(candidatePath, prefix)) ||
approvedLocalReadPaths.has(candidatePath)
existsSync(normalizedCandidatePath) ||
allowedPrefixes.some((prefix) => isPathInsideDirectory(normalizedCandidatePath, prefix)) ||
approvedLocalReadPaths.has(normalizedCandidatePath)
);
}
@@ -308,9 +309,6 @@ export async function loadProjectFromPath(projectPath: string) {
message: mediaSources.message,
};
}
setCurrentProjectPath(normalizedPath);
setCurrentVideoPath(mediaSources.videoPath);
const projectObj = project as Record<string, unknown>;
const editorObj = projectObj?.editor as Record<string, unknown> | undefined;
const audioTracks = editorObj?.audioTracks as { sourcePath?: unknown }[] | undefined;
@@ -326,12 +324,15 @@ export async function loadProjectFromPath(projectPath: string) {
}
}
await replaceApprovedSessionLocalReadPaths(approvedProjectPaths);
await rememberRecentProject(normalizedPath);
setCurrentProjectPath(normalizedPath);
setCurrentVideoPath(mediaSources.videoPath);
setCurrentRecordingSession({
videoPath: mediaSources.videoPath,
webcamPath: mediaSources.webcamPath,
timeOffsetMs: 0,
} as RecordingSessionData);
await rememberRecentProject(normalizedPath);
return {
success: true,
+5 -4
View File
@@ -4,7 +4,7 @@ import type { SelectedSource } from "../types";
import {
ffmpegCaptureOutputBuffer,
} from "../state";
import { getScreen } from "../utils";
import { getScreen, parseWindowId } from "../utils";
import { resolveWindowsCaptureDisplay } from "../windowsCaptureSelection";
import { resolveLinuxWindowBounds } from "../cursor/bounds";
@@ -32,12 +32,13 @@ export async function buildFfmpegCaptureArgs(source: SelectedSource, outputPath:
if (process.platform === "win32") {
if (source?.id?.startsWith("window:")) {
const windowId = parseWindowId(source.id);
const windowTitle =
typeof source.windowTitle === "string"
? source.windowTitle.trim()
: source.name.trim();
if (!windowTitle) {
throw new Error("Missing window title for FFmpeg window capture");
if (!windowId && !windowTitle) {
throw new Error("Missing window identifier for FFmpeg window capture");
}
return [
@@ -49,7 +50,7 @@ export async function buildFfmpegCaptureArgs(source: SelectedSource, outputPath:
"-draw_mouse",
"0",
"-i",
`title=${windowTitle}`,
windowId ? `hwnd=${windowId}` : `title=${windowTitle}`,
...commonOutputArgs,
];
}
+3 -2
View File
@@ -52,9 +52,10 @@ export function waitForNativeCaptureStart(process: ChildProcessWithoutNullStream
reject(new Error("Timed out waiting for ScreenCaptureKit recorder to start"));
}, 12000);
let stdoutBuffer = "";
const onStdout = (chunk: Buffer) => {
const text = chunk.toString();
if (text.includes("Recording started")) {
stdoutBuffer += chunk.toString();
if (stdoutBuffer.includes("Recording started")) {
cleanup();
resolve();
}
+3 -2
View File
@@ -53,9 +53,10 @@ export function waitForWindowsCaptureStart(proc: ChildProcessWithoutNullStreams)
reject(new Error("Timed out waiting for native Windows capture to start"));
}, 12000);
let stdoutBuffer = "";
const onStdout = (chunk: Buffer) => {
const text = chunk.toString();
if (text.includes("Recording started")) {
stdoutBuffer += chunk.toString();
if (stdoutBuffer.includes("Recording started")) {
cleanup();
resolve();
}
+10 -5
View File
@@ -16,7 +16,7 @@ import {
setCurrentRecordingSession,
} from "../state";
import { normalizeVideoSourcePath } from "../utils";
import { replaceApprovedSessionLocalReadPaths } from "../project/manager";
import { isPathInsideDirectory, replaceApprovedSessionLocalReadPaths } from "../project/manager";
import {
getTelemetryPathForVideo,
isAutoRecordingPath,
@@ -351,14 +351,19 @@ export function registerProjectHandlers() {
ipcMain.handle('delete-recording-file', async (_, filePath: string) => {
try {
if (!filePath || !isAutoRecordingPath(filePath)) {
if (!filePath) {
return { success: false, error: 'Only auto-generated recordings can be deleted' };
}
await fs.unlink(filePath);
const resolvedPath = await fs.realpath(filePath).catch(() => path.resolve(filePath));
const recordingsDir = await getRecordingsDir();
if (!isPathInsideDirectory(resolvedPath, recordingsDir) || !isAutoRecordingPath(resolvedPath)) {
return { success: false, error: 'Only auto-generated recordings can be deleted' };
}
await fs.unlink(resolvedPath);
// Also delete the cursor telemetry sidecar if it exists
const telemetryPath = getTelemetryPathForVideo(filePath);
const telemetryPath = getTelemetryPathForVideo(resolvedPath);
await fs.unlink(telemetryPath).catch(() => {});
if (currentVideoPath === filePath) {
if (currentVideoPath === resolvedPath) {
setCurrentVideoPath(null);
setCurrentRecordingSession(null);
}
+17 -9
View File
@@ -1040,17 +1040,25 @@ export function registerRecordingHandlers(
ipcMain.handle('get-recorded-video-path', async () => {
try {
const recordingsDir = await getRecordingsDir()
const files = await fs.readdir(recordingsDir)
const videoFiles = files.filter(file => /\.(webm|mov|mp4)$/i.test(file))
if (videoFiles.length === 0) {
const entries = await fs.readdir(recordingsDir, { withFileTypes: true })
const candidates = await Promise.all(
entries
.filter((entry) => entry.isFile() && /^recording-\d+\.(webm|mov|mp4)$/i.test(entry.name))
.map(async (entry) => {
const fullPath = path.join(recordingsDir, entry.name)
const stat = await fs.stat(fullPath).catch(() => null)
return stat ? { path: fullPath, mtimeMs: stat.mtimeMs } : null
}),
)
const latestVideo = candidates
.filter((candidate): candidate is { path: string; mtimeMs: number } => candidate !== null)
.sort((left, right) => right.mtimeMs - left.mtimeMs)[0]
if (!latestVideo) {
return { success: false, message: 'No recorded video found' }
}
const latestVideo = videoFiles.sort().reverse()[0]
const videoPath = path.join(recordingsDir, latestVideo)
return { success: true, path: videoPath }
return { success: true, path: latestVideo.path }
} catch (error) {
console.error('Failed to get video path:', error)
return { success: false, message: 'Failed to get video path', error: String(error) }
+27 -6
View File
@@ -283,12 +283,23 @@ export function registerSourceHandlers({
// ── 1. Bring window to front ──
if (isWindow && process.platform === "darwin") {
const appName = source.appName || source.name?.split(" — ")[0]?.trim();
const rawAppName = source.appName || source.name?.split(" — ")[0]?.trim();
const appName =
rawAppName && /^[\w .&()+'-]{1,64}$/.test(rawAppName) ? rawAppName : null;
if (appName) {
try {
await execFileAsync(
"osascript",
["-e", `tell application "${appName}" to activate`],
[
"-e",
"on run argv",
"-e",
"tell application (item 1 of argv) to activate",
"-e",
"end run",
"--",
appName,
],
{ timeout: 2000 },
);
await new Promise((resolve) => setTimeout(resolve, 350));
@@ -332,13 +343,23 @@ export function registerSourceHandlers({
bounds = getDisplayBoundsForSource(source);
}
if (!bounds || bounds.width <= 0 || bounds.height <= 0) {
const primaryBounds = getScreen().getPrimaryDisplay().bounds;
if (primaryBounds.width <= 0 || primaryBounds.height <= 0) {
return { success: false };
}
bounds = primaryBounds;
}
const resolvedBounds = bounds;
// ── 3. Show traveling wave highlight ──
const pad = 6;
const highlightWin = new BrowserWindow({
x: bounds.x - pad,
y: bounds.y - pad,
width: bounds.width + pad * 2,
height: bounds.height + pad * 2,
x: resolvedBounds.x - pad,
y: resolvedBounds.y - pad,
width: resolvedBounds.width + pad * 2,
height: resolvedBounds.height + pad * 2,
frame: false,
transparent: true,
alwaysOnTop: true,
+2 -2
View File
@@ -90,7 +90,7 @@ export let cachedNativeMacWindowSources: import("./types").NativeMacWindowSource
export let cachedNativeMacWindowSourcesAtMs = 0;
// ── Native video export ───────────────────────────────────────────────────────
export let cachedNativeVideoEncoder: { ffmpegPath: string; encoderName: string } | null = null;
export let cachedNativeVideoEncoder: { ffmpegPath: string; encodingMode: string; encoderName: string } | null = null;
// ── Native helper migration ───────────────────────────────────────────────────
export let nativeHelperMigrationPromise: Promise<void> | null = null;
@@ -164,6 +164,6 @@ export function setWindowBoundsCaptureInterval(v: NodeJS.Timeout | null) { windo
export function setCachedNativeMacWindowSources(v: import("./types").NativeMacWindowSource[] | null) { cachedNativeMacWindowSources = v; }
export function setCachedNativeMacWindowSourcesAtMs(v: number) { cachedNativeMacWindowSourcesAtMs = v; }
export function setCachedNativeVideoEncoder(v: { ffmpegPath: string; encoderName: string } | null) { cachedNativeVideoEncoder = v; }
export function setCachedNativeVideoEncoder(v: { ffmpegPath: string; encodingMode: string; encoderName: string } | null) { cachedNativeVideoEncoder = v; }
export function setNativeHelperMigrationPromise(v: Promise<void> | null) { nativeHelperMigrationPromise = v; }