Skip to content

feat: add Windows native capture and cursor pipeline#217

Merged
siddharthvaddem merged 44 commits into
siddharthvaddem:mainfrom
EtienneLescot:feat/cursor-pipeline
May 10, 2026
Merged

feat: add Windows native capture and cursor pipeline#217
siddharthvaddem merged 44 commits into
siddharthvaddem:mainfrom
EtienneLescot:feat/cursor-pipeline

Conversation

@EtienneLescot
Copy link
Copy Markdown
Collaborator

@EtienneLescot EtienneLescot commented Mar 15, 2026

Description

This PR adds a Windows-only native recording path for OpenScreen. The goal is to avoid the preview/export cursor drift caused by splitting capture, cursor telemetry, audio, webcam, and muxing across Electron/browser APIs.

On Windows, recordings are routed through a bundled native Windows Graphics Capture helper. macOS and Linux behavior is intentionally unchanged in this PR.

What changed

  • Adds a redistributable Windows WGC helper at electron/native/bin/win32-x64/wgc-capture.exe.
  • Captures full display and app windows through Windows Graphics Capture.
  • Captures native Windows cursor telemetry and reconstructs a high-quality scalable cursor overlay in preview/export.
  • Adds Windows cursor settings only when the active platform is Windows.
  • Captures system audio through WASAPI loopback.
  • Captures microphone audio through WASAPI and mixes mic + system audio into the MP4.
  • Captures webcam natively through Media Foundation, with DirectShow fallback for virtual cameras such as NVIDIA Broadcast.
  • Composes webcam picture-in-picture in the helper-owned MP4 during the Windows-native migration.
  • Packages the native helper with Windows builds through asarUnpack.
  • Adds/updates fast Windows smoke-test scripts for native cursor, WGC display/window capture, audio, mic, mixed audio, and webcam.
  • Adds a focused Playwright Electron smoke test for the maintainer launch/editor checklist paths.
  • Documents the Windows native recorder architecture and testing pipeline.

Windows-only scope

This PR does not add native capture for macOS or Linux. Windows-only cursor/capture controls are hidden outside Windows where applicable.

Linux remains on the existing Electron capture path. macOS behavior is not changed by this PR.

Validation run

Passed locally on Windows:

  • npm run build-vite
  • npm run build:win
  • npm run test:e2e:windows-native-checklist
  • npm run test:cursor-native:win
  • npm run test:wgc-helper:win
  • npm run test:wgc-window:win
  • npm run test:wgc-audio:win
  • npm run test:wgc-mic:win
  • npm run test:wgc-mixed-audio:win
  • npm run test:wgc-webcam:win
  • OPENSCREEN_WGC_TEST_WEBCAM_DEVICE_NAME="NVIDIA Broadcast" npm run test:wgc-webcam:win
  • OPENSCREEN_WGC_TEST_WEBCAM_DEVICE_NAME="NVIDIA Broadcast" npm run test:wgc-full:win
  • npx vitest --run src/lib/cursorTelemetryBuffer.test.ts

Known repo-level checks still needing separate cleanup:

  • npm run lint currently reports broad existing Biome formatting/line-ending issues outside the native-capture work.
  • npm test has existing failures in blur effects, tutorial translations, and browser exporter fixture/canvas handling.
  • npm run test:e2e currently times out in tests/e2e/gif-export.spec.ts.

Maintainer test checklist

Capture + Launch

  • pre commit check
  • Source selector opens and lists screens/windows with thumbnails
  • Selecting a source works for full screen and app window
  • Record start/stop works from launch window
  • Tray “Stop Recording” works while recording
  • Opening existing video file works from launch window
  • Opening existing project works from launch window

Audio

  • Mic toggle on/off works before recording
  • Mic device selection works
  • System audio toggle works across recordings
  • Mic-only recording works
  • System-audio-only recording works
  • Mic + system audio mix works and levels are balanced

Editor Load + Playback

  • Playback, pause, seek works.
  • Cursor telemetry overlays correctly

Timeline

  • Add/edit/remove different elements (zoom/ annotation/ trim/ speed), see how they are in playback and if they are 1:1 with exported video
  • Region drag/resize snaps and persists correctly
  • No overlap/ordering bugs on timeline items (apart from annotations)

Project Persistence

  • save/ load works

Other

  • Tweaking sliders/ wallpapers/ other effects.

Notes for reviewers

The checked items above are the paths covered by local Windows automation and manual iteration on this branch. The unchecked items are still useful manual QA targets before considering the PR ready to merge.

Summary by CodeRabbit

  • New Features

    • Windows native recording (WGC) with system audio, microphone and webcam; integrated native cursor capture, preview and export; native bridge APIs for project/video flows and cursor telemetry.
    • Cursor rendering controls (visibility, size, smoothing, motion blur, click bounce) with preview and export support.
  • Documentation

    • Native bridge architecture, Windows recorder roadmap, native helper build/run docs and Windows cursor test workflow; README developer notes link added.
  • Tests

    • Windows-native smoke scripts and Playwright E2E checklist.
  • Chores

    • Expanded .gitignore, packaging unpack rules, Windows native build/test scripts, and audio export switched to AAC (MP4).

@EtienneLescot EtienneLescot changed the title feat: add cursor overlay pipeline feat: add cursor overlay pipeline (depends on #207) Mar 15, 2026
@siddharthvaddem
Copy link
Copy Markdown
Owner

siddharthvaddem commented Mar 17, 2026

Just wanted to thank you for working on this and helping contribute to the project 🙏

I know this is a draft, but wanted to reiterate that I was never against Native Capture. We can have the cursor related settings hidden for Linux and Windows. If that helps. Let me know if there's any further clarification needed. Sorry if this feels like going back and forth

Had a couple of folks on X reach out and say they really wanted this for macOS.

No rush on this at all. Don't feel obliged.

This is a pretty large change which would require a lot of testing to avoid bugs. This is what usually helps me (might have missed something):

Capture + Launch

  • pre commit check
  • Source selector opens and lists screens/windows with thumbnails
  • Selecting a source works for full screen and app window
  • Record start/stop works from launch window
  • Tray “Stop Recording” works while recording
  • Opening existing video file works from launch window
  • Opening existing project works from launch window

Audio

  • Mic toggle on/off works before recording
  • Mic device selection works
  • System audio (toggle these across 2 recordings to see if both paths work
  • Mic-only recording works
  • System-audio-only recording works
  • Mic + system audio mix works and levels are balanced

Editor Load + Playback

  • Playback, pause, seek works.
  • Cursor telemetry overlays correctly

Timeline

  • Add/edit/remove different elements (zoom/ annotation/ trim/ speed), see how they are in playback and if they are 1:1 with exported video
  • Region drag/resize snaps and persists correctly
  • No overlap/ordering bugs on timeline items (apart from annotations)

Project Persistence

  • save/ load works

Other

  • Tweaking sliders/ wallpapers/ other effects.

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

EtienneLescot commented Mar 23, 2026

@siddharthvaddem
Thank you for the testing procedure.
I lack a little bit of time this week but I will work on this as soon as possible.

@siddharthvaddem
Copy link
Copy Markdown
Owner

@siddharthvaddem Thank you for the testing procedure. I like a little bit of time this week that I will work on this as soon as possible.

take your time 🙏 no rush. want to make sure it is implemented correctly even if it takes time.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a typed native bridge and Windows-native recording pipeline: bridge contracts, state store, services, IPC/preload wiring, renderer client and hooks, cursor recording sessions/adapters, a native wgc-capture helper (C++), audio/video/webcam capture + encoder, exporter and cursor-rendering integration, build/test scripts, and documentation.

Changes

Native bridge + Windows-native recording (single DAG)

Layer / File(s) Summary
Data Shape / Contracts
src/native/contracts.ts, src/lib/nativeWindowsRecording.ts, electron/electron-env.d.ts
Adds bridge constants/version, typed request/response envelopes, cursor telemetry/recording types, project/system result shapes, Windows recording request/result types, and extends preload typings for new electronAPI methods.
Runtime State
electron/native-bridge/store.ts
New in-memory NativeBridgeState and NativeBridgeStateStore with setters for system/project/cursor state and telemetry load metadata.
Service Layer
electron/native-bridge/services/*.ts
Adds SystemService, ProjectService, CursorService to wrap adapters/state and expose platform/project/video/cursor operations.
Adapter / Cursor Session Interfaces
electron/native-bridge/cursor/adapter.ts, .../recording/session.ts
Declare CursorNativeAdapter, CursorTelemetryLoadResult, and CursorRecordingSession interfaces.
Cursor Session Implementations
electron/native-bridge/cursor/recording/*.ts
Add TelemetryRecordingSession (timed telemetry) and WindowsNativeRecordingSession (PowerShell helper spawn, NDJSON parsing), types, and PowerShell script builder.
Native Bridge IPC Handler
electron/ipc/nativeBridge.ts, electron/ipc/handlers.ts
Register native-bridge:invoke, validate requests, create NativeBridgeStateStore, wire services; update IPC handlers to coordinate cursor sessions, native Windows capture start/stop, project/video path management, file approvals, and persisted session manifests.
Preload / Renderer Client / Hooks
electron/preload.ts, src/native/client.ts, src/native/hooks/*, src/native/index.ts
Expose invokeNativeBridge and native recording APIs in preload; add nativeBridgeClient wrapper and helpers; add hooks useCursorTelemetry and useCursorRecordingData.
Windows Native Helper (C++ build)
electron/native/wgc-capture/CMakeLists.txt, electron/native/wgc-capture/src/*
Adds wgc-capture executable: WGC session, MF encoder (H.264 + AAC), WASAPI loopback & mic capture, audio mixer, webcam capture (MF + DirectShow), monitor utils, main orchestration, and NDJSON/stdin contract.
Audio/Encoder Utilities
electron/native/wgc-capture/src/audio_sample_utils.*, mf_encoder.*
New audio conversion/mixing utilities and AudioMixer worker; Media Foundation encoder with staging-texture readback and audio stream handling.
Build & Packaging
scripts/build-windows-wgc-helper.mjs, package.json, electron-builder.json5, .gitignore
Windows build script using vcvarsall/CMake/Ninja, build:native:win script, add electron/native/bin/** to asarUnpack, and ignore native build outputs in .gitignore.
Developer Tools & Tests
scripts/test-windows-native-cursor.mjs, scripts/test-windows-wgc-helper.mjs, scripts/capture-openscreen-preview.mjs, tests/e2e/windows-native-checklist.spec.ts
Add Windows diagnostics and smoke tests: PowerShell cursor sampler, helper smoke tests (ffprobe/ffmpeg checks), OpenScreen preview automation, and Playwright E2E checklist.
Renderer integration: UI, playback, exporters
src/hooks/useScreenRecorder.ts, src/native/*, src/components/video-editor/*, src/lib/exporter/*, src/lib/cursor/nativeCursor.ts
Wire native-recording flow into recording hook; add native cursor utilities, smoothing, asset preload, Pixi overlay + DOM preview, and propagate cursorRecordingData and tuning through FrameRenderer, VideoExporter, and GifExporter.
UI tweaks & testids
src/components/launch/*, src/components/video-editor/SettingsPanel.tsx, src/components/launch/SourceSelector.tsx, src/components/video-editor/timeline/TimelineEditor.tsx
Add cursor settings UI, crop dropdown behavior, pointer-down counters, webcam/mic device name sync, data-testid attributes, and hasVideoSource prop for timeline empty state.
Docs / READMEs / Roadmap
docs/*, electron/native/README.md, README.md
Add native bridge architecture doc, Windows native recorder roadmap, Windows cursor testing guide, native helper README, and README developer note link.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem
  • FabLrc

"a midnight patch for native capture"
powershell hums and the helper spits ndjson,
dshow and mf argue in c++ while audio threads mix,
pixi smooths ghost cursors, exporters bake h264+aac,
nit: build paths are kinda cursed but lowkey clever,
ship when tests stop complaining and windows stops surprising.

@siddharthvaddem
Copy link
Copy Markdown
Owner

@EtienneLescot please close the PR if there are no plans on continuing work here. Appreciate all the work and help put in 🙏

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@EtienneLescot please close the PR if there are no plans on continuing work here. Appreciate all the work and help put in 🙏

@siddharthvaddem my goodness, time goes by so quickly!
Let me see if I can find time on this, tuesday or wednesday.
If not I will close it to reopen it when possible ;-)

@EtienneLescot EtienneLescot force-pushed the feat/cursor-pipeline branch 2 times, most recently from 00b6e83 to e48dd9c Compare May 5, 2026 08:41
@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem I am working on it today ;-)

@EtienneLescot EtienneLescot changed the title feat: add cursor overlay pipeline (depends on #207) feat: add Windows native capture and cursor pipeline May 5, 2026
@EtienneLescot EtienneLescot marked this pull request as ready for review May 5, 2026 17:42
@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem I am still making some e2e tests but you can start reviewing if you want.
Windows only for the moment. I will make another PR for MacOSX. I'll check Linux, but this might not be supported.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcb402f7e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread electron/ipc/handlers.ts Outdated
Comment thread src/components/video-editor/projectPersistence.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/components/video-editor/projectPersistence.ts (1)

121-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

encodeURI on the whole path breaks legit file URL round-trips.

Filenames with # or ? stay unescaped by encodeURI, so when new URL() parses them later, it treats those chars as fragment/query delimiters. means persisted paths come back truncated—a#1.mp4 loads as a. lowkey risky for project save/load. the fix is pretty straightforward: segment-level encoding so those chars get properly escaped before URL parsing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/projectPersistence.ts` around lines 121 - 149,
The toFileUrl currently calls encodeURI on the whole path which leaves '#' and
'?' unescaped and breaks round-trips; change to encode each path segment with
encodeURIComponent (split on "/", encode each segment, then rejoin) so special
chars like '#' and '?' are percent-encoded, preserving proper parsing by new
URL() in fromFileUrl; keep fromFileUrl as-is (it can continue to use new URL()
and decodeURIComponent on the pathname) and ensure you preserve the drive-letter
handling logic (the regex that checks for /^[a-zA-Z]:/ and the slice(1)
behavior) in the updated toFileUrl implementation.
package.json (1)

17-17: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

npm run build can still emit a broken Windows artifact.

build:win now does the right thing, but the default build script still skips build:native:win. so anyone packaging on Windows via the standard entrypoint will ship an app without the helper binary, and the native capture path is kinda dead on arrival. I’d make build platform-aware or route Windows builds through build:win so there’s only one correct path.

Also applies to: 24-25

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 17, The current "build" npm script can produce a
Windows package missing the helper binary because it doesn't invoke the
Windows-specific step; update the "build" script in package.json to be
platform-aware or to delegate to the correct platform-specific script: detect
process.platform (or use a cross-platform runner) and run "build:win" when on
win32, otherwise run the existing sequence (ensure it also executes
"build:native:win" when targeting Windows); modify the "build" entry so it
always triggers the Windows native build path for Windows hosts (preserving
current behavior for other platforms) and keep references to "build",
"build:win", and "build:native:win" so reviewers can locate the change.
electron/ipc/handlers.ts (1)

1195-1218: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

get-recorded-video-path fallback only finds .webm — native helper outputs .mp4.

This is fallback-only (when currentRecordingSession is missing), so usually harmless, but if state ever gets dropped after a Windows native recording, the fallback will skip every recording-*.mp4 and report "No recorded video found". Consider including .mp4 (and excluding -webcam.mp4) so the fallback also covers the new pipeline.

-			const videoFiles = files.filter(
-				(file) => file.endsWith(".webm") && !file.endsWith("-webcam.webm"),
-			);
+			const videoFiles = files.filter(
+				(file) =>
+					(file.endsWith(".webm") || file.endsWith(".mp4")) &&
+					!file.endsWith("-webcam.webm") &&
+					!file.endsWith("-webcam.mp4"),
+			);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 1195 - 1218, The fallback in
ipcMain.handle("get-recorded-video-path") only looks for ".webm" files so it
misses native Windows ".mp4" outputs; update the files filtering logic (the
videoFiles computation that references RECORDINGS_DIR and
currentRecordingSession) to include both ".webm" and ".mp4" while still
excluding webcam variants (e.g., files ending with "-webcam.webm" or
"-webcam.mp4"), keep the existing sorting/reverse selection to pick the latest
file, and preserve the same return shapes and error handling.
🟡 Minor comments (13)
src/components/video-editor/timeline/TimelineEditor.tsx-1442-1449 (1)

1442-1449: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hard-coded empty-state copy regresses i18n.

These new literals bypass useScopedT, so non-English locales will suddenly get English for the first timeline state. lowkey easy fix: add translation keys for both branches instead of inlining the strings here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/timeline/TimelineEditor.tsx` around lines 1442 -
1449, The empty-state text in TimelineEditor.tsx currently uses hard-coded
English strings controlled by hasVideoSource ("Loading Timeline"/"No Video
Loaded" and the subtitle lines); replace these inline literals with translated
strings obtained via the existing i18n hook (useScopedT) and new translation
keys (e.g. timeline.loadingTitle, timeline.noVideoTitle,
timeline.loadingSubtitle, timeline.noVideoSubtitle) so both branches call t(...)
instead of embedding English, and add the corresponding keys to the locale
files.
scripts/test-windows-native-cursor.mjs-1060-1084 (1)

1060-1084: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

JSON.parse on each stdout line will nuke the run on any non-JSON byte

if PowerShell ever emits a non-JSON line — partial chunk straddling a flush boundary, a CLIXML preamble that didn't go to stderr, a transient warning, anything — JSON.parse(trimmed) throws inside the data handler and takes the whole script with it. lowkey risky for a diagnostic that's meant to be self-healing.

wrap it and surface the bad line so you can keep collecting valid samples.

🛡️ guard
 			for (const line of lines) {
 				const trimmed = line.trim();
 				if (!trimmed) {
 					continue;
 				}

-				const event = JSON.parse(trimmed);
-				events.push(event);
-				if (event.asset?.id && !assets.has(event.asset.id)) {
-					assets.set(event.asset.id, event.asset);
-				}
+				let event;
+				try {
+					event = JSON.parse(trimmed);
+				} catch (error) {
+					process.stderr.write(
+						`[cursor-native-test] dropped non-JSON line: ${trimmed.slice(0, 200)}\n`,
+					);
+					continue;
+				}
+				events.push(event);
+				if (event.asset?.id && !assets.has(event.asset.id)) {
+					assets.set(event.asset.id, event.asset);
+				}
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-native-cursor.mjs` around lines 1060 - 1084, The
onStdout handler currently calls JSON.parse(trimmed) directly which will throw
on any non-JSON line and crash the sampler; wrap the parse in a try/catch inside
the onStdout callback (around JSON.parse(trimmed)) so parsing errors are caught,
write a brief diagnostic to stderr including the offending trimmed line and any
parse error (use the existing prefix like `[cursor-native-test]`), and continue
processing remaining lines so valid events still get pushed to events and assets
is updated; reference the onStdout handler, lineBuffer, trimmed,
JSON.parse(trimmed), events, assets and stoppingSampler when making the change.
docs/engineering/windows-native-recorder-roadmap.md-19-21 (1)

19-21: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

duplicate non-goal bullet

"Replacing the editor/export pipeline." shows up twice back-to-back; the second one looks like the intended version with the trailing clarification. probably a rebase smudge.

📝 fix
 ## Non-Goals

-- Replacing the editor/export pipeline.
 - Replacing the editor/export pipeline. A later pass can reintroduce a separate editable native `webcamVideoPath`; the current Windows-native milestone prioritizes a helper-owned multi-flux MP4 with deterministic screen/audio/mic/webcam sync.
 - Adding a native fallback for macOS or Linux in this branch.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/engineering/windows-native-recorder-roadmap.md` around lines 19 - 21,
Remove the duplicate bullet that reads "Replacing the editor/export pipeline."
and keep the clarified version that extends it ("Replacing the editor/export
pipeline. A later pass can reintroduce a separate editable native
`webcamVideoPath`; the current Windows-native milestone prioritizes a
helper-owned multi-flux MP4 with deterministic screen/audio/mic/webcam sync."),
ensuring only the single, full explanatory bullet remains in the list.
electron/native/wgc-capture/src/dshow_webcam_capture.cpp-167-229 (1)

167-229: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove dead registry-resolution code that's never called. findRegisteredVideoInput, deviceMatchScore, readPropertyString, readRegistryString, splitWords, normalizeDeviceName, and containsInsensitive are all sitting in the anonymous namespace but never get invoked anywhere — initialize() just uses the supplied directShowClsid directly without any registry lookups. This is a chunk of nontrivial code that's kinda cursed to leave laying around. Since everything's TU-local anyway, might as well yeet it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/dshow_webcam_capture.cpp` around lines 167 -
229, Delete the dead registry-resolution functions and any related declarations
in the anonymous namespace: remove findRegisteredVideoInput, deviceMatchScore,
readPropertyString, readRegistryString, splitWords, normalizeDeviceName, and
containsInsensitive (both their declarations and definitions) since initialize()
never calls them and they are TU-local; ensure you also remove any forward
declarations and using/including headers that exist solely to support these
functions to avoid unused-symbol or include clutter.
electron/native/wgc-capture/src/audio_sample_utils.cpp-183-205 (1)

183-205: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unbounded enqueue if the encoder stalls — lowkey risky on long captures.

pushSystem / pushMicrophone only bail on stopRequested_. If the sink writer (or whatever output_ does in main.cpp) blocks for any reason, both systemQueue_ and microphoneQueue_ grow without bound, and you'll only notice via memory pressure. A simple high-water-mark cap (drop or coalesce when queue exceeds, say, 2 seconds of audio) plus a one-shot warning would make failure modes far more debuggable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/audio_sample_utils.cpp` around lines 183 -
205, pushSystem and pushMicrophone can enqueue unbounded data if the downstream
stalls; add a high-water-mark check inside the mutex before calling append to
prevent uncontrolled growth. Specifically, in pushSystem and pushMicrophone
(guarded by includeSystem_/includeMicrophone_ and stopRequested_), compute
whether systemQueue_ / microphoneQueue_ already hold >= ~2 seconds worth of
audio (based on your queue's byte-per-second or sample-count units) and if so
drop or coalesce the incoming chunk instead of calling append; emit a one-shot
warning log the first time this cap is hit and still call cv_.notify_all() as
before. Keep the check+drop logic synchronous and non-blocking (do not wait on
space), and reuse existing symbols append, systemQueue_, microphoneQueue_,
microphoneGain_, mutex_, stopRequested_, and cv_.notify_all().
electron/native/wgc-capture/src/mf_encoder.cpp-99-101 (1)

99-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

MFShutdown in finalize() runs even if MFStartup failed → unbalanced ref-count.

If MFStartup on line 99 fails, initialize() returns false without ever taking a Startup ref. The destructor then calls finalize() (line 81), which unconditionally calls MFShutdown() on line 359. It's mostly harmless (returns MF_E_SHUTDOWN-ish), but you've already got the right pattern in webcam_capture.cpp with mfStarted_ — mirror it here:

🛡️ Proposed fix
@@ class MFEncoder ...
+    bool mfStarted_ = false;
@@ MFEncoder::initialize ...
-    if (!succeeded(MFStartup(MF_VERSION), "MFStartup")) {
+    if (!succeeded(MFStartup(MF_VERSION), "MFStartup")) {
         return false;
     }
+    mfStarted_ = true;
@@ MFEncoder::finalize ...
-    MFShutdown();
+    if (mfStarted_) {
+        MFShutdown();
+        mfStarted_ = false;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/mf_encoder.cpp` around lines 99 - 101,
MFShutdown is being called unconditionally in finalize() even when MFStartup
failed; add a boolean member (e.g., mfStarted_) defaulting to false, set
mfStarted_ = true only after a successful MFStartup() in initialize(), and
modify finalize()/the destructor to call MFShutdown() only if mfStarted_ is
true; ensure the new mfStarted_ field is declared on the encoder class so the
startup/shutdown ref-counts are balanced.
electron-builder.json5-7-10 (1)

7-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move the Windows helper to Windows-only packaging config.

The top-level asarUnpack (line 10) and files (line 22) globs apply to all platforms in electron-builder, so macOS and Linux bundles will pick up electron/native/bin/** too. Since the helper is Windows-only, throw both globs under the win target instead (lines 70–75) to keep it outta the other platforms and stay clean on package size.

Also applies to: lines 19-22

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron-builder.json5` around lines 7 - 10, Move the Windows-only helper
globs out of the top-level "asarUnpack" and top-level "files" arrays and add
them under the "win" target configuration so they apply only to Windows;
specifically remove "electron/native/bin/**" from the top-level "asarUnpack" and
the corresponding Windows helper entries in the top-level "files" array, then
insert those globs into the "win" target (the existing win block) so the
"asarUnpack" and "files" entries for that helper are Windows-only.
src/hooks/useScreenRecorder.ts-188-200 (1)

188-200: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

handing the camera off from preview to native helper is lowkey racy.

stopWebcamPreviewStream() calls track.stop() and immediately returns. on Windows, webcam release isn't always synchronous — some drivers/virtual cams (NVIDIA Broadcast 👀) hold the device for a short tail. firing up the native helper right after to grab the same deviceId can intermittently fail with "device in use".

if you've seen this work reliably across the targeted webcams that's fine, but worth either (a) awaiting a small grace period, or (b) handling the native error and showing a friendlier toast/retry. otherwise users will hit it once and assume the feature is broken.

also: setting webcamReady.current = true after stopping the preview stream reads weirdly — the comment on the variable is "preview is acquired and ready". in this branch you're using it as "we're done waiting on the preview, native owns it now". a tiny rename or a comment would save the next reader a double-take.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScreenRecorder.ts` around lines 188 - 200,
stopWebcamPreviewStream currently stops tracks synchronously which can race with
immediately acquiring the same device in the native helper; update
stopWebcamPreviewStream to wait briefly after calling track.stop() (e.g., await
a small timeout/poll for tracks to fully end) or implement retry logic when the
native helper fails to open the device, and surface a friendly toast on failure;
also clarify the semantics of webcamReady.current (either rename to something
like previewReleasedOrReady or add a comment) so its meaning (“preview released
and native may acquire”) matches how it’s set in stopWebcamPreviewStream.
src/components/launch/LaunchWindow.tsx-258-259 (1)

258-259: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

these pointer-down counters look like orphaned scaffolding.

setHudPointerDownCount and setRecordPointerDownCount are declared with the count value discarded ([, setX]). nothing reads them anywhere in this component, yet every pointerdown on the HUD root and on the mic button triggers a state update → a full HUD re-render for zero observable behavior. on a recording HUD that's a real number of wasted renders during normal mouse activity.

if these are placeholders for an upcoming feature, consider parking them behind a TODO with a tracking issue. otherwise they should probably go.

cleanup if not needed
-	const [, setHudPointerDownCount] = useState(0);
-	const [, setRecordPointerDownCount] = useState(0);
...
-		<div
-			className={`...`}
-			onPointerDown={() => {
-				setHudPointerDownCount((count) => count + 1);
-			}}
-		>
+		<div
+			className={`...`}
+		>
...
-					onPointerDown={() => {
-						setRecordPointerDownCount((count) => count + 1);
-					}}

Also applies to: 330-332, 541-543

scripts/test-windows-wgc-helper.mjs-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Number("") is 0, which is kinda cursed for a duration.

?? only catches undefined/null. If anyone exports OPENSCREEN_WGC_TEST_DURATION_MS= (empty) or sets it to a non-numeric value in CI, you'll silently get a 0ms stop and the helper never gets a chance to record. Same trap on line 14 — guard with finite check.

🛡️ Proposed guard
-const DURATION_MS = Number(process.env.OPENSCREEN_WGC_TEST_DURATION_MS ?? 5000);
+const DURATION_RAW = Number(process.env.OPENSCREEN_WGC_TEST_DURATION_MS);
+const DURATION_MS = Number.isFinite(DURATION_RAW) && DURATION_RAW > 0 ? DURATION_RAW : 5000;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-wgc-helper.mjs` at line 14, The DURATION_MS
initialization using Number(process.env.OPENSCREEN_WGC_TEST_DURATION_MS ?? 5000)
treats an empty string or non-numeric value as 0; change the logic around
DURATION_MS to parse the env var then validate with a finite/numeric check and
fall back to 5000 if invalid — e.g., read
process.env.OPENSCREEN_WGC_TEST_DURATION_MS, convert to Number, and if
Number.isFinite(value) (or !Number.isNaN(value) && value > 0) use it, otherwise
use the default 5000; update the DURATION_MS constant and any related usage to
rely on this validated value.
electron/ipc/nativeBridge.ts-159-179 (1)

159-179: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent payload guarding — project handlers will TypeError on malformed requests.

cursor uses request.payload?.videoPath (lines 197, 202) but project accesses request.payload.projectData/.path directly. A renderer that sends { domain: "project", action: "setCurrentVideoPath" } without a payload will throw Cannot read properties of undefined (reading 'path'), which the outer catch then turns into an INTERNAL_ERROR with retryable: true — both wrong (it's a validation failure, not a retryable internal error).

lowkey safer to align styles or validate payload shape per action.

🛡️ Minimal fix — at least don't crash
 						case "saveProjectFile":
+							if (!request.payload || typeof request.payload !== "object") {
+								return createErrorResponse(requestId, "INVALID_REQUEST", "Missing payload for saveProjectFile.");
+							}
 							return createSuccessResponse(
 								requestId,
 								await projectService.saveProjectFile(
 									request.payload.projectData,
 									request.payload.suggestedName,
 									request.payload.existingProjectPath,
 								),
 							);
@@
 						case "setCurrentVideoPath":
+							if (!request.payload || typeof request.payload.path !== "string") {
+								return createErrorResponse(requestId, "INVALID_REQUEST", "Missing path payload for setCurrentVideoPath.");
+							}
 							return createSuccessResponse(
 								requestId,
 								projectService.setCurrentVideoPath(request.payload.path),
 							);
src/components/video-editor/VideoEditor.tsx-159-159 (1)

159-159: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mac exports silently drop click emphasis when "only on clicks" is enabled.

cursorClickTimestamps stays empty and exporters only read that array via clickEmphasisAlpha(). When onlyOnClicks: true (macOS only), this returns alpha=0, killing all click viz in exports. Preview works fine because it uses interactionType from cursorRecordingData.samples[*] instead. Exporters need to branch on that same path for macOS, or derive timestamps from cursorRecordingData during export setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` at line 159,
cursorClickTimestamps is always empty so exports get alpha=0 when onlyOnClicks
is true on macOS; update the export setup to derive click timestamps from
cursorRecordingData instead of relying solely on cursorClickTimestamps.
Specifically, in the export path that calls clickEmphasisAlpha(), detect when
cursorClickTimestamps is empty and onlyOnClicks is enabled and then scan
cursorRecordingData.samples for samples whose interactionType indicates a click,
populate the timestamps array (or have clickEmphasisAlpha branch to use
interactionType from cursorRecordingData.samples) so click emphasis is preserved
in exports. Ensure you reference the cursorClickTimestamps variable, the
clickEmphasisAlpha() callsite, the onlyOnClicks flag, and
cursorRecordingData.samples/interactionType when implementing the change.
electron/ipc/handlers.ts-1064-1067 (1)

1064-1067: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

proc.stdin.write can throw — wrap it.

If the helper crashed between start and stop (or its stdin pipe is already gone), this throws synchronously and we skip awaiting stoppedPathPromise. Outer try/catch will still fire stopCursorRecording and reset state, which is okay-ish, but we lose any in-flight Recording stopped... output and propagate a less helpful error. Cheap fix:

-			const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc);
-			proc.stdin.write("stop\n");
+			const stoppedPathPromise = waitForNativeWindowsCaptureStop(proc);
+			try {
+				proc.stdin.write("stop\n");
+			} catch (writeErr) {
+				console.warn("Failed to send stop to native helper, awaiting exit anyway:", writeErr);
+			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/handlers.ts` around lines 1064 - 1067, The
proc.stdin.write("stop\n") call can throw synchronously; wrap that call in its
own try/catch so a write failure doesn't skip awaiting the stoppedPathPromise
from waitForNativeWindowsCaptureStop(proc). Specifically, around the write call
in the same block that creates stoppedPathPromise (reference stoppedPathPromise,
waitForNativeWindowsCaptureStop and proc), catch any synchronous error from
proc.stdin.write, record or log it, then still await stoppedPathPromise and
assign stoppedPath; after awaiting, rethrow or surface the original write error
if needed so outer error handling still runs but you don't lose the in-flight
"Recording stopped..." output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93b26c76-1739-40b9-822f-4d613be6bfd4

📥 Commits

Reviewing files that changed from the base of the PR and between c13ec0d and dcb402f.

⛔ Files ignored due to path filters (26)
  • src/assets/cursors/Cursor=App-Starting.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Beachball.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Cross.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Default.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Hand-(Grabbing).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Hand-(Open).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Hand-(Pointing).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Help.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Menu.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Move.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Not-Allowed.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-(Down).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-(Left).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-(Left-Right).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-(Right).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-(Up).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-(Up-Down).svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-North-East-South-West.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-North-South.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-North-West-South-East.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Resize-West-East.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Text-Cursor.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Up-Arrow.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Wait.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Zoom-In.svg is excluded by !**/*.svg
  • src/assets/cursors/Cursor=Zoom-Out.svg is excluded by !**/*.svg
📒 Files selected for processing (68)
  • .gitignore
  • README.md
  • docs/architecture/native-bridge.md
  • docs/engineering/windows-native-recorder-roadmap.md
  • docs/testing/windows-native-cursor.md
  • electron-builder.json5
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/ipc/nativeBridge.ts
  • electron/main.ts
  • electron/native-bridge/cursor/adapter.ts
  • electron/native-bridge/cursor/recording/factory.ts
  • electron/native-bridge/cursor/recording/session.ts
  • electron/native-bridge/cursor/recording/telemetryRecordingSession.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts
  • electron/native-bridge/cursor/telemetryCursorAdapter.ts
  • electron/native-bridge/services/cursorService.ts
  • electron/native-bridge/services/projectService.ts
  • electron/native-bridge/services/systemService.ts
  • electron/native-bridge/store.ts
  • electron/native/README.md
  • electron/native/wgc-capture/CMakeLists.txt
  • electron/native/wgc-capture/src/audio_sample_utils.cpp
  • electron/native/wgc-capture/src/audio_sample_utils.h
  • electron/native/wgc-capture/src/dshow_webcam_capture.cpp
  • electron/native/wgc-capture/src/dshow_webcam_capture.h
  • electron/native/wgc-capture/src/main.cpp
  • electron/native/wgc-capture/src/mf_encoder.cpp
  • electron/native/wgc-capture/src/mf_encoder.h
  • electron/native/wgc-capture/src/monitor_utils.cpp
  • electron/native/wgc-capture/src/monitor_utils.h
  • electron/native/wgc-capture/src/wasapi_loopback_capture.cpp
  • electron/native/wgc-capture/src/wasapi_loopback_capture.h
  • electron/native/wgc-capture/src/webcam_capture.cpp
  • electron/native/wgc-capture/src/webcam_capture.h
  • electron/native/wgc-capture/src/wgc_session.cpp
  • electron/native/wgc-capture/src/wgc_session.h
  • electron/preload.ts
  • package.json
  • scripts/build-windows-wgc-helper.mjs
  • scripts/capture-openscreen-preview.mjs
  • scripts/test-windows-native-cursor.mjs
  • scripts/test-windows-wgc-helper.mjs
  • src/components/launch/LaunchWindow.tsx
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/timeline/TimelineEditor.tsx
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/cursorRenderer.ts
  • src/components/video-editor/videoPlayback/motionSmoothing.ts
  • src/components/video-editor/videoPlayback/uploadedCursorAssets.ts
  • src/hooks/useScreenRecorder.ts
  • src/lib/cursor/nativeCursor.ts
  • src/lib/exporter/audioEncoder.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/muxer.ts
  • src/lib/exporter/videoExporter.ts
  • src/lib/nativeWindowsRecording.ts
  • src/native/client.ts
  • src/native/contracts.ts
  • src/native/hooks/useCursorRecordingData.ts
  • src/native/hooks/useCursorTelemetry.ts
  • src/native/index.ts

Comment thread electron/ipc/handlers.ts
Comment thread electron/main.ts Outdated
Comment thread electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts Outdated
Comment thread electron/native-bridge/cursor/telemetryCursorAdapter.ts
Comment thread src/components/video-editor/SettingsPanel.tsx Outdated
Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Comment thread src/components/video-editor/videoPlayback/cursorRenderer.ts
Comment thread src/hooks/useScreenRecorder.ts Outdated
Comment thread src/lib/exporter/frameRenderer.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 294-296: The code calls window.electronAPI.switchToEditor()
unconditionally after awaiting
nativeBridgeClient.project.setCurrentVideoPath(result.path); change this so the
editor transition is gated on the bridge call's success: call
nativeBridgeClient.project.setCurrentVideoPath(result.path), inspect its
returned result (e.g., a success flag or error payload), and only call
window.electronAPI.switchToEditor() when that returned result indicates success;
on failure, handle it (log via processLogger/console or surface a user error)
and avoid switching to the editor. Ensure you reference the existing result
variable and the functions setCurrentVideoPath and switchToEditor when
implementing the check.

In `@tests/e2e/windows-native-checklist.spec.ts`:
- Around line 15-26: The tests are locale-fragile because electron.launch starts
with a fresh user-data-dir so the app picks the host Windows locale; update the
launch call (the electron.launch invocation that sets MAIN_JS and
testUserDataDir) to force a stable locale (e.g. add a --lang=en-US CLI arg or
set env LANG/LC_ALL to "en_US.UTF-8" in the env object) or alternatively add
deterministic selectors to the app (add data-testid attributes for the language
prompt, loading/editor/play/background components and update assertions to use
those data-testid values); change the electron.launch env/args in the
testUserDataDir block and any launchApp helper to ensure the locale is forced or
switch assertions to the new data-testid selectors referenced in the tests (also
apply same change to the other occurrences noted around the file).
- Around line 55-63: The cleanup races deleting the Electron temp profile with
the child process teardown: after calling childProcess.kill() (or when
childProcess exists), wait for the child's actual exit event instead of
proceeding immediately—e.g., create a Promise that resolves on the childProcess
'exit'/'close' event (with a fallback timeout like the existing 5_000 ms) and
await that before removing the testUserDataDir (the __testUserDataDir value
accessed as testUserDataDir) and before calling fs.rmSync; also keep the
existing guard checks for childProcess and childProcess.killed so you only wait
when needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c8099f92-8ba0-4a1f-83d6-70a21faee958

📥 Commits

Reviewing files that changed from the base of the PR and between dcb402f and d310e91.

📒 Files selected for processing (4)
  • package.json
  • src/components/launch/LaunchWindow.tsx
  • src/components/launch/SourceSelector.tsx
  • tests/e2e/windows-native-checklist.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/launch/SourceSelector.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Comment thread src/components/launch/LaunchWindow.tsx
Comment thread tests/e2e/windows-native-checklist.spec.ts
Comment thread tests/e2e/windows-native-checklist.spec.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (11)
src/components/video-editor/SettingsPanel.tsx (2)

1251-1329: ⚡ Quick win

All new cursor accordion labels are hardcoded English — missing i18n.

Every other settings section in this file (zoom, speed, effects, background, layout, crop) goes through t("…"), but the entire new "Cursor" accordion uses raw string literals: "Cursor", "Show Cursor", "Size", "Smoothing", "Motion Blur", "Click Bounce". This will break any non-English locale.

♻️ Example — wire through translation keys
-<span className="text-xs font-medium">Cursor</span>
+<span className="text-xs font-medium">{t("cursor.title")}</span>

-<div className="text-[10px] font-medium text-slate-300">Show Cursor</div>
+<div className="text-[10px] font-medium text-slate-300">{t("cursor.showCursor")}</div>

-<div className="text-[10px] font-medium text-slate-300">Size</div>
+<div className="text-[10px] font-medium text-slate-300">{t("cursor.size")}</div>

-<div className="text-[10px] font-medium text-slate-300">Smoothing</div>
+<div className="text-[10px] font-medium text-slate-300">{t("cursor.smoothing")}</div>

-<div className="text-[10px] font-medium text-slate-300">Motion Blur</div>
+<div className="text-[10px] font-medium text-slate-300">{t("cursor.motionBlur")}</div>

-<div className="text-[10px] font-medium text-slate-300">Click Bounce</div>
+<div className="text-[10px] font-medium text-slate-300">{t("cursor.clickBounce")}</div>

Add the corresponding keys to all locale JSON files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 1251 - 1329,
Replace the hardcoded English labels in the new Cursor accordion with
translation keys using the existing i18n helper (call t("...")) where the labels
appear (the "Cursor" title inside AccordionTrigger, "Show Cursor" near the
Switch, and the field labels "Size", "Smoothing", "Motion Blur", "Click Bounce"
shown around the Slider components and value spans); update the JSX in
SettingsPanel (look for AccordionTrigger, AccordionContent, Switch, Slider and
the variables showCursor, cursorSize, cursorSmoothing, cursorMotionBlur,
cursorClickBounce) to call t(...) for each literal and then add corresponding
keys to all locale JSON files so the strings resolve in all locales.

1272-1327: ⚡ Quick win

Cursor sliders are missing onValueCommit — lowkey risky on a hot drag path.

Every other performance-sensitive slider in this file (motionBlur, shadow, roundness, padding, webcam size, crop highlight size/offset) pairs onValueChange with an onValueCommit that triggers the expensive downstream operation (re-render, export re-computation, etc.). The four cursor sliders fire only onValueChange, which means downstream cursor rendering will thrash on every pointer-move tick during a drag.

If the cursor pipeline (smoothing, motion blur, etc.) does any non-trivial work, consider adding commit callbacks:

⚡ Proposed fix
// Interface additions (SettingsPanelProps)
+onCursorSizeCommit?: () => void;
+onCursorSmoothingCommit?: () => void;
+onCursorMotionBlurCommit?: () => void;
+onCursorClickBounceCommit?: () => void;

// Destructor + defaults
+onCursorSizeCommit,
+onCursorSmoothingCommit,
+onCursorMotionBlurCommit,
+onCursorClickBounceCommit,

// Size slider
 onValueChange={(values) => onCursorSizeChange?.(values[0])}
+onValueCommit={() => onCursorSizeCommit?.()}

// Smoothing slider
 onValueChange={(values) => onCursorSmoothingChange?.(values[0])}
+onValueCommit={() => onCursorSmoothingCommit?.()}

// Motion blur slider
 onValueChange={(values) => onCursorMotionBlurChange?.(values[0])}
+onValueCommit={() => onCursorMotionBlurCommit?.()}

// Click bounce slider
 onValueChange={(values) => onCursorClickBounceChange?.(values[0])}
+onValueCommit={() => onCursorClickBounceCommit?.()}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 1272 - 1327, The
four cursor Sliders (value props using cursorSize, cursorSmoothing,
cursorMotionBlur, cursorClickBounce and change handlers onCursorSizeChange,
onCursorSmoothingChange, onCursorMotionBlurChange, onCursorClickBounceChange)
are missing onValueCommit callbacks and currently cause downstream work on every
drag tick; add onValueCommit handlers for each Slider that call a corresponding
commit prop (e.g., onCursorSizeCommit, onCursorSmoothingCommit,
onCursorMotionBlurCommit, onCursorClickBounceCommit) with values[0] so expensive
rendering/export work only runs on commit (keep existing onValueChange for live
UI feedback).
electron/native/wgc-capture/src/main.cpp (2)

601-632: 💤 Low value

Capture-side timestamps are dropped; mixer paces audio off its own clock.

You explicitly discard timestampHns/durationHns from both the mic and loopback callbacks ((void)timestampHns; (void)durationHns;) and just push raw bytes into the mixer. The mixer then synthesizes its own emittedFrames_-based timestamps (audio_sample_utils.cpp:394). Functionally OK as long as WASAPI keeps the queue real-time aligned (the silence backfill in wasapi_loopback_capture.cpp does help here), but it means any persistent capture stall — without a discontinuity flag — translates into the mixer happily stamping silence at the "wrong" wall-clock position, producing slow A/V drift on long recordings.

Not a release blocker, but if you ever see drift over 10+ minute recordings this is the first place I'd look.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/main.cpp` around lines 601 - 632, The
callbacks passed to microphoneCapture.start and loopbackCapture.start currently
drop timestampHns/durationHns and call audioMixer->pushMicrophone/pushSystem
with only raw bytes; update the call chain to preserve and forward capture-side
timestamps so the mixer can pace output from the capture clock (instead of
synthesizing via emittedFrames_). Concretely: extend or add overloads on
audioMixer (e.g., pushMicrophoneWithTimestamp / pushSystemWithTimestamp or add
timestamp parameters to pushMicrophone/pushSystem) to accept timestampHns and
durationHns, change the lambdas in microphoneCapture.start and
loopbackCapture.start to pass those values through, and update the mixer logic
(audio_sample_utils.cpp behavior that uses emittedFrames_) to prefer capture
timestamps and handle discontinuities when timestamps jump or are missing.
Ensure tests/usage that expect the old API are updated and that silence-backfill
logic remains compatible.

126-242: 💤 Low value

parseConfig is a positional substring scan — fragile if the schema ever nests.

findBool/findInt/findDouble/findString all do json.find("\"" + key + "\"") against the whole blob. Today the helper contract is flat so it works, but any nested object that reuses a key name (e.g. a sourceId inside a webcam object plus a top-level sourceId) will silently grab the wrong one. Also findString doesn't handle \uXXXX escapes, so non-ASCII device names that arrive in escaped form will be mangled.

Pulling in a tiny header-only JSON lib (nlohmann/json or simdjson) would kill a whole class of future foot-guns. Not urgent for v1, but the moment the schema grows this'll bite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/main.cpp` around lines 126 - 242, The current
helpers (findBool, findInt64, findInt, findDouble, findString used by
parseConfig) are fragile because they substring-scan the whole JSON blob (wrong
for nested keys) and findString doesn't decode \uXXXX; replace these ad-hoc
parsers with a proper JSON parser (e.g. include nlohmann::json single-header),
parse the input once into a nlohmann::json object, and change
findBool/findInt64/findInt/findDouble/findString to look up keys from that
parsed top-level object (using .contains() and appropriate .get<T>() calls) so
nested fields won't collide and unicode escapes (including \uXXXX) are handled
by the library. Ensure error handling mirrors current fallbacks by returning the
provided fallback on parse or type errors.
electron/native/wgc-capture/src/audio_sample_utils.cpp (4)

347-409: 💤 Low value

Pacing the mixer with sleep_until against wall-clock is a bit redundant and lowkey risky.

The mixer is fed by WASAPI capture, which is already realtime. The sleep_until(audioClockStart + emittedFrames/sampleRate) at line 407 effectively re-paces to wall-clock — fine on the happy path, but if for any reason input bursts in (e.g. WASAPI gap-fill from wasapi_loopback_capture.cpp emits a backlog of silence frames after a discontinuity), the mixer will also sleep through that catch-up, growing the queues unboundedly while the encoder waits.

Two things worth considering:

  1. If the queues are meant to be drained at realtime, the upstream capture rate already enforces that — the sleep is mostly just adding latency.
  2. There's no upper bound on systemQueue_/microphoneQueue_ size. A stalled output callback or a slow encoder will let these grow forever. A simple cap (e.g. drop oldest beyond N seconds) would prevent unbounded memory growth in pathological cases.

Not a release blocker for typical usage, just flagging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/audio_sample_utils.cpp` around lines 347 -
409, AudioMixer::mixLoop currently re-paces output with
sleep_until(audioClockStart + emittedFrames_/sampleRate) which can delay
draining when upstream bursts backlog, and you also lack a cap on
systemQueue_/microphoneQueue_ sizes; change mixLoop to stop using long
wall-clock sleep for pacing (remove sleep_until and rely on the existing
cv_.wait_for and the real-time WASAPI feed / output_ timings) or replace it with
a short non-blocking yield (e.g., sleep_for a few ms) only when necessary, and
implement a bounded-queue policy for systemQueue_ and microphoneQueue_ (e.g.,
maxBytes = N_seconds * format_.sampleRate * format_.blockAlign) so producers
drop oldest bytes when pushing beyond the cap (implement drop-in-push in the
producer or in pop/push helpers), referencing AudioMixer::mixLoop,
audioClockStart, emittedFrames_, sleep_until, systemQueue_, microphoneQueue_,
pop(), includeSystem_/includeMicrophone_, and output_. Ensure queue-dropping is
thread-safe under mutex_ and that stopRequested_ and timelineStarted_ logic
remain unchanged.

178-198: 💤 Low value

Nearest-neighbor resampling will produce audible aliasing on real format mismatches.

convertAudioWithGain does a naive nearest-neighbor frame mapping (std::llround(sourcePosition)) when sample rates differ. That's functional but kinda cursed for audio — any time mic is at 16k/44.1k and the target mix is 48k (or vice versa), you'll get noticeable aliasing/zipper artifacts, especially on speech. In practice WASAPI shared-mode usually hands you the mix format already at 48k so this path may rarely fire, but the moment a mic exposes a different native rate it's gonna sound rough.

For v1 it's probably fine, but consider at minimum linear interpolation, or ideally a real polyphase resampler (or just use MF's IMFTransform resampler since you've already got MF in the build). Worth a TODO if not addressing now.


263-275: 💤 Low value

Nit: start() has no guard against being called twice.

If start() ever gets invoked while thread_ is already joinable, the previous std::thread gets stomped and std::terminate fires. Looking at main.cpp it's only called once per AudioMixer instance, so this is purely defensive, but a quick if (thread_.joinable()) return false; (or assert) would prevent future foot-guns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/audio_sample_utils.cpp` around lines 263 -
275, AudioMixer::start lacks a guard against being called when a previous thread
is still running; check thread_.joinable() at the top of start() and bail
(return false or assert) if true to avoid stomping an active std::thread.
Specifically, in AudioMixer::start() before mutating stopRequested_,
emittedFrames_, timelineStarted_ and launching the lambda that calls mixLoop(),
add a guard using thread_.joinable() to prevent starting a second thread while
the first is still active.

200-241: ⚡ Quick win

Mix-then-clip without headroom = hard clipping when both sources are loud.

mixAudioInPlace straight-up sums two streams and clamps at full-scale. When system audio and mic are both hot (e.g. full-volume music + a loud talker), the sum saturates and you get audible distortion in the recording. Very common pitfall for screen+mic mixing.

A simple improvement: attenuate by ~0.5 (or sqrt(0.5) ≈ 0.707 for power-preserving) before summing, or apply a soft-knee limiter. Even just a constant -3 to -6 dB on each branch buys a lot of headroom. Not a blocker, but users on hot input will notice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/audio_sample_utils.cpp` around lines 200 -
241, mixAudioInPlace currently sums destination and source samples then
hard-clips, causing distortion when both are loud; modify each mixing branch in
mixAudioInPlace (float path detected by isFloatFormat, 16-bit and 32-bit PCM
paths detected by isPcmFormat) to apply a fixed attenuation/gain to inputs
before summing (e.g., multiply both samples by ~0.7071 or 0.5 for headroom) and
then add and clamp (use clampTo for integer paths and std::clamp for float) so
the sum preserves headroom and avoids hard clipping; ensure conversions use
floating math when applying the gain and keep existing clamping behavior after
summation.
electron/native/wgc-capture/src/wasapi_loopback_capture.h (1)

24-24: 💤 Low value

AudioCallback returns void while AudioMixer::OutputCallback returns bool — kinda inconsistent.

The capture-side callback is fire-and-forget (std::function<void(...)>), so even when downstream has good reason to bail (encoder failure, mixer stopped), there's no clean way to propagate that back here. The wasapi_loopback_capture.cpp capture loop already discards whatever the callback "returns" (lines ~339, 388, 390), and consumers in main.cpp lean on the shared stopRequested atomic to short-circuit pushes. Works, but it's lowkey awkward — the audio capture keeps grinding away pushing into queues even after the encoder is toast, and the API shape diverges from AudioMixer::OutputCallback which deliberately uses bool for flow control.

Aligning the signatures (returning bool and breaking out of captureLoop when the consumer says "done") would tighten things up. Not a blocker since stopRequested covers it in practice, but worth a thought.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/wgc-capture/src/wasapi_loopback_capture.h` at line 24, Change
the capture callback typedef to return bool (match AudioMixer::OutputCallback)
and propagate that decision from the capture loop: update using AudioCallback =
std::function<bool(const BYTE* data, DWORD byteCount, int64_t timestampHns,
int64_t durationHns)> in wasapi_loopback_capture.h, modify the captureLoop
implementation in wasapi_loopback_capture.cpp to check the callback return value
and break/stop the loop when it returns false, and adjust all callers (e.g., the
lambda passed from main.cpp and any other consumers) to return false when they
want the capture to stop (encoder failure, mixer stopped) and true to continue;
ensure stopRequested atomic handling remains consistent with the new early-exit
behavior.
electron/native/wgc-capture/src/wasapi_loopback_capture.cpp (2)

345-411: 💤 Low value

Outer captureLoop keeps spinning even if GetBuffer errors out.

If GetBuffer fails (line 362), we break the inner while, but the outer while (!stopRequested_) just sleeps 5ms and tries GetNextPacketSize again. Most transient HRESULTs will probably recover, but a sticky failure (device removal, format change, etc.) means we burn CPU spinning and silently produce no audio until the parent process notices and sends stop. That's lowkey risky for the encoder timeline since video keeps moving.

Minimal fix: track consecutive failures and bail (or surface the error to stderr + set a sentinel so main.cpp can react). Even just propagating the device-removal case would help.

Also, fwiw, lastDevicePositionEnd_ never resets across recovered failures, so if the device clock ever rolls back (e.g. on default-device switch), the gap-detection at line 369 silently swallows it. Edge case — probably fine for v1.


90-103: 💤 Low value

i18n nit: hardcoded English stopwords in scoreDeviceName.

The stopword filter (L"microphone", L"mic", L"audio", L"input") only helps English-locale Windows installs. On a French/German/Japanese system, friendly names come back localized (e.g. "Microphone (Realtek)" in French is still "Microphone" but "Mikrofon" in German), so the heuristic silently does nothing useful and noisy filler words can dominate the score.

Not a blocker — the exact-match and substring paths above (lines 75–82) usually win, and worst case we fall back to the default endpoint. Just flagging that name-based mic selection is implicitly English-biased.

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 1416-1421: The picker handler should re-run the existing
validation before calling approveFilePath: instead of directly calling
approveFilePath(result.filePaths[0]) in the open-video-file-picker handler, pass
the selected path through approveReadableVideoPath (or the same validation logic
used there) and only call approveFilePath if that validation returns success; if
validation fails, return { success: false, error: <validation message> } and do
not set currentProjectPath to null. Ensure you reference the
open-video-file-picker handler, approveFilePath, and approveReadableVideoPath
when making the change so the dialog cannot bless non-video or non-existent
paths.
- Around line 1729-1732: The handlers resolveVideoPath, readCursorRecordingFile,
and readCursorTelemetryFile currently accept a normalized videoPath directly,
bypassing the module's path approval checks; update resolveVideoPath to run
normalizeVideoSourcePath(videoPath ?? currentVideoPath) through the same
path-approval function used elsewhere in this file (the approval/check utility
used for other IPC reads) and only return or forward the approved path, and
change the cursor loaders (invocations of readCursorRecordingFile and
readCursorTelemetryFile) to receive the approved path; if approval fails, return
or throw a consistent error instead of calling the reader.

In `@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts`:
- Around line 104-105: The await this.waitUntilReady() call in start() can
reject and currently leaves the spawned helper running; wrap the readiness await
in a try/catch, call the session's helper-teardown routine (e.g.,
this.teardownHelper() or the existing stop/kill/cleanup method that closes the
PowerShell helper process) inside the catch to ensure the helper is terminated,
then rethrow the error; apply the same pattern to the other readiness call site
around lines 260-267 so any failing waitUntilReady() always triggers the helper
cleanup before propagating the error.

In `@electron/native/wgc-capture/src/dshow_webcam_capture.cpp`:
- Around line 366-378: start() is not exception-safe: if std::thread constructor
throws after impl_->mediaControl->Run() and after impl_->running is set, the
graph remains running with no capture thread. Fix by making thread creation
exception-safe in DirectShowWebcamCapture::start(): after calling
impl_->mediaControl->Run(), set stopRequested_ and impl_->running only once the
thread is successfully created, or wrap the thread_ = std::thread(...) call in a
try/catch that on failure calls impl_->mediaControl->Stop(), clears
impl_->running/stopRequested_ as needed, and returns false so state is
consistent; reference impl_->mediaControl->Run(), impl_->running,
stopRequested_, and thread_.
- Around line 46-229: The listed helper functions (readPropertyString,
containsInsensitive, normalizeDeviceName, splitWords, deviceMatchScore,
readRegistryString, and findRegisteredVideoInput) are dead code; either wire
findRegisteredVideoInput into the DirectShow init/fallback path or
remove/deduplicate them: if registry-based fallback is intended, modify the
capture initialization code that resolves directShowClsid to call
findRegisteredVideoInput(deviceId, deviceName, selectedClsid, selectedName,
bestScore) when directShowClsid resolution fails and use
selectedClsid/selectedName as the resolved device; otherwise delete these helper
functions from dshow_webcam_capture.cpp and move any shared helpers into a
single shared utility used by webcam_capture.cpp and wasapi_loopback_capture.cpp
to avoid duplication.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 583-590: The startNativeWindowsRecordingIfAvailable function
currently throws an Error when isNativeWindowsCaptureAvailable reports
unsupported-os or missing-helper, preventing the caller from falling back to
getDisplayMedia; change the behavior so the function does not throw on those
availability failures but instead returns a clear "not available" result (e.g.,
false or null) so the caller can run the existing browser getDisplayMedia path;
update the same pattern in the other availability check block (the one at the
later occurrence) to mirror this fallback behavior and preserve throwing only
for unexpected fatal errors.
- Around line 398-441: finalizeNativeWindowsRecording currently nulls
nativeWindowsRecording.current and resets UI state before awaiting
window.electronAPI.stopNativeWindowsRecording, which can lose the active handle
on failure; change the flow so you only clear nativeWindowsRecording.current and
call setRecording/setPaused/setElapsedSeconds/reset
accumulatedDurationMs/segmentStartedAt after stopNativeWindowsRecording has
succeeded or the recording is discarded, and if stopNativeWindowsRecording
throws or returns a non-success keep nativeWindowsRecording.current intact and
reset activeNativeRecording.finalizing to false so the renderer can
retry/cancel; keep the initial check/setting of activeNativeRecording.finalizing
(in finalizeNativeWindowsRecording) but move the UI-reset and clearing of
discardRecordingId.current into the success/discard path or into finally only
when the stop completed successfully.

In `@tests/e2e/windows-native-checklist.spec.ts`:
- Around line 276-277: Replace the bare array return for the cursor.getTelemetry
stub with the real CursorTelemetryLoadResult-shaped object so callers see the
expected fields; specifically, in the branch checking request.domain ===
"cursor" && request.action === "getTelemetry" return success({ success: true,
samples: [] }) (or the equivalent production shape with a samples array) instead
of success([]).

---

Nitpick comments:
In `@electron/native/wgc-capture/src/audio_sample_utils.cpp`:
- Around line 347-409: AudioMixer::mixLoop currently re-paces output with
sleep_until(audioClockStart + emittedFrames_/sampleRate) which can delay
draining when upstream bursts backlog, and you also lack a cap on
systemQueue_/microphoneQueue_ sizes; change mixLoop to stop using long
wall-clock sleep for pacing (remove sleep_until and rely on the existing
cv_.wait_for and the real-time WASAPI feed / output_ timings) or replace it with
a short non-blocking yield (e.g., sleep_for a few ms) only when necessary, and
implement a bounded-queue policy for systemQueue_ and microphoneQueue_ (e.g.,
maxBytes = N_seconds * format_.sampleRate * format_.blockAlign) so producers
drop oldest bytes when pushing beyond the cap (implement drop-in-push in the
producer or in pop/push helpers), referencing AudioMixer::mixLoop,
audioClockStart, emittedFrames_, sleep_until, systemQueue_, microphoneQueue_,
pop(), includeSystem_/includeMicrophone_, and output_. Ensure queue-dropping is
thread-safe under mutex_ and that stopRequested_ and timelineStarted_ logic
remain unchanged.
- Around line 263-275: AudioMixer::start lacks a guard against being called when
a previous thread is still running; check thread_.joinable() at the top of
start() and bail (return false or assert) if true to avoid stomping an active
std::thread. Specifically, in AudioMixer::start() before mutating
stopRequested_, emittedFrames_, timelineStarted_ and launching the lambda that
calls mixLoop(), add a guard using thread_.joinable() to prevent starting a
second thread while the first is still active.
- Around line 200-241: mixAudioInPlace currently sums destination and source
samples then hard-clips, causing distortion when both are loud; modify each
mixing branch in mixAudioInPlace (float path detected by isFloatFormat, 16-bit
and 32-bit PCM paths detected by isPcmFormat) to apply a fixed attenuation/gain
to inputs before summing (e.g., multiply both samples by ~0.7071 or 0.5 for
headroom) and then add and clamp (use clampTo for integer paths and std::clamp
for float) so the sum preserves headroom and avoids hard clipping; ensure
conversions use floating math when applying the gain and keep existing clamping
behavior after summation.

In `@electron/native/wgc-capture/src/main.cpp`:
- Around line 601-632: The callbacks passed to microphoneCapture.start and
loopbackCapture.start currently drop timestampHns/durationHns and call
audioMixer->pushMicrophone/pushSystem with only raw bytes; update the call chain
to preserve and forward capture-side timestamps so the mixer can pace output
from the capture clock (instead of synthesizing via emittedFrames_). Concretely:
extend or add overloads on audioMixer (e.g., pushMicrophoneWithTimestamp /
pushSystemWithTimestamp or add timestamp parameters to
pushMicrophone/pushSystem) to accept timestampHns and durationHns, change the
lambdas in microphoneCapture.start and loopbackCapture.start to pass those
values through, and update the mixer logic (audio_sample_utils.cpp behavior that
uses emittedFrames_) to prefer capture timestamps and handle discontinuities
when timestamps jump or are missing. Ensure tests/usage that expect the old API
are updated and that silence-backfill logic remains compatible.
- Around line 126-242: The current helpers (findBool, findInt64, findInt,
findDouble, findString used by parseConfig) are fragile because they
substring-scan the whole JSON blob (wrong for nested keys) and findString
doesn't decode \uXXXX; replace these ad-hoc parsers with a proper JSON parser
(e.g. include nlohmann::json single-header), parse the input once into a
nlohmann::json object, and change
findBool/findInt64/findInt/findDouble/findString to look up keys from that
parsed top-level object (using .contains() and appropriate .get<T>() calls) so
nested fields won't collide and unicode escapes (including \uXXXX) are handled
by the library. Ensure error handling mirrors current fallbacks by returning the
provided fallback on parse or type errors.

In `@electron/native/wgc-capture/src/wasapi_loopback_capture.h`:
- Line 24: Change the capture callback typedef to return bool (match
AudioMixer::OutputCallback) and propagate that decision from the capture loop:
update using AudioCallback = std::function<bool(const BYTE* data, DWORD
byteCount, int64_t timestampHns, int64_t durationHns)> in
wasapi_loopback_capture.h, modify the captureLoop implementation in
wasapi_loopback_capture.cpp to check the callback return value and break/stop
the loop when it returns false, and adjust all callers (e.g., the lambda passed
from main.cpp and any other consumers) to return false when they want the
capture to stop (encoder failure, mixer stopped) and true to continue; ensure
stopRequested atomic handling remains consistent with the new early-exit
behavior.

In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 1251-1329: Replace the hardcoded English labels in the new Cursor
accordion with translation keys using the existing i18n helper (call t("..."))
where the labels appear (the "Cursor" title inside AccordionTrigger, "Show
Cursor" near the Switch, and the field labels "Size", "Smoothing", "Motion
Blur", "Click Bounce" shown around the Slider components and value spans);
update the JSX in SettingsPanel (look for AccordionTrigger, AccordionContent,
Switch, Slider and the variables showCursor, cursorSize, cursorSmoothing,
cursorMotionBlur, cursorClickBounce) to call t(...) for each literal and then
add corresponding keys to all locale JSON files so the strings resolve in all
locales.
- Around line 1272-1327: The four cursor Sliders (value props using cursorSize,
cursorSmoothing, cursorMotionBlur, cursorClickBounce and change handlers
onCursorSizeChange, onCursorSmoothingChange, onCursorMotionBlurChange,
onCursorClickBounceChange) are missing onValueCommit callbacks and currently
cause downstream work on every drag tick; add onValueCommit handlers for each
Slider that call a corresponding commit prop (e.g., onCursorSizeCommit,
onCursorSmoothingCommit, onCursorMotionBlurCommit, onCursorClickBounceCommit)
with values[0] so expensive rendering/export work only runs on commit (keep
existing onValueChange for live UI feedback).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a8aafb2e-f190-41ac-a42c-a2d04f4bba31

📥 Commits

Reviewing files that changed from the base of the PR and between d310e91 and d076a9b.

📒 Files selected for processing (24)
  • electron/ipc/handlers.ts
  • electron/ipc/nativeBridge.ts
  • electron/main.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native-bridge/cursor/telemetryCursorAdapter.ts
  • electron/native-bridge/services/projectService.ts
  • electron/native/README.md
  • electron/native/wgc-capture/src/audio_sample_utils.cpp
  • electron/native/wgc-capture/src/audio_sample_utils.h
  • electron/native/wgc-capture/src/dshow_webcam_capture.cpp
  • electron/native/wgc-capture/src/main.cpp
  • electron/native/wgc-capture/src/wasapi_loopback_capture.cpp
  • electron/native/wgc-capture/src/wasapi_loopback_capture.h
  • scripts/test-windows-wgc-helper.mjs
  • src/components/launch/LaunchWindow.tsx
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/videoPlayback/cursorRenderer.ts
  • src/hooks/useScreenRecorder.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/nativeWindowsRecording.ts
  • tests/e2e/windows-native-checklist.spec.ts
✅ Files skipped from review due to trivial changes (4)
  • src/lib/nativeWindowsRecording.ts
  • electron/main.ts
  • src/lib/exporter/frameRenderer.ts
  • electron/native/README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • electron/native-bridge/services/projectService.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts
  • scripts/test-windows-wgc-helper.mjs
  • electron/ipc/nativeBridge.ts
  • src/components/video-editor/videoPlayback/cursorRenderer.ts
  • src/components/video-editor/VideoEditor.tsx

Comment thread electron/ipc/handlers.ts Outdated
Comment thread electron/ipc/handlers.ts
Comment thread electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts Outdated
Comment thread electron/native/wgc-capture/src/dshow_webcam_capture.cpp Outdated
Comment thread electron/native/wgc-capture/src/dshow_webcam_capture.cpp
Comment thread src/hooks/useScreenRecorder.ts
Comment thread src/hooks/useScreenRecorder.ts
Comment thread tests/e2e/windows-native-checklist.spec.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/video-editor/VideoEditor.tsx (1)

1934-1954: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

preview is ignoring the saved cursor highlight settings now.

VideoPlayback still falls back to DEFAULT_CURSOR_HIGHLIGHT and [] when the parent omits cursorHighlight / cursorClickTimestamps. Since this call site stopped passing both props, preview no longer reflects the editor state even though export still uses effectiveCursorHighlight.

small wiring fix
 											cursorRecordingData={cursorRecordingData}
 											trimRegions={trimRegions}
 											speedRegions={speedRegions}
 											annotationRegions={annotationOnlyRegions}
@@
 											onBlurDataChange={handleBlurDataPreviewChange}
 											onBlurDataCommit={commitState}
 											cursorTelemetry={cursorTelemetry}
+											cursorHighlight={effectiveCursorHighlight}
+											cursorClickTimestamps={cursorClickTimestamps}
 											showCursor={showCursor}
 											cursorSize={cursorSize}
 											cursorSmoothing={cursorSmoothing}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/VideoEditor.tsx` around lines 1934 - 1954, The
preview stopped reflecting saved cursor highlight because VideoPlayback is no
longer being passed the cursorHighlight and cursorClickTimestamps props; update
the VideoPlayback call (the component receiving props like cursorRecordingData,
trimRegions, etc.) to pass cursorHighlight={effectiveCursorHighlight} and
cursorClickTimestamps={cursorClickTimestamps} (or the corresponding state/prop
names used in this file) so the preview uses the editor's effective settings
rather than falling back to DEFAULT_CURSOR_HIGHLIGHT/[].
♻️ Duplicate comments (1)
electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts (1)

106-107: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Readiness failure can still orphan the helper process.

lowkey risky: start() awaits readiness, but timeout/rejection only rejects the promise and doesn’t guarantee teardown. If caller drops the session on error, the helper can keep running.

Suggested minimal fix
 async start(): Promise<void> {
@@
-		await this.waitUntilReady();
+		try {
+			await this.waitUntilReady();
+		} catch (error) {
+			const normalizedError =
+				error instanceof Error ? error : new Error(String(error));
+			this.failHelper(normalizedError);
+			throw normalizedError;
+		}
 	}
@@
 	private waitUntilReady() {
 		return new Promise<void>((resolve, reject) => {
 			this.readyResolve = resolve;
 			this.readyReject = reject;
 			this.readyTimer = setTimeout(() => {
-				this.rejectReady(new Error("Timed out waiting for Windows cursor helper readiness"));
+				this.failHelper(
+					new Error("Timed out waiting for Windows cursor helper readiness"),
+				);
 			}, READY_TIMEOUT_MS);
 		});
 	}

Also applies to: 249-255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts`
around lines 106 - 107, start() currently awaits waitUntilReady() but if that
promise times out or rejects the helper process can be orphaned; change start()
(and the other call site that awaits waitUntilReady around lines 249-255) to
ensure cleanup on failure by wrapping the await in try/catch/finally (or using
Promise.race/AbortController) and calling the session teardown method (e.g.,
this.stop(), this.dispose(), or the helper-kill routine) in the catch/finally
path so the helper process is always terminated when readiness fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 154-159: cursorClickTimestamps has been hardcoded to an empty
array which disables click-driven emphasis on macOS; change it to derive
timestamps conditionally instead: keep the same inputs (videoSourcePath,
useCursorTelemetry, useCursorRecordingData) and compute cursorClickTimestamps
based on available data (prefer cursorRecordingData if present, otherwise
extract click events from cursorTelemetry), and only fall back to an empty array
when neither source provides click timestamps; update the derivation referenced
by effectiveCursorHighlight and preview/export paths so mac projects get the
platform-specific fallback rather than a hardcoded [] (use the symbols
cursorClickTimestamps, useCursorTelemetry, useCursorRecordingData,
cursorRecordingData, cursorTelemetry, videoSourcePath/fromFileUrl, and
effectiveCursorHighlight to locate and change the logic).

In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 382-390: The fallback that treats any positive video.currentTime
as the full duration in resolveCurrentDuration is unsafe (forceResolveDuration
sets currentTime to 0.1); change the check so we only accept currentTime as the
duration when playback has actually reached the end: i.e., require video.ended
=== true OR that video.duration is finite and video.currentTime >=
video.duration - EPSILON (or matches seekable end), then call onDurationChange;
otherwise ignore small positive currentTime values. Update
resolveCurrentDuration (and related logic that relies on
lastResolvedDurationRef/onDurationChange) to use this stricter check.

In `@src/lib/cursor/nativeCursor.ts`:
- Around line 291-315: getNativeCursorClickBounceProgress (and the similar
helper at lines 429-448) currently reverse-scans recordingData.samples on every
frame which causes O(samples × frames) perf; replace the per-frame linear tail
scan with a binary-search helper that finds the last sample with sample.timeMs
<= timeMs (or maintain a monotonic cursor index when iterating sequential
frames) and use that index to compute ageMs and the click-bounce value; update
resolveInterpolatedNativeCursorFrame and getNativeCursorClickBounceProgress to
call the new binary-search (or use the monotonic index) so semantics (handling
null/undefined recordingData, NATIVE_CURSOR_CLICK_ANIMATION_MS threshold, and
interactionType === "click") remain the same while avoiding repeated full
reverse scans called by drawNativeCursor and the VideoPlayback ticker.

In `@src/lib/exporter/frameRenderer.ts`:
- Around line 599-606: When projectNativeCursorToLocal(...) returns null we only
reset motion blur; also reset the cursor smoothing state so exports don't ease
in from stale off-screen positions. In the same branch where
resetNativeCursorMotionBlurState(this.nativeCursorMotionBlurState) is called,
also clear the smoothing state by calling the smoothing reset used elsewhere
(e.g. resetNativeCursorSmoothingState(this.nativeCursorSmoothingState) or
otherwise clear the smoothing buffer/last-position stored on
this.nativeCursorSmoothingState) so both motion blur and smoothing are reset
when the cursor falls outside the crop.

---

Outside diff comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1934-1954: The preview stopped reflecting saved cursor highlight
because VideoPlayback is no longer being passed the cursorHighlight and
cursorClickTimestamps props; update the VideoPlayback call (the component
receiving props like cursorRecordingData, trimRegions, etc.) to pass
cursorHighlight={effectiveCursorHighlight} and
cursorClickTimestamps={cursorClickTimestamps} (or the corresponding state/prop
names used in this file) so the preview uses the editor's effective settings
rather than falling back to DEFAULT_CURSOR_HIGHLIGHT/[].

---

Duplicate comments:
In `@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts`:
- Around line 106-107: start() currently awaits waitUntilReady() but if that
promise times out or rejects the helper process can be orphaned; change start()
(and the other call site that awaits waitUntilReady around lines 249-255) to
ensure cleanup on failure by wrapping the await in try/catch/finally (or using
Promise.race/AbortController) and calling the session teardown method (e.g.,
this.stop(), this.dispose(), or the helper-kill routine) in the catch/finally
path so the helper process is always terminated when readiness fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99cef220-8320-4f1c-a307-ab49414c1eee

📥 Commits

Reviewing files that changed from the base of the PR and between d076a9b and 1c92110.

📒 Files selected for processing (11)
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts
  • scripts/test-windows-native-cursor.mjs
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/lib/cursor/nativeCursor.ts
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/videoExporter.ts
  • src/native/contracts.ts
✅ Files skipped from review due to trivial changes (1)
  • src/native/contracts.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/exporter/videoExporter.ts
  • scripts/test-windows-native-cursor.mjs
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts

Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Comment thread src/components/video-editor/VideoPlayback.tsx
Comment thread src/lib/cursor/nativeCursor.ts
Comment thread src/lib/exporter/frameRenderer.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
scripts/test-windows-native-cursor.mjs (2)

6-12: 💤 Low value

Number(process.env.X ?? default) will happily produce NaN and bake it into PowerShell.

If anyone sets e.g. CURSOR_TEST_SAMPLE_INTERVAL_MS=fast in CI, you get NaN, and then Start-Sleep -Milliseconds NaN (line 322/469) fails inside PS, which throws under $ErrorActionPreference = 'Stop' and the sampler dies immediately with a not-super-obvious error. A tiny clamp/validate at the top would save someone a 2am debugging session.

diff
-const SAMPLE_INTERVAL_MS = Number(process.env.CURSOR_TEST_SAMPLE_INTERVAL_MS ?? 25);
-const DURATION_MS = Number(process.env.CURSOR_TEST_DURATION_MS ?? 1800);
-const SCREEN_FRAME_INTERVAL_MS = Number(process.env.CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS ?? 100);
+function readPositiveIntEnv(name, fallback) {
+	const raw = process.env[name];
+	if (raw === undefined) return fallback;
+	const parsed = Number(raw);
+	if (!Number.isFinite(parsed) || parsed <= 0) {
+		throw new Error(`Invalid ${name}=${raw}; expected positive number`);
+	}
+	return Math.floor(parsed);
+}
+
+const SAMPLE_INTERVAL_MS = readPositiveIntEnv("CURSOR_TEST_SAMPLE_INTERVAL_MS", 25);
+const DURATION_MS = readPositiveIntEnv("CURSOR_TEST_DURATION_MS", 1800);
+const SCREEN_FRAME_INTERVAL_MS = readPositiveIntEnv("CURSOR_TEST_SCREEN_FRAME_INTERVAL_MS", 100);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-native-cursor.mjs` around lines 6 - 12, The numeric
environment parsing for SAMPLE_INTERVAL_MS, DURATION_MS,
SCREEN_FRAME_INTERVAL_MS (and READY_TIMEOUT_MS if desired) can yield NaN when
env vars are non-numeric; validate and clamp each parsed value immediately after
creation (e.g., check Number.isFinite(value) and fall back to the intended
default or a safe minimum/maximum) and emit a warning if an env value was
invalid so downstream PowerShell calls (Start-Sleep) never receive NaN; update
the initialization around SAMPLE_INTERVAL_MS, DURATION_MS,
SCREEN_FRAME_INTERVAL_MS to perform these checks and fallback logic and ensure
OUTPUT_DIR handling remains unchanged.

476-492: 💤 Low value

waitForReady polls instead of resolving from the stream; nit: cleaner.

The current 25ms setInterval over a shared events array works, but it's a polling loop tied to a buffer that's mutated elsewhere. A small "ready" promise resolved directly from onStdout would remove the timer/race-y interplay and make the timeout the only wall-clock concern. Totally optional — current code is correct, just kinda cursed at 2am.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-windows-native-cursor.mjs` around lines 476 - 492, Replace the
polling loop in waitForReady with a promise that is resolved directly when the
stdout handler pushes a "ready" event: change waitForReady to return a new
Promise that sets a single timeout (setTimeout) to reject after READY_TIMEOUT_MS
and exposes a local resolve function; remove setInterval. In the stdout handler
(the function that currently mutates events, e.g., onStdout or wherever
events.push(...) happens) call that resolve when you detect an event.type ===
"ready" so the promise resolves immediately; ensure the timeout is cleared on
resolve to avoid leaks. Keep the existing rejection message and behavior but
switch polling to this direct-resolution pattern.
electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts (1)

4-21: 💤 Low value

nit: asset should be WindowsCursorAssetPayload | null for consistency.

The PowerShell sampler emits asset = $null on every sample where no asset is captured (see scripts/test-windows-native-cursor.mjs line 332/360), so JSON.parse will produce asset: null rather than absent. bounds on line 19 already correctly allows | null, but asset on line 20 is just optional. Consumers happen to use optional chaining (payload.asset?.id) so it works, but the type lowkey lies about what comes off the wire.

diff
-	asset?: WindowsCursorAssetPayload;
+	asset?: WindowsCursorAssetPayload | null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts`
around lines 4 - 21, The WindowsCursorSampleEvent type's asset field is declared
optional but should explicitly allow null; update the WindowsCursorSampleEvent
interface to change the asset property from optional (asset?) to a non-optional
field typed as WindowsCursorAssetPayload | null (i.e., asset:
WindowsCursorAssetPayload | null) so it matches incoming JSON from the
PowerShell sampler, and then grep for uses of
WindowsCursorSampleEvent.payload.asset to ensure callers handle null (or adjust
code to use optional chaining if needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/test-windows-native-cursor.mjs`:
- Around line 1025-1044: The current findPlaywrightChromiumExecutable uses
lexicographic sort on chromium-* directory names which misorders numeric
revisions; change the candidate selection to extract the numeric revision from
each entry.name (the suffix after "chromium-"), convert to integer (handle
non-numeric safely), sort candidates by that numeric revision descending, then
pick the highest numeric candidate (falling back to defaultPath). Update the
mapping/filtration logic around candidates in findPlaywrightChromiumExecutable
so it pairs each path with its parsed revision number, sorts by that number (not
by string), and returns the highest-version path or defaultPath if none valid.
- Around line 1100-1118: The handler that parses stdout lines calls
JSON.parse(trimmed) directly which can throw and crash the sampler; wrap that
parse in a try/catch (or otherwise validate the trimmed string is valid JSON)
inside the stdout data handler so malformed or non-JSON lines are skipped (and
optionally logged with the `[cursor-native-test]` prefix), and only push to
events/update assets when parsing succeeds — reference the JSON.parse(trimmed)
call, the events array, and the assets Map in the stdout handler and mirror the
defensive filtering used in onStderr for non-JSON/CLIXML lines.

---

Nitpick comments:
In
`@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts`:
- Around line 4-21: The WindowsCursorSampleEvent type's asset field is declared
optional but should explicitly allow null; update the WindowsCursorSampleEvent
interface to change the asset property from optional (asset?) to a non-optional
field typed as WindowsCursorAssetPayload | null (i.e., asset:
WindowsCursorAssetPayload | null) so it matches incoming JSON from the
PowerShell sampler, and then grep for uses of
WindowsCursorSampleEvent.payload.asset to ensure callers handle null (or adjust
code to use optional chaining if needed).

In `@scripts/test-windows-native-cursor.mjs`:
- Around line 6-12: The numeric environment parsing for SAMPLE_INTERVAL_MS,
DURATION_MS, SCREEN_FRAME_INTERVAL_MS (and READY_TIMEOUT_MS if desired) can
yield NaN when env vars are non-numeric; validate and clamp each parsed value
immediately after creation (e.g., check Number.isFinite(value) and fall back to
the intended default or a safe minimum/maximum) and emit a warning if an env
value was invalid so downstream PowerShell calls (Start-Sleep) never receive
NaN; update the initialization around SAMPLE_INTERVAL_MS, DURATION_MS,
SCREEN_FRAME_INTERVAL_MS to perform these checks and fallback logic and ensure
OUTPUT_DIR handling remains unchanged.
- Around line 476-492: Replace the polling loop in waitForReady with a promise
that is resolved directly when the stdout handler pushes a "ready" event: change
waitForReady to return a new Promise that sets a single timeout (setTimeout) to
reject after READY_TIMEOUT_MS and exposes a local resolve function; remove
setInterval. In the stdout handler (the function that currently mutates events,
e.g., onStdout or wherever events.push(...) happens) call that resolve when you
detect an event.type === "ready" so the promise resolves immediately; ensure the
timeout is cleared on resolve to avoid leaks. Keep the existing rejection
message and behavior but switch polling to this direct-resolution pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42c3641f-1ebe-441e-a7cf-1ec0a2f42a1e

📥 Commits

Reviewing files that changed from the base of the PR and between 1c92110 and 0f26eac.

📒 Files selected for processing (4)
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts
  • scripts/test-windows-native-cursor.mjs
✅ Files skipped from review due to trivial changes (1)
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.script.ts

Comment thread scripts/test-windows-native-cursor.mjs
Comment thread scripts/test-windows-native-cursor.mjs
@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

Addressed the non-inline nitpicks from the latest CodeRabbit summary in 159b24e:

  • validated numeric cursor diagnostic env vars with fallback warnings;
  • replaced waitForReady polling with a ready promise resolved directly from stdout parsing;
  • corrected the Windows cursor sample wire type to asset: WindowsCursorAssetPayload | null.

Validation run:

  • npx biome check scripts\test-windows-native-cursor.mjs electron\native-bridge\cursor\recording\windowsNativeRecordingSession.types.ts
  • npm run test:cursor-native:win
  • npm run build-vite

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

Addressed the two latest non-inline CodeRabbit notes in f609611 as well:

  • VideoPlayback now receives cursorHighlight={effectiveCursorHighlight} and cursorClickTimestamps={cursorClickTimestamps}, so preview and export use the same effective cursor highlight settings again.
  • WindowsNativeRecordingSession.start() now kills the PowerShell helper and cleans up the temporary script if readiness fails or times out, preventing an orphaned cursor sampler.

Validation run:

  • npx biome check src\components\video-editor\VideoEditor.tsx src\components\video-editor\VideoPlayback.tsx src\lib\cursor\nativeCursor.ts src\lib\exporter\frameRenderer.ts electron\native-bridge\cursor\recording\windowsNativeRecordingSession.ts
  • npm run build-vite
  • npm run test:cursor-native:win

npm test was also run, but the full suite is still failing on pre-existing unrelated failures: blur overlay expected value, tutorial locale data, and browser exporter tests failing on relative fixture URLs / missing canvas implementation in the current Vitest environment.

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

Addressed the older unresolved CodeRabbit thread group in eab205a:

  • re-validates files selected by open-video-file-picker before approval;
  • requires approved video paths for native-bridge cursor sidecar reads;
  • removes unused DirectShow registry matching helpers;
  • makes DirectShow webcam thread startup exception-safe;
  • keeps the native recording handle retryable until stop succeeds;
  • allows non-Windows / unsupported OS paths to continue to the browser recorder while intentionally keeping missing Windows helper as a hard native-recorder error.

One thread was verified as not applicable: the E2E native bridge mock for cursor.getTelemetry should continue returning CursorTelemetryPoint[], because CursorService.getTelemetry() unwraps the internal CursorTelemetryLoadResult before data reaches the renderer-facing bridge response.

Validation run:

  • npx biome check electron\ipc\handlers.ts src\hooks\useScreenRecorder.ts tests\e2e\windows-native-checklist.spec.ts
  • npm run build-vite
  • npm run build:native:win

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem all tests ok on my side on windows.

Comment thread README.md Outdated
@siddharthvaddem
Copy link
Copy Markdown
Owner

madlad 🐐 taking a look

@EtienneLescot EtienneLescot force-pushed the feat/cursor-pipeline branch from 764f60d to 0720a6d Compare May 10, 2026 13:19
@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

EtienneLescot commented May 10, 2026

@siddharthvaddem @FabLrc
Rebased this branch on latest upstream/main (162e734b).

There was one risky area during the rebase: the editor UI and IPC wiring had diverged from upstream, so I restored the native cursor wiring explicitly in 0720a6d8022683eb308b99ff8c8d6a382e69bd43.

Validation run after the rebase:

  • npm run build-vite
  • npm test (133 tests passing)
  • npx vitest run src\lib\cursor\nativeCursor.test.ts src\lib\exporter\audioEncoder.test.ts src\lib\exporter\videoExporter.test.ts
  • npm run build:native:win

Residual risk is mainly around the Windows editor cursor panel / preview / export integration because those files had the most overlap with upstream changes. I would manually re-check a short Windows editable-cursor recording, the cursor controls, and exports with and without the editable cursor enabled.

@EtienneLescot EtienneLescot requested a review from FabLrc May 10, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants