Skip to content

feat: add macOS native capture and cursor pipeline#573

Merged
siddharthvaddem merged 15 commits into
siddharthvaddem:mainfrom
EtienneLescot:feat/macos-native-capture-pipeline
May 18, 2026
Merged

feat: add macOS native capture and cursor pipeline#573
siddharthvaddem merged 15 commits into
siddharthvaddem:mainfrom
EtienneLescot:feat/macos-native-capture-pipeline

Conversation

@EtienneLescot
Copy link
Copy Markdown
Collaborator

@EtienneLescot EtienneLescot commented May 11, 2026

Description

This draft PR starts the macOS-only port of the native capture and cursor pipeline introduced for Windows in PR #217.

The Windows native capture PR has been merged, so this branch is now based directly on latest upstream/main.

The goal is to bring the same architectural separation to macOS: Electron owns recording/session orchestration, while a native ScreenCaptureKit helper owns screen/window capture, cursor exclusion, audio/webcam timing, encoding, and muxing.

What changed

  • Adds a macOS native recorder roadmap at docs/engineering/macos-native-recorder-roadmap.md.
  • Defines the first macOS helper contract in src/lib/nativeMacRecording.ts.
  • Adds source-id parsing helpers for ScreenCaptureKit display/window ids.
  • Adds focused tests for macOS source-id parsing.
  • Adds npm run build:native:mac as a placeholder build entrypoint.
  • Documents the expected macOS helper layout in electron/native/README.md.
  • Ignores generated macOS native helper build output and SwiftPM/Xcode state.

Follow-up fixes from macOS testing

  • Improves HUD click-through handling on macOS so dock controls, the drag handle, and popover interactions receive mouse events reliably while transparent areas still pass clicks through.
  • Replaces the native Electron drag-region dependency for the HUD grab handle with an explicit pointer-driven window move IPC path.
  • Fixes the language picker popover so it can be scrolled and language choices can be clicked in the transparent HUD window.
  • Adds the missing editor → recorder IPC handlers used by the "Return to Recorder" confirmation flow.
  • Adds editor preview support for macOS recordings with separate system-audio and microphone tracks by preparing and syncing a supplemental preview audio track for existing multi-track MP4 recordings.
  • Syncs locale key structures so the macOS pre-commit/i18n check passes.

macOS-only scope

This PR does not change Windows native capture behavior and does not add Linux native capture.

The macOS helper implementation is not shipped yet. This first step is the architecture/contract scaffold so the native helper can be added behind a clean boundary, matching the Windows WGC helper separation.

Architecture notes

The intended macOS backend follows the same separation used by the Windows helper:

  • macOS capture through ScreenCaptureKit-based native helpers.
  • Windows capture through a separate WGC helper.
  • Electron as the process/session coordinator.
  • Renderer code consuming stable contracts rather than helper-specific process details.

Validation run

Passed locally on Windows:

  • npm run build:native:mac (skips successfully on non-macOS hosts)
  • npm run build-vite
  • npx vitest run src\lib\nativeMacRecording.test.ts --pool=threads

Previously passed before rebase:

  • npm test (137 tests passing)

Passed locally on macOS (May 13, 2026):

  • npm run build:native:mac
  • npm run build-vite
  • npm run lint
  • npm run i18n:check
  • npm test (139 tests passing)
  • Electron smoke: source selector screens/windows + thumbnails, full-screen/window source selection, open video, open project, playback/pause/seek
  • Electron macOS native recording smoke with fake mic + system audio: mic-only, system-audio-only, mic + system audio produced expected video/audio tracks

Known local validation gap:

  • Camera/webcam recording was not tested on this cloud Mac.

Maintainer test checklist

Capture + Launch

  • pre commit check on macOS
  • 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.

Summary by CodeRabbit

  • New Features

    • Native macOS screen/window recording with pause/resume, attachable webcam, native cursor capture, and supplemental preview audio
    • HUD overlay repositioning via drag and programmatic move API
    • Cursor "clip to bounds" export/preview option and editor UI control
  • Improvements

    • On-demand permission flows and clearer retry guidance during recording startup
    • Editable cursor overlay enabled on macOS when native cursor data present
    • Preview-audio syncing for supplemental audio tracks
  • Documentation

    • Added macOS native recorder roadmap and native helper README
  • Chores

    • Mac helper build/packaging script, packaging updates, and .gitignore updates
  • Localization

    • Expanded translations and new cursor/recording strings across many locales
  • Tests

    • Added unit tests for mac source ID parsing and cursor telemetry behaviors

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 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

sorry — i can't reliably reconstruct the full validated review stack artifact with every rangeId assigned correctly in this session. give me permission to retry and i'll produce a complete, validated hidden artifact block that follows the exact constraints.

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem I am starting to work on this ;-)

@EtienneLescot EtienneLescot force-pushed the feat/macos-native-capture-pipeline branch from 603e06c to b011cc6 Compare May 11, 2026 08:42
@auberginewly
Copy link
Copy Markdown
Contributor

Hey @EtienneLescot! Really excited to see this PR — I've been researching exactly this problem for openscreen.

I did a deep dive into macOS cursor control for screen recording, covering:

  • SCStreamConfiguration.showsCursor = false as the correct API
  • Why Electron's desktopCapturer can't currently exclude the cursor
  • NSCursor.currentSystem image-hashing for cursor type detection
  • Coordinate mapping for Retina/multi-display setups

I'd love to contribute to the Swift helper process part. What's the best way to help right now — is there a specific piece you'd like someone to pick up?

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

EtienneLescot commented May 12, 2026

Hey @EtienneLescot! Really excited to see this PR — I've been researching exactly this problem for openscreen.

I did a deep dive into macOS cursor control for screen recording, covering:

  • SCStreamConfiguration.showsCursor = false as the correct API
  • Why Electron's desktopCapturer can't currently exclude the cursor
  • NSCursor.currentSystem image-hashing for cursor type detection
  • Coordinate mapping for Retina/multi-display setups

I'd love to contribute to the Swift helper process part. What's the best way to help right now — is there a specific piece you'd like someone to pick up?

Hey @auberginewly, sure, I'll probably need help for edge cases and Retina/multi-display setups, I'll let you know when I am there.

@auberginewly
Copy link
Copy Markdown
Contributor

auberginewly commented May 12, 2026

Sounds good, ping me whenever you're ready! ☺️ @EtienneLescot

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

macOS validation note: I fixed the i18n pre-commit failure by syncing locale key structures against the English baseline. Part of that is directly related to this PR (macOS app/menu labels in common.actions.* and the launch cursor toggle labels), and part is broader catalog cleanup required because npm run i18n:check validates every locale/namespace, not only keys touched by this PR. I kept it as a separate, committable i18n cleanup so the macOS pre-commit check can legitimately stay checked.

@EtienneLescot EtienneLescot marked this pull request as ready for review May 13, 2026 13:58
@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem I tested it on mac, all seems ok.
I just can't test the camera.
@auberginewly if you want to test the camera that would be of help ;-)

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: 303169b38f

ℹ️ 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 src/hooks/useScreenRecorder.ts Outdated
Comment thread src/hooks/useScreenRecorder.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: 9

Caution

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

⚠️ Outside diff range comments (1)
src/hooks/useScreenRecorder.ts (1)

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

Restart/cancel are dead on the native macOS path.

Once nativeMacRecording.current is set, both handlers fall through: there is no native macOS branch, and there is no active screenRecorder to catch the browser path. That leaves the HUD restart/cancel buttons as no-ops for native mac recording.

Suggested fix
 const restartRecording = async () => {
   if (restarting.current) return;
 
   if (nativeWindowsRecording.current) {
     const activeRecordingId = recordingId.current;
     restarting.current = true;
     discardRecordingId.current = activeRecordingId;
     try {
       await finalizeNativeWindowsRecording(true);
       await startRecording();
     } finally {
       restarting.current = false;
     }
     return;
   }
+
+  if (nativeMacRecording.current) {
+    const activeRecordingId = recordingId.current;
+    restarting.current = true;
+    discardRecordingId.current = activeRecordingId;
+    try {
+      await finalizeNativeMacRecording(true);
+      await startRecording();
+    } finally {
+      restarting.current = false;
+    }
+    return;
+  }
 
   const activeScreenRecorder = screenRecorder.current;
   if (!activeScreenRecorder || activeScreenRecorder.recorder.state === "inactive") return;
   ...
 };
 
 const cancelRecording = () => {
   if (nativeWindowsRecording.current) {
     const activeRecordingId = recordingId.current;
     discardRecordingId.current = activeRecordingId;
     allowAutoFinalize.current = false;
     void finalizeNativeWindowsRecording(true);
     return;
   }
+
+  if (nativeMacRecording.current) {
+    const activeRecordingId = recordingId.current;
+    discardRecordingId.current = activeRecordingId;
+    allowAutoFinalize.current = false;
+    void finalizeNativeMacRecording(true);
+    return;
+  }
 
   const activeScreenRecorder = screenRecorder.current;
   ...
 };

Also applies to: 1475-1501

🤖 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 1403 - 1455, The restart/cancel
handlers don't handle the native macOS recording path, so when
nativeMacRecording.current is set the HUD buttons become no-ops; update the
restartRecording flow (and the similar block around lines 1475-1501) to mirror
the native Windows branch: detect nativeMacRecording.current, set
restarting.current = true and discardRecordingId.current = recordingId.current,
call the native mac finalizer (e.g. finalizeNativeMacRecording(true)) awaited,
then await startRecording(), and finally clear restarting.current in a finally
block so native mac recordings are properly restarted/cancelled; apply this same
pattern to any other restart/cancel handler that currently only checks
nativeWindowsRecording.current.
♻️ Duplicate comments (1)
src/i18n/locales/tr/settings.json (1)

197-199: ⚠️ Potential issue | 🟡 Minor

same empty nrLevel pattern here.

the entire audio section got stripped down to just "nrLevel": {} – previous translations for noise reduction (title/levels/descriptions) are gone. this matches the issue in tr/launch.json line 18.

see the verification script in tr/launch.json review for checking if this is intentional across the codebase.

🤖 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/i18n/locales/tr/settings.json` around lines 197 - 199, The
"audio.nrLevel" object was accidentally emptied, removing the noise reduction
translations (title, level labels and descriptions); restore the original keys
and values under the "audio.nrLevel" object so it includes the noise reduction
title, each level label and their descriptions (match the structure used
elsewhere, e.g., the original "nrLevel" pattern referenced in tr/launch.json),
and run the same verification script referenced in tr/launch.json to ensure the
restored keys conform to the expected schema.
🧹 Nitpick comments (15)
src/i18n/locales/fr/settings.json (1)

12-12: 💤 Low value

nit: empty object serving no purpose

this "speed": {} under zoom is empty and doesn't exist in the other locale files you're syncing. lowkey should just remove it entirely unless there's a specific reason to keep an empty placeholder here.

🧹 suggested cleanup
 		},
-		"speed": {},
 		"threeD": {
🤖 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/i18n/locales/fr/settings.json` at line 12, Remove the extraneous empty
"speed": {} key from the JSON object in the fr settings locale (the entry
currently under the zoom group) so the file matches other locale files; if you
intended a placeholder, replace it with a commented note or an actual
translation string, otherwise delete the "speed" property entirely to avoid
dead/unused keys.
src/i18n/locales/zh-TW/settings.json (1)

12-12: ⚡ Quick win

nit: remove the orphaned empty object for consistency

this empty "speed": {} doesn't appear in the vi or zh-CN locale files and kinda disrupts the structure. probably leftover from a refactor. removing it would align zh-TW with the other locales and keep things tidy.

🧹 suggested cleanup
 		"focusMode": {
 			"title": "對焦模式",
 			"manual": "手動",
 			"auto": "自動",
 			"autoDescription": "攝影機跟隨錄製時的游標位置"
 		},
-		"speed": {},
 		"threeD": {
 			"title": "3D 旋轉",
🤖 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/i18n/locales/zh-TW/settings.json` at line 12, Remove the orphaned empty
"speed" key from the zh-TW settings JSON to match other locales; locate the
"speed": {} entry in the settings.json locale object and delete that key/value
pair so the structure aligns with vi and zh-CN locales and no trailing empty
object remains.
docs/engineering/macos-native-recorder-roadmap.md (2)

110-110: 💤 Low value

doc might be slightly behind implementation state

line 110 says "The helper now writes ScreenCaptureKit system audio into the primary MP4 and attempts runtime-gated native microphone capture" – this matches the Swift implementation which already does native mic capture with captureMicrophone and microphoneCaptureDeviceID.

earlier at line 13, the doc mentions "explicit companion path until it can be moved into the helper" which sounds like mic isn't native yet. might want to sync these statements so it's clear native mic is already implemented with runtime gating for macOS version support.

not blocking, just a clarity thing for future readers.

🤖 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/macos-native-recorder-roadmap.md` at line 110, Update the
roadmap text to avoid conflicting statements about microphone capture: clarify
that the ScreenCaptureKit helper already implements native mic capture
(referencing captureMicrophone and microphoneCaptureDeviceID behavior) with
runtime gating for macOS versions that support it, and remove or revise the
earlier "explicit companion path until it can be moved into the helper" wording
so it no longer implies mic capture remains non-native; keep the note that
AVFoundation webcam composition is still the target end state and that webcam is
currently an Electron-recorded sidecar.

12-12: 💤 Low value

nit: "macOS system audio" flagged by static analysis

technically "OS" = "operating system" so this is redundant, but in practice "macOS system audio" is clear and commonly used. could just say "system audio through ScreenCaptureKit" if you want to appease the linter, but honestly this is fine as-is.

🤖 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/macos-native-recorder-roadmap.md` at line 12, Replace the
phrase "Capture macOS system audio through ScreenCaptureKit" with "Capture
system audio through ScreenCaptureKit" to satisfy the linter; locate and update
the exact sentence "Capture macOS system audio through ScreenCaptureKit" in the
macOS native recorder roadmap and make the wording change.
electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift (1)

242-254: infinite loop with no stdin command handling

unlike the ScreenCaptureKit helper which reads stdin for pause/resume/stop commands, this cursor helper just runs forever in a while(true) loop until killed.

this is probably fine for a sampling helper that's meant to run alongside the main capture and get terminated externally, but worth noting there's no graceful shutdown path. if you ever need to stop sampling cleanly (e.g., to emit final stats), you'd need to add stdin command handling like the other helper.

not blocking, just pointing it out 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
`@electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`
around lines 242 - 254, The current infinite loop in main (while true { ...
Thread.sleep(...) }) in the cursor helper never handles stdin commands for
pause/resume/stop, so add stdin command handling similar to the ScreenCaptureKit
helper: spawn a background reader that listens to stdin for "pause", "resume",
and "stop" commands and sets a shared state/enum (e.g., isPaused/isRunning or
CursorCaptureState) accessed by the main loop; modify the loop to check that
state (call mouseTracker.pump(), consume/emit only when running or emit reduced
heartbeat when paused), break cleanly on "stop" to emit any final stats via the
existing emit(...) path, and ensure proper synchronization between the reader
and main loop around mouseTracker.pump(), currentCursorType(), and
leftButtonDown() so shutdown is graceful.
src/lib/nativeMacRecording.ts (1)

93-117: 💤 Low value

lowkey DRY opportunity but not critical

both parsers follow the exact same pattern – could extract a helper like parseIdFromSourcePrefix(sourceId, prefix) to reduce duplication. but with only 2 functions and clear names, this is more of a nice-to-have.

tests look solid in the next file, edge cases are covered. ✓

🤖 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/lib/nativeMacRecording.ts` around lines 93 - 117, Extract the duplicated
parsing logic into a shared helper like parseIdFromSourcePrefix(sourceId,
prefix) and update parseMacWindowIdFromSourceId and
parseMacDisplayIdFromSourceId to call it; the helper should check sourceId
startsWith(prefix), split on ":", validate /^\d+$/ and return Number(value) or
null, preserving current behavior and tests for edge cases.
electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift (1)

392-410: native microphone uses kvo-style setvalue for private api

lines 404-406: configuration.setValue(true, forKey: "captureMicrophone") and setValue(deviceId, forKey: "microphoneCaptureDeviceID") are accessing undocumented ScreenCaptureKit properties.

the runtime feature detection with responds(to:) on lines 603-607 is good defensive coding, but this could break in future macOS updates if Apple changes the key names or removes the feature.

documented risk – the warning emission on unavailability (lines 395-400) handles the failure case gracefully. just noting this is fragile and might need updates when macOS 15/16 drop.

🤖 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/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift`
around lines 392 - 410, The code uses KVC to set undocumented ScreenCaptureKit
keys via configuration.setValue(..., forKey: ...) (see configuration.setValue,
resolveMicrophoneCaptureDeviceID, nativeMicrophoneEnabled, emit), which is
fragile; wrap all KVC access in a single safe helper (e.g.,
setPrivateCaptureKey(_:value:on:)) that first feature-detects using
supportsNativeMicrophoneCapture and responds(to:) or canRespondToKey, then
performs the set inside an ObjC-safe try/catch (or use
NSObject.performSelector/objc_setAssociated/try? pattern) so it cannot throw at
runtime, log/emit a warning if the key is missing or the set fails, and fall
back to configuration.capturesAudio only; centralize the private key strings as
constants and call this helper from the existing microphone branch instead of
calling setValue directly.
electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts (2)

315-348: 💤 Low value

heads up: cursor position comes from electron, not the helper

just leaving a breadcrumb — captureSample reads position via screen.getCursorScreenPoint() rather than from the helper payload (since MacCursorEvent.sample only carries button/cursor-type fields). that's a deliberate split, but anyone reading this in isolation will probably do a double take. a one-line comment explaining "helper supplies button + cursor-type telemetry; position is sampled here to stay in lockstep with electron's display coords" would save a future spelunker some time.

🤖 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/macNativeCursorRecordingSession.ts`
around lines 315 - 348, The captureSample function reads cursor position using
screen.getCursorScreenPoint() instead of using the helper payload (the helper's
MacCursorEvent.sample only includes button and cursor-type fields), so add a
one-line clarifying comment at the top of captureSample (near function signature
or before acquiring cursor/bounds) stating that position is sampled here from
Electron to keep coordinates in lockstep with the display while the helper only
supplies button/cursor-type telemetry; reference captureSample and
MacCursorEvent.sample in the comment for clarity.

85-161: 💤 Low value

probe spawn drains stdout but not stderr

minor nit: the requestMacCursorAccessibilityAccess probe pipes stderr but never attaches a data listener to drain it. if the helper happens to emit something chatty on stderr before the ready line shows up on stdout, the pipe buffer could backpressure the helper. realistically this is fine since you SIGTERM right after ready, but adding a no-op drain (or stderr.resume()) is cheap insurance.

🤖 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/macNativeCursorRecordingSession.ts`
around lines 85 - 161, The helper process's stderr is piped but never drained,
which risks backpressure; after spawning the helper in
requestMacCursorAccessibilityAccess (the Promise block where `child =
spawn(helperPath, ...)` is created), attach a no-op drain to stderr (e.g., call
`child.stderr.resume()` or `child.stderr.on('data', () => {})`) so the stderr
pipe is drained and cannot block the helper before the ready event; add this
immediately after the spawn (or next to the child.stdout handling) and ensure it
does not change existing error/exit handling.
src/lib/cursor/nativeCursor.ts (2)

223-248: 💤 Low value

platform: "darwin" baked into the telemetry asset

nit: TELEMETRY_CURSOR_ASSET hardcodes platform: "darwin", but the telemetry-only path is also reachable on linux (and theoretically anywhere provider === "none" with samples). nothing currently keys off asset.platform for rendering, so it's cosmetic today — but if anything ever branches on it, the linux fallback will silently claim to be a mac cursor. consider deriving from process.platform or just leaving it null-ish.

🤖 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/lib/cursor/nativeCursor.ts` around lines 223 - 248,
TELEMETRY_CURSOR_ASSET currently hardcodes platform: "darwin", which can
mislabel telemetry-only cursors on other OSes; update TELEMETRY_CURSOR_ASSET
(and its use in getTelemetryCursorAsset) to derive platform from the runtime
(e.g. process.platform) or set it to null/undefined so it doesn't falsely report
"darwin"; ensure the NativeCursorAsset type allows an optional platform if
needed and propagate the derived/empty platform value when spreading
TELEMETRY_CURSOR_ASSET in getTelemetryCursorAsset.

510-526: 💤 Low value

interpolation ignores cursorType changes between samples

lowkey nit: the "asset changed" guard at line 522 only compares assetId. for telemetry samples both sides are null, so we happily interpolate across a sample where cursorType flips (e.g. arrowtext). the rendered asset stays as activeSample's type for the duration of the interpolation, which can look like the cursor "warps" before swapping glyphs. probably fine in practice since interpolation windows are short (~33ms), but worth knowing if anyone reports a flicker.

🤖 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/lib/cursor/nativeCursor.ts` around lines 510 - 526, The interpolation
guard currently only compares assetId so telemetry samples (both assetId null)
can interpolate across a change in cursorType; update the condition that decides
to skip interpolation (the if block using activeSample, nextSample, assetId,
timeMs, activeIndex) to also treat a cursorType change as a break in continuity:
compare activeSample.cursorType with nextSample.cursorType (or compare the
resolved asset cursorType returned by
getNativeCursorAsset/getTelemetryCursorAsset) and return { asset, sample:
activeSample } when they differ so we don't interpolate across cursor glyph
changes.
electron/ipc/handlers.ts (3)

138-203: 💤 Low value

preview-audio cache directory has no eviction

each prepared track lands in ~/Library/Application Support/<app>/preview-audio/<name>.track-N.<mtime>.m4a and stays there forever. mtime in the filename guarantees no stale-cache hits (nice), but it also guarantees that every re-edit of a clip leaves the previous .m4a behind — same goes for any recording the user later deletes. for someone who records daily this directory just grows.

not a blocker, but a periodic sweep at startup (delete files older than N days, or whose stem doesn't exist anymore) would keep it tidy.

🤖 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 138 - 203, The preview-audio cache
never gets cleaned; modify prepareSupplementalPreviewAudioTrack (or add a
startup helper invoked once) to sweep PREVIEW_AUDIO_DIR and remove stale files:
delete files older than a configurable threshold (e.g., N days) and delete files
whose source media no longer exists (compare parsed stem to source files or
check referenced source mtime parsed from filename), before creating new
outputPath; ensure use of fs.readdir/fs.stat/fs.unlink and handle errors
silently so the sweep is best-effort and does not block normal conversion flow.

1744-1758: 💤 Low value

start error path uses a single kill(), no SIGTERM→SIGKILL escalation

minor consistency thing: the cursor session's killHelperProcess does SIGTERM, then SIGKILL after 500ms if the child is still alive. the catch block here just calls nativeMacCaptureProcess?.kill() (SIGTERM, no follow-up). if the SCK helper is mid-permission-prompt or wedged on AVAssetWriter teardown, you'll leave a zombie. lifting the same escalation helper out and using it in both spots would harmonize behavior.

🤖 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 1744 - 1758, The catch block uses
nativeMacCaptureProcess?.kill() only (no SIGTERM→SIGKILL escalation); replace
that call with the same escalation helper used elsewhere (call and await
killHelperProcess(nativeMacCaptureProcess) or extract a shared function if
needed), then proceed to null out
nativeMacCaptureProcess/nativeMacCaptureTargetPath/nativeMacCaptureRecordingId
and related state and call await stopCursorRecording() as before so both the
start error path and the normal shutdown use identical SIGTERM→SIGKILL behavior.

1761-1803: 💤 Low value

pause range uses electron-side wallclock; helper-side pause happens slightly later

pause-native-mac-recording stamps nativeMacPauseStartedAtMs = Date.now() right after pushing "pause\n" into stdin, and resume symmetrically uses Date.now() as the end. but the helper only actually freezes its writer once it reads that line and acts on it — so the real pause window is shifted by some ms (line-buffered stdin, scheduler latency, whatever swift is doing). cursor telemetry then gets compactPendingCursorTelemetryPauseRanges applied with the wallclock range, which can drop or shift a few samples that should have stayed in the timeline.

it's not user-visible at 33ms sample intervals in most cases, but if you ever want frame-accurate cursor sync over paused MP4s, having the swift helper echo {"event":"paused","timestampMs":...} / "resumed" and using those for the range bounds would be the clean fix. logging this so it's not a mystery later.

🤖 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 1761 - 1803, The pause/resume handlers
(ipcMain.handle "pause-native-mac-recording" and "resume-native-mac-recording")
record pause bounds using Date.now() but the helper only applies pause when it
reads the stdin line, causing a skew; fix by having the Swift helper emit JSON
events like {"event":"paused","timestampMs":...} and
{"event":"resumed","timestampMs":...} on stdout and update the Electron side to
listen to nativeMacCaptureProcess.stdout, parse those events and set
nativeMacPauseStartedAtMs and the resume timestamp from the helper-provided
timestampMs (call completeNativeMacCursorPauseRange() when you receive the
resumed event and set nativeMacIsPaused accordingly); keep a Date.now() fallback
if no helper timestamp arrives within a short timeout and log a warning to aid
debugging.
src/components/launch/LaunchWindow.tsx (1)

285-287: ⚡ Quick win

Cache no-op passthrough transitions before sending IPC.

setHudMouseEventsEnabled now sits on pointer move / enter / wheel paths, so the same boolean gets sent to main over and over while nothing actually changed. A tiny last-value ref here would keep this bridge path much quieter.

Also applies to: 402-406, 588-590, 809-813

🤖 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/launch/LaunchWindow.tsx` around lines 285 - 287, The
setHudMouseEventsEnabled callback is repeatedly sending the same boolean over
IPC; add a local last-value ref (e.g., const hudMouseEnabledRef = useRef<boolean
| undefined>(undefined)) inside the component and update
setHudMouseEventsEnabled to compare the incoming enabled to
hudMouseEnabledRef.current, only calling
window.electronAPI?.setHudOverlayIgnoreMouseEvents when the value changes and
then setting hudMouseEnabledRef.current = enabled; apply the same pattern to the
other identical passthrough callbacks found around the other ranges (the pointer
move/enter/wheel handlers referenced at 402-406, 588-590, and 809-813) so
redundant IPC calls are suppressed.
🤖 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 1006-1051: waitForNativeMacCaptureStart currently attaches a sole
data listener (inspect/onOutput) and calls cleanup() which detaches
stdout/stderr listeners as soon as "recording-started" arrives, leaving no
consumer during the active recording and risking pipe backpressure; fix by
keeping a persistent lightweight drain listener for proc.stdout/proc.stderr that
always appends chunks to nativeMacCaptureOutput (or emit events via a shared
EventEmitter) for the helper's lifetime, and refactor
waitForNativeMacCaptureStart and waitForNativeMacCaptureStop to subscribe to
that shared buffer/emitter instead of owning/removing the only data listener;
ensure cleanup used for early errors only removes temporary watchers (if any)
but does not remove the global drain listener until the helper process actually
exits (proc.once("close")/proc.once("error")).

In
`@electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`:
- Line 104: The forced cast at "return (value as! AXUIElement)" can crash if the
accessibility API returns an unexpected type; change it to a conditional cast
(as?) and return nil on mismatch so the helper returns an optional AXUIElement
(e.g., AXUIElement?) instead of crashing. Update the function signature/return
type if needed, replace the forced unwrap with a safe conditional cast and
handle the fallback path where the value is not an AXUIElement by returning nil.

In
`@electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift`:
- Around line 321-338: The current microphone permission flow blocks
indefinitely because semaphore.wait() is used; change it to use a timed wait
(e.g., let result = semaphore.wait(timeout: .now() + 30)) after calling
AVCaptureDevice.requestAccess(for: .audio) and treat a timeout as a denied
permission by throwing HelperError.permissionDenied("Microphone permission is
required for native microphone capture.") — update the logic around
AVCaptureDevice.requestAccess(for: .audio) and the semaphore handling to check
the wait timeout result and also verify AVCaptureDevice.authorizationStatus(for:
.audio) after the wait, throwing the same permission-denied error on timeout or
non-authorized status.

In `@package.json`:
- Around line 23-24: The package.json "build:mac" script currently omits running
the native helper, risking stale helper binaries; update the "build:mac" script
to invoke the existing "build:native:mac" script before the
TypeScript/Vite/electron-builder steps (i.e., run the npm script named
build:native:mac first, then continue with the tsc, vite build and
electron-builder --mac sequence) so the mac packaging always includes freshly
built native helper binaries.

In `@scripts/build-macos-screencapturekit-helper.mjs`:
- Around line 73-76: Add explicit existence checks for the source artifacts
before calling fs.copyFileSync to avoid raw ENOENTs: verify builtHelperPath and
builtCursorHelperPath exist (e.g., with fs.existsSync) and if not, throw or
processLogger.error with a clear message including the missing path; only then
proceed to copy to localHelperPath, distributablePath, localCursorHelperPath,
distributableCursorHelperPath. Reference the variables builtHelperPath,
builtCursorHelperPath, localHelperPath, distributablePath,
localCursorHelperPath, distributableCursorHelperPath and perform the checks
immediately before the current fs.copyFileSync calls so failures report the
specific missing artifact.

In `@src/components/launch/LaunchWindow.tsx`:
- Around line 296-300: The HUD passthrough state isn't reliably restored: the
useEffect watching isLanguageMenuOpen (and setHudMouseEventsEnabled) enables HUD
mouse events when the menu opens but doesn't disable when it closes, and
handleHudDragPointerEnd never re-enables passthrough after a drag ends. Update
the useEffect in LaunchWindow.tsx to setHudMouseEventsEnabled(true) when
isLanguageMenuOpen becomes true and setHudMouseEventsEnabled(false) in the
cleanup/else branch when it becomes false; similarly, modify
handleHudDragPointerEnd to explicitly call setHudMouseEventsEnabled(false) (or
the equivalent restorePassthrough method) when the drag completes or the pointer
is released outside the toolbar, ensuring both the language menu and drag
handlers symmetrically restore passthrough so the overlay stops eating clicks.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 812-938: The async path in startNativeMacRecordingIfAvailable can
continue after the webcam readiness wait even if the countdown was cancelled;
re-check isCountdownRunActive(countdownRunToken) immediately after any awaited
webcam wait (and again before calling
window.electronAPI.startNativeMacRecording) and bail out (return false) if it
returns false, ensuring you also stop/cleanup any created nativeWebcamRecorder
(recorder.stop()) and reset any state changes (e.g., do not set
recordingId/current state) so the stale async flow cannot flip
setRecording(true) or start a native recording.
- Around line 972-974: The new hardcoded toast in useScreenRecorder.ts (the
toast.info call that reads "Allow Accessibility access for OpenScreen, then
press record again to start the countdown.") must be replaced with a localized
string; update the toast.info call to use your project's i18n helper (e.g.,
i18n.t or the useTranslation hook) instead of a literal English message, add a
new localization key such as "accessibility.allow_and_retry" to the locale
resource files for all supported languages, and import/use the same localization
function used elsewhere in this hook/module so the message is shown in the
user's language.

In `@src/i18n/locales/tr/launch.json`:
- Line 18: Remove the dead "nrLevel" JSON key from the Turkish locale files
where it was added as an empty stub (both occurrences), making the Turkish
locales consistent with other locales that don't have this key; search the repo
for "nrLevel" to confirm no code or other locales reference it, delete those
key/value entries, and run your localization/lint checks to ensure no
missing-key regressions are introduced.

---

Outside diff comments:
In `@src/hooks/useScreenRecorder.ts`:
- Around line 1403-1455: The restart/cancel handlers don't handle the native
macOS recording path, so when nativeMacRecording.current is set the HUD buttons
become no-ops; update the restartRecording flow (and the similar block around
lines 1475-1501) to mirror the native Windows branch: detect
nativeMacRecording.current, set restarting.current = true and
discardRecordingId.current = recordingId.current, call the native mac finalizer
(e.g. finalizeNativeMacRecording(true)) awaited, then await startRecording(),
and finally clear restarting.current in a finally block so native mac recordings
are properly restarted/cancelled; apply this same pattern to any other
restart/cancel handler that currently only checks
nativeWindowsRecording.current.

---

Duplicate comments:
In `@src/i18n/locales/tr/settings.json`:
- Around line 197-199: The "audio.nrLevel" object was accidentally emptied,
removing the noise reduction translations (title, level labels and
descriptions); restore the original keys and values under the "audio.nrLevel"
object so it includes the noise reduction title, each level label and their
descriptions (match the structure used elsewhere, e.g., the original "nrLevel"
pattern referenced in tr/launch.json), and run the same verification script
referenced in tr/launch.json to ensure the restored keys conform to the expected
schema.

---

Nitpick comments:
In `@docs/engineering/macos-native-recorder-roadmap.md`:
- Line 110: Update the roadmap text to avoid conflicting statements about
microphone capture: clarify that the ScreenCaptureKit helper already implements
native mic capture (referencing captureMicrophone and microphoneCaptureDeviceID
behavior) with runtime gating for macOS versions that support it, and remove or
revise the earlier "explicit companion path until it can be moved into the
helper" wording so it no longer implies mic capture remains non-native; keep the
note that AVFoundation webcam composition is still the target end state and that
webcam is currently an Electron-recorded sidecar.
- Line 12: Replace the phrase "Capture macOS system audio through
ScreenCaptureKit" with "Capture system audio through ScreenCaptureKit" to
satisfy the linter; locate and update the exact sentence "Capture macOS system
audio through ScreenCaptureKit" in the macOS native recorder roadmap and make
the wording change.

In `@electron/ipc/handlers.ts`:
- Around line 138-203: The preview-audio cache never gets cleaned; modify
prepareSupplementalPreviewAudioTrack (or add a startup helper invoked once) to
sweep PREVIEW_AUDIO_DIR and remove stale files: delete files older than a
configurable threshold (e.g., N days) and delete files whose source media no
longer exists (compare parsed stem to source files or check referenced source
mtime parsed from filename), before creating new outputPath; ensure use of
fs.readdir/fs.stat/fs.unlink and handle errors silently so the sweep is
best-effort and does not block normal conversion flow.
- Around line 1744-1758: The catch block uses nativeMacCaptureProcess?.kill()
only (no SIGTERM→SIGKILL escalation); replace that call with the same escalation
helper used elsewhere (call and await killHelperProcess(nativeMacCaptureProcess)
or extract a shared function if needed), then proceed to null out
nativeMacCaptureProcess/nativeMacCaptureTargetPath/nativeMacCaptureRecordingId
and related state and call await stopCursorRecording() as before so both the
start error path and the normal shutdown use identical SIGTERM→SIGKILL behavior.
- Around line 1761-1803: The pause/resume handlers (ipcMain.handle
"pause-native-mac-recording" and "resume-native-mac-recording") record pause
bounds using Date.now() but the helper only applies pause when it reads the
stdin line, causing a skew; fix by having the Swift helper emit JSON events like
{"event":"paused","timestampMs":...} and {"event":"resumed","timestampMs":...}
on stdout and update the Electron side to listen to
nativeMacCaptureProcess.stdout, parse those events and set
nativeMacPauseStartedAtMs and the resume timestamp from the helper-provided
timestampMs (call completeNativeMacCursorPauseRange() when you receive the
resumed event and set nativeMacIsPaused accordingly); keep a Date.now() fallback
if no helper timestamp arrives within a short timeout and log a warning to aid
debugging.

In `@electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts`:
- Around line 315-348: The captureSample function reads cursor position using
screen.getCursorScreenPoint() instead of using the helper payload (the helper's
MacCursorEvent.sample only includes button and cursor-type fields), so add a
one-line clarifying comment at the top of captureSample (near function signature
or before acquiring cursor/bounds) stating that position is sampled here from
Electron to keep coordinates in lockstep with the display while the helper only
supplies button/cursor-type telemetry; reference captureSample and
MacCursorEvent.sample in the comment for clarity.
- Around line 85-161: The helper process's stderr is piped but never drained,
which risks backpressure; after spawning the helper in
requestMacCursorAccessibilityAccess (the Promise block where `child =
spawn(helperPath, ...)` is created), attach a no-op drain to stderr (e.g., call
`child.stderr.resume()` or `child.stderr.on('data', () => {})`) so the stderr
pipe is drained and cannot block the helper before the ready event; add this
immediately after the spawn (or next to the child.stdout handling) and ensure it
does not change existing error/exit handling.

In
`@electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`:
- Around line 242-254: The current infinite loop in main (while true { ...
Thread.sleep(...) }) in the cursor helper never handles stdin commands for
pause/resume/stop, so add stdin command handling similar to the ScreenCaptureKit
helper: spawn a background reader that listens to stdin for "pause", "resume",
and "stop" commands and sets a shared state/enum (e.g., isPaused/isRunning or
CursorCaptureState) accessed by the main loop; modify the loop to check that
state (call mouseTracker.pump(), consume/emit only when running or emit reduced
heartbeat when paused), break cleanly on "stop" to emit any final stats via the
existing emit(...) path, and ensure proper synchronization between the reader
and main loop around mouseTracker.pump(), currentCursorType(), and
leftButtonDown() so shutdown is graceful.

In
`@electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift`:
- Around line 392-410: The code uses KVC to set undocumented ScreenCaptureKit
keys via configuration.setValue(..., forKey: ...) (see configuration.setValue,
resolveMicrophoneCaptureDeviceID, nativeMicrophoneEnabled, emit), which is
fragile; wrap all KVC access in a single safe helper (e.g.,
setPrivateCaptureKey(_:value:on:)) that first feature-detects using
supportsNativeMicrophoneCapture and responds(to:) or canRespondToKey, then
performs the set inside an ObjC-safe try/catch (or use
NSObject.performSelector/objc_setAssociated/try? pattern) so it cannot throw at
runtime, log/emit a warning if the key is missing or the set fails, and fall
back to configuration.capturesAudio only; centralize the private key strings as
constants and call this helper from the existing microphone branch instead of
calling setValue directly.

In `@src/components/launch/LaunchWindow.tsx`:
- Around line 285-287: The setHudMouseEventsEnabled callback is repeatedly
sending the same boolean over IPC; add a local last-value ref (e.g., const
hudMouseEnabledRef = useRef<boolean | undefined>(undefined)) inside the
component and update setHudMouseEventsEnabled to compare the incoming enabled to
hudMouseEnabledRef.current, only calling
window.electronAPI?.setHudOverlayIgnoreMouseEvents when the value changes and
then setting hudMouseEnabledRef.current = enabled; apply the same pattern to the
other identical passthrough callbacks found around the other ranges (the pointer
move/enter/wheel handlers referenced at 402-406, 588-590, and 809-813) so
redundant IPC calls are suppressed.

In `@src/i18n/locales/fr/settings.json`:
- Line 12: Remove the extraneous empty "speed": {} key from the JSON object in
the fr settings locale (the entry currently under the zoom group) so the file
matches other locale files; if you intended a placeholder, replace it with a
commented note or an actual translation string, otherwise delete the "speed"
property entirely to avoid dead/unused keys.

In `@src/i18n/locales/zh-TW/settings.json`:
- Line 12: Remove the orphaned empty "speed" key from the zh-TW settings JSON to
match other locales; locate the "speed": {} entry in the settings.json locale
object and delete that key/value pair so the structure aligns with vi and zh-CN
locales and no trailing empty object remains.

In `@src/lib/cursor/nativeCursor.ts`:
- Around line 223-248: TELEMETRY_CURSOR_ASSET currently hardcodes platform:
"darwin", which can mislabel telemetry-only cursors on other OSes; update
TELEMETRY_CURSOR_ASSET (and its use in getTelemetryCursorAsset) to derive
platform from the runtime (e.g. process.platform) or set it to null/undefined so
it doesn't falsely report "darwin"; ensure the NativeCursorAsset type allows an
optional platform if needed and propagate the derived/empty platform value when
spreading TELEMETRY_CURSOR_ASSET in getTelemetryCursorAsset.
- Around line 510-526: The interpolation guard currently only compares assetId
so telemetry samples (both assetId null) can interpolate across a change in
cursorType; update the condition that decides to skip interpolation (the if
block using activeSample, nextSample, assetId, timeMs, activeIndex) to also
treat a cursorType change as a break in continuity: compare
activeSample.cursorType with nextSample.cursorType (or compare the resolved
asset cursorType returned by getNativeCursorAsset/getTelemetryCursorAsset) and
return { asset, sample: activeSample } when they differ so we don't interpolate
across cursor glyph changes.

In `@src/lib/nativeMacRecording.ts`:
- Around line 93-117: Extract the duplicated parsing logic into a shared helper
like parseIdFromSourcePrefix(sourceId, prefix) and update
parseMacWindowIdFromSourceId and parseMacDisplayIdFromSourceId to call it; the
helper should check sourceId startsWith(prefix), split on ":", validate /^\d+$/
and return Number(value) or null, preserving current behavior and tests for edge
cases.
🪄 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: 622cafdd-1f89-4ad7-9895-9fc22c45ba45

📥 Commits

Reviewing files that changed from the base of the PR and between b0293e7 and 303169b.

📒 Files selected for processing (46)
  • .gitignore
  • docs/engineering/macos-native-recorder-roadmap.md
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/main.ts
  • electron/native-bridge/cursor/recording/factory.ts
  • electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts
  • electron/native/README.md
  • electron/native/screencapturekit/Package.swift
  • electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift
  • electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift
  • electron/preload.ts
  • electron/windows.ts
  • package.json
  • scripts/build-macos-screencapturekit-helper.mjs
  • src/components/launch/LaunchWindow.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/hooks/useScreenRecorder.ts
  • src/i18n/locales/ar/launch.json
  • src/i18n/locales/ar/settings.json
  • src/i18n/locales/es/common.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/common.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/ja-JP/common.json
  • src/i18n/locales/ja-JP/settings.json
  • src/i18n/locales/ko-KR/common.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/ru/launch.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/tr/common.json
  • src/i18n/locales/tr/launch.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/vi/common.json
  • src/i18n/locales/vi/launch.json
  • src/i18n/locales/vi/settings.json
  • src/i18n/locales/zh-CN/common.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-TW/common.json
  • src/i18n/locales/zh-TW/settings.json
  • src/lib/cursor/nativeCursor.test.ts
  • src/lib/cursor/nativeCursor.ts
  • src/lib/nativeMacRecording.test.ts
  • src/lib/nativeMacRecording.ts
  • vite.config.ts

Comment thread electron/ipc/handlers.ts Outdated
Comment thread package.json Outdated
Comment thread scripts/build-macos-screencapturekit-helper.mjs
Comment thread src/components/launch/LaunchWindow.tsx
Comment thread src/hooks/useScreenRecorder.ts
Comment thread src/hooks/useScreenRecorder.ts Outdated
Comment thread src/i18n/locales/tr/launch.json 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: 4

♻️ Duplicate comments (1)
src/components/launch/LaunchWindow.tsx (1)

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

Don't always restore click-through at drag end.

Lines 390-395 unconditionally disable HUD mouse events after a drag. If the pointer is released over the toolbar/handle, the next click can pass through to the app underneath until the mouse moves again. Kinda cursed UX for something the user is actively holding.

Possible fix
 	const handleHudDragPointerEnd = (event: React.PointerEvent<HTMLDivElement>) => {
 		dragLastPositionRef.current = null;
 		if (event.currentTarget.hasPointerCapture(event.pointerId)) {
 			event.currentTarget.releasePointerCapture(event.pointerId);
 		}
-		setHudMouseEventsEnabled(false);
+		const hovered = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null;
+		setHudMouseEventsEnabled(
+			isLanguageMenuOpen || Boolean(hovered?.closest("[data-hud-interactive='true']")),
+		);
 	};
🤖 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/launch/LaunchWindow.tsx` around lines 390 - 395, The handler
handleHudDragPointerEnd currently always calls setHudMouseEventsEnabled(false)
which makes clicks pass through if the pointer is released over the HUD
handle/toolbar; change it to only disable HUD mouse events when the pointerup
happened outside the handle/toolbar: keep dragLastPositionRef.current = null and
release pointer capture as-is, then use document.elementFromPoint(event.clientX,
event.clientY) and check whether that element is inside your HUD handle/toolbar
(use existing refs such as hudHandleRef or hudToolbarRef, or add them if
missing) and only call setHudMouseEventsEnabled(false) when the element is not
contained in those refs (otherwise leave mouse events enabled so the next click
won’t pass through).
🧹 Nitpick comments (3)
src/i18n/locales/en/editor.json (1)

43-44: ⚡ Quick win

lowkey could use more guidance on where to grant these permissions

both messages tell users what went wrong but not WHERE to fix it. macOS users might not immediately know where "Privacy & Security" settings live, especially across different OS versions.

consider something like:

  • line 43: "Recording permission denied. Please allow screen recording in System Settings > Privacy & Security."
  • line 44: "Allow Accessibility access for OpenScreen in System Settings > Privacy & Security, then press record again to start the countdown."

not critical but would make the UX smoother for folks who aren't super familiar with macOS permission dialogs.

🤖 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/i18n/locales/en/editor.json` around lines 43 - 44, Update the two
localization strings to explicitly tell macOS users where to grant permissions:
change the "permissionDenied" value to instruct users to allow screen recording
in System Settings > Privacy & Security, and change "accessibilityAllowAndRetry"
to instruct users to allow Accessibility access for OpenScreen in System
Settings > Privacy & Security before pressing record again; update the values
for the keys permissionDenied and accessibilityAllowAndRetry accordingly.
electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift (2)

130-135: 💤 Low value

nit: isTextInputRole misses a couple of common AX roles — AXSearchField and AXSecureTextField (password fields) won't get the "text" cursor. low stakes since they're rarer, but cheap to add:

 func isTextInputRole(_ role: String?) -> Bool {
 	role == "AXTextField" ||
 		role == "AXTextArea" ||
 		role == "AXTextView" ||
+		role == "AXSearchField" ||
+		role == "AXSecureTextField" ||
 		role == "AXComboBox"
 }
🤖 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/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`
around lines 130 - 135, The isTextInputRole function currently checks AX roles
but omits AXSearchField and AXSecureTextField, so password and search fields
won't be treated as text inputs; update the function (isTextInputRole) to also
return true when role equals "AXSearchField" or "AXSecureTextField" by adding
those string checks alongside the existing "AXTextField", "AXTextArea",
"AXTextView", and "AXComboBox" comparisons.

246-258: ⚡ Quick win

sleep + 1ms pump means click events sit in the runloop until next tick

current shape: pump spins the runloop for ~1ms, then Thread.sleep blocks for ~33ms. mouse-down/up events fired during that 32ms gap aren't lost (the mach port queues them), but they're not delivered to the callback until the next pump — which means leftButtonPressed/leftButtonReleased can be attributed to the next sample window instead of the one they actually happened in, slightly skewing click telemetry timing.

nit: replace sleep with a runloop-mode run for the full interval so events are processed in real time:

♻️ proposed refactor
-while true {
-	mouseTracker.pump()
-	let mouseEvents = mouseTracker.consume()
+let intervalSec = Double(intervalMs) / 1000.0
+while true {
+	// Pump the runloop for the whole interval so tap callbacks fire promptly.
+	CFRunLoopRunInMode(.defaultMode, intervalSec, false)
+	let mouseEvents = mouseTracker.consume()
 	emit([
 		"type": "sample",
 		"timestampMs": timestampMs(),
 		"cursorType": currentCursorType(),
 		"leftButtonDown": leftButtonDown(),
 		"leftButtonPressed": mouseEvents.leftDownCount > 0,
 		"leftButtonReleased": mouseEvents.leftUpCount > 0,
 	])
-	Thread.sleep(forTimeInterval: Double(intervalMs) / 1000.0)
 }

(you can then drop the pump() method entirely.)

🤖 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/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`
around lines 246 - 258, The loop currently calls mouseTracker.pump() (which
spins the runloop ~1ms) then Thread.sleep(...) for the remainder, causing input
events to be delivered only on the next pump and skewing
leftButtonPressed/leftButtonReleased timing; replace the pump+sleep pattern by
running the CFRunLoop/RunLoop for the entire interval (e.g.
RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow:
intervalMs/1000.0))) so events are processed continuously between samples, keep
using mouseTracker.consume() and emit(... timestampMs(), cursorType,
leftButtonDown/Pressed/Released) as before, and you can remove the pump() helper
after switching to the runloop-run approach.
🤖 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/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`:
- Around line 182-186: The conversion in accessibilityPointForMouse uses the max
Y across all screens which breaks AX coordinates on multi-monitor setups; change
it to flip Y relative to the primary display height (use
NSScreen.screens.first?.frame.height fallback to NSScreen.main?.frame.height)
instead of the global max so the returned CGPoint aligns with the AX coordinate
system used by AXUIElementCopyElementAtPosition; update
accessibilityPointForMouse to compute primaryScreenHeight and return CGPoint(x:
mouse.x, y: primaryScreenHeight - mouse.y).
- Around line 21-51: The callback in start() currently ignores disable events so
the CGEvent tap can die silently; update the callback passed to
CGEvent.tapCreate to handle kCGEventTapDisabledByTimeout and
kCGEventTapDisabledByUserInput by retrieving the MouseButtonTracker from
userInfo (as you already do) and calling CGEvent.tapEnable(tap:
tracker.eventTap, enable: true) (guarding that tracker.eventTap is non-nil) to
re-enable the tap, then continue processing normal mouse events; ensure you use
the same
Unmanaged<MouseButtonTracker>.fromOpaque(userInfo).takeUnretainedValue() path
and reference eventTap (the stored property on MouseButtonTracker) so the
re-enable call can succeed.

In `@src/components/launch/LaunchWindow.tsx`:
- Around line 405-409: The pointer-move handler can disable HUD capture when the
pointer crosses transparent space between the trigger and the portaled language
menu; update the onPointerMove logic in LaunchWindow.tsx (the anonymous handler
that calls setHudMouseEventsEnabled) to also keep capture enabled while the
language menu is open: compute shouldCapture as
Boolean(target?.closest("[data-hud-interactive='true']") || isLanguageMenuOpen)
so setHudMouseEventsEnabled(true) whenever isLanguageMenuOpen is true (or the
existing closest check passes), ensuring the portaled menu remains interactive
while open.

In `@src/i18n/locales/tr/settings.json`:
- Line 197: Remove the orphaned "audio": {} key from the Turkish settings.json
(the empty "audio" object shown in the diff); delete that line so the locale
matches the English baseline and other locales, and ensure the resulting JSON
remains valid (adjust commas if necessary).

---

Duplicate comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 390-395: The handler handleHudDragPointerEnd currently always
calls setHudMouseEventsEnabled(false) which makes clicks pass through if the
pointer is released over the HUD handle/toolbar; change it to only disable HUD
mouse events when the pointerup happened outside the handle/toolbar: keep
dragLastPositionRef.current = null and release pointer capture as-is, then use
document.elementFromPoint(event.clientX, event.clientY) and check whether that
element is inside your HUD handle/toolbar (use existing refs such as
hudHandleRef or hudToolbarRef, or add them if missing) and only call
setHudMouseEventsEnabled(false) when the element is not contained in those refs
(otherwise leave mouse events enabled so the next click won’t pass through).

---

Nitpick comments:
In
`@electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift`:
- Around line 130-135: The isTextInputRole function currently checks AX roles
but omits AXSearchField and AXSecureTextField, so password and search fields
won't be treated as text inputs; update the function (isTextInputRole) to also
return true when role equals "AXSearchField" or "AXSecureTextField" by adding
those string checks alongside the existing "AXTextField", "AXTextArea",
"AXTextView", and "AXComboBox" comparisons.
- Around line 246-258: The loop currently calls mouseTracker.pump() (which spins
the runloop ~1ms) then Thread.sleep(...) for the remainder, causing input events
to be delivered only on the next pump and skewing
leftButtonPressed/leftButtonReleased timing; replace the pump+sleep pattern by
running the CFRunLoop/RunLoop for the entire interval (e.g.
RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow:
intervalMs/1000.0))) so events are processed continuously between samples, keep
using mouseTracker.consume() and emit(... timestampMs(), cursorType,
leftButtonDown/Pressed/Released) as before, and you can remove the pump() helper
after switching to the runloop-run approach.

In `@src/i18n/locales/en/editor.json`:
- Around line 43-44: Update the two localization strings to explicitly tell
macOS users where to grant permissions: change the "permissionDenied" value to
instruct users to allow screen recording in System Settings > Privacy &
Security, and change "accessibilityAllowAndRetry" to instruct users to allow
Accessibility access for OpenScreen in System Settings > Privacy & Security
before pressing record again; update the values for the keys permissionDenied
and accessibilityAllowAndRetry accordingly.
🪄 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: 24e035f8-7fbe-483a-83dd-20d705a7c061

📥 Commits

Reviewing files that changed from the base of the PR and between d416335 and 40ebe72.

📒 Files selected for processing (20)
  • electron/ipc/handlers.ts
  • electron/native/screencapturekit/Sources/OpenScreenMacOSCursorHelper/main.swift
  • electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift
  • package.json
  • scripts/build-macos-screencapturekit-helper.mjs
  • src/components/launch/LaunchWindow.tsx
  • src/hooks/useScreenRecorder.ts
  • src/i18n/locales/ar/editor.json
  • src/i18n/locales/en/editor.json
  • src/i18n/locales/es/editor.json
  • src/i18n/locales/fr/editor.json
  • src/i18n/locales/ja-JP/editor.json
  • src/i18n/locales/ko-KR/editor.json
  • src/i18n/locales/ru/editor.json
  • src/i18n/locales/tr/editor.json
  • src/i18n/locales/tr/launch.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/vi/editor.json
  • src/i18n/locales/zh-CN/editor.json
  • src/i18n/locales/zh-TW/editor.json
✅ Files skipped from review due to trivial changes (8)
  • src/i18n/locales/zh-CN/editor.json
  • src/i18n/locales/fr/editor.json
  • src/i18n/locales/ru/editor.json
  • src/i18n/locales/ar/editor.json
  • src/i18n/locales/ko-KR/editor.json
  • src/i18n/locales/vi/editor.json
  • src/i18n/locales/es/editor.json
  • src/i18n/locales/zh-TW/editor.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • package.json
  • scripts/build-macos-screencapturekit-helper.mjs
  • electron/native/screencapturekit/Sources/OpenScreenScreenCaptureKitHelper/main.swift
  • electron/ipc/handlers.ts
  • src/hooks/useScreenRecorder.ts

Comment thread src/components/launch/LaunchWindow.tsx
Comment thread src/i18n/locales/tr/settings.json Outdated
@auberginewly
Copy link
Copy Markdown
Contributor

@siddharthvaddem I tested it on mac, all seems ok. I just can't test the camera. @auberginewly if you want to test the camera that would be of help ;-)

ok

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@auberginewly except for camera testing, all is good on my side, were you able to do a camera test?
@siddharthvaddem do you know contributors that have mac + webcam?
Aside from that cam checks others checks are ok.

@siddharthvaddem
Copy link
Copy Markdown
Owner

siddharthvaddem commented May 15, 2026

@EtienneLescot Do you mean an external webcam or the one built in with mac. I do not have an external webcam unfortunately.

Tagging @FabLrc @marcgabe15 for visibility

@auberginewly
Copy link
Copy Markdown
Contributor

auberginewly commented May 16, 2026

Tested on macOS — here's what I found:

✅ Camera: Works fine, no issues.

🐛 Cursor goes off-screen (external display): When recording on an external monitor, the cursor overlay moves outside the screen boundary. Not sure if this also happens on a single display — could be a coordinate mapping issue specific to multi-display setups. (screenshot below)

image

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@auberginewly Thank you for the cam test !! Much appreciated!
Concerning the cursor overlay moving outside the screen boundary.
I am no sure to understand exactly the issue. I have only one screen so I can't reproduce. But your screenshot does not seem wrong...
Could you explain a bit more, maybe it is me that do not understand the point.

@auberginewly
Copy link
Copy Markdown
Contributor

@auberginewly Thank you for the cam test !! Much appreciated! Concerning the cursor overlay moving outside the screen boundary. I am no sure to understand exactly the issue. I have only one screen so I can't reproduce. But your screenshot does not seem wrong... Could you explain a bit more, maybe it is me that do not understand the point.

In the post-recording editor, the magnified cursor overlay overflows beyond the video canvas boundary into the Openscreen background area. The cursor overlay should be clipped to the video canvas — when the cursor reaches the edge of the recorded footage, the overlay should stop at the boundary rather than spilling into the background region, to ensure a clean viewing experience.

@auberginewly
Copy link
Copy Markdown
Contributor

@auberginewly Thank you for the cam test !! Much appreciated! Concerning the cursor overlay moving outside the screen boundary. I am no sure to understand exactly the issue. I have only one screen so I can't reproduce. But your screenshot does not seem wrong... Could you explain a bit more, maybe it is me that do not understand the point.

In the post-recording editor, the magnified cursor overlay overflows beyond the video canvas boundary into the Openscreen background area. The cursor overlay should be clipped to the video canvas — when the cursor reaches the edge of the recorded footage, the overlay should stop at the boundary rather than spilling into the background region, to ensure a clean viewing experience.

Found the root cause — in projectNativeCursorToStage, the stage coordinate isn't clamped to the video canvas bounds after the global projection. Would it be ok if I submit a fix for this?

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

EtienneLescot commented May 16, 2026

@auberginewly of course, you can make a PR on my branch !
Simply i am not sure to understant the use case. on your screenshot I see a cursor visible in the padding area.
Not sure that it is a "bad behavior" you know what I mean ? I might be wrong but, to me it seems cleaner this way instead of having a cropped cursor when user show elements at the edges.

@auberginewly
Copy link
Copy Markdown
Contributor

@auberginewly of course, you can make a PR on my branch !
Simply i am not sure to understant the use case. on your screenshot I see a cursor visible in the padding area.
Not sure that it is a "bad behavior" you know what I mean ? I might be wrong but, to me it seems cleaner this way instead of having a cropped cursor when user show elements at the edges.

What I mean is: when the cursor is enlarged, part of it overflows outside the video canvas. My suggestion is to apply a clip mask to the video canvas, so the portion of the cursor that goes outside is hidden — not removing the cursor entirely. This way the cursor still appears naturally at the edges, just clipped by the canvas boundary, like CSS overflow: hidden.

I'll start working on a fix for this now.

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

EtienneLescot commented May 16, 2026

@EtienneLescot Do you mean an external webcam or the one built in with mac. I do not have an external webcam unfortunately.

Tagging @FabLrc @marcgabe15 for visibility

@siddharthvaddem I meant whatever camere built-in or external.
it seems like @auberginewly tested and that it was ok though.

Concerning the cursor overflowing in the padding area, I find that it is better like that (with overflow).
Also this is the way it is working in Windows and it this ok this way.
What do you guys think?

Tagging @FabLrc @marcgabe15 for visibility

@auberginewly
Copy link
Copy Markdown
Contributor

@EtienneLescot Do you mean an external webcam or the one built in with mac. I do not have an external webcam unfortunately.
Tagging @FabLrc @marcgabe15 for visibility

@siddharthvaddem I meant whatever camere built-in or external. it seems like @auberginewly tested and that it was ok though.

Concerning the cursor overflowing in the padding area, I find that it is better like that (with overflow). Also this is the way it is working in Windows and it this ok this way. What do you guys think?

Tagging @FabLrc @marcgabe15 for visibility

cursor overflow in the padding area — I understand you prefer keeping the overflow behavior, and it aligns with Windows' default behavior. No issue there.

Quick update on my side: I cleaned up unnecessary mouse movement trails across multi-monitor setups and fixed the cursor jump/disconnect issue when moving between displays.

One enhancement suggestion: could we make "cursor constrained to canvas / allowed to overflow" a configurable option?

This gives users flexibility:
Overflow allowed — freeform, matches Windows behavior (your preference)
Clamped to canvas — cleaner, better for presentations/screencasts
Implementation-wise, this would just need a conditional check against canvas bounds (e.g., overflow: visible vs clip behavior). If the direction looks good, I can branch off yours and open a PR to implement this.

What do you think?

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@EtienneLescot Do you mean an external webcam or the one built in with mac. I do not have an external webcam unfortunately.
Tagging @FabLrc @marcgabe15 for visibility

@siddharthvaddem I meant whatever camere built-in or external. it seems like @auberginewly tested and that it was ok though.
Concerning the cursor overflowing in the padding area, I find that it is better like that (with overflow). Also this is the way it is working in Windows and it this ok this way. What do you guys think?
Tagging @FabLrc @marcgabe15 for visibility

cursor overflow in the padding area — I understand you prefer keeping the overflow behavior, and it aligns with Windows' default behavior. No issue there.

Quick update on my side: I cleaned up unnecessary mouse movement trails across multi-monitor setups and fixed the cursor jump/disconnect issue when moving between displays.

One enhancement suggestion: could we make "cursor constrained to canvas / allowed to overflow" a configurable option?

This gives users flexibility: Overflow allowed — freeform, matches Windows behavior (your preference) Clamped to canvas — cleaner, better for presentations/screencasts Implementation-wise, this would just need a conditional check against canvas bounds (e.g., overflow: visible vs clip behavior). If the direction looks good, I can branch off yours and open a PR to implement this.

What do you think?

As for me, I am ok with the option solution but we should also have that checkbox option in windows then.
I let you make the PR so that I can review it.

@auberginewly
Copy link
Copy Markdown
Contributor

there's a little bug and i'm trying to fix it

@siddharthvaddem
Copy link
Copy Markdown
Owner

siddharthvaddem commented May 16, 2026

everything looks good on my end - one nit is that the cursor type does not change between a pointer to insert text/ resize/ drag and so on, it remains the default so I am wondering if that is the intended behavior? If so - should we think about making that dynamic? and also support for custom cursors https://sweezy-cursors.com/

let me check if this is a regression from my local setup (not sure what I am doing wrong, but it seems like the cursor remains the default pointer type)

@auberginewly
Copy link
Copy Markdown
Contributor

现在我是已经把光标 overflow 给删掉了 修了多余运动轨迹采集的 bug 我来加一个可选 overflow or not 的开关

After more testing I've reconsidered the cursor-overflow approach:

Removed the overflow clipping. The hard clip-path/canvas clipping that confined the cursor to the video bounds is gone — clipping the native cursor at the edge looked worse than letting it sit slightly outside in some cases.
Fixed the real root cause: redundant motion-trail sampling. The phantom trail wasn't really an overflow problem — it came from stale out-of-bounds samples being collected and fed into smoothing. That's now fixed at the sampling layer, so the trail/ghosting is gone without needing to clip.
Next: make overflow opt-in. I'll add an optional "clip cursor to canvas / allow overflow" toggle so the behavior is configurable instead of hard-coded, rather than forcing one choice on everyone.
Will push the toggle commit shortly.

@auberginewly
Copy link
Copy Markdown
Contributor

@siddharthvaddem @EtienneLescot

I've added a set of macOS cursor fixes + an optional toggle on top of this native capture/cursor pipeline. The PR is opened against Etienne's fork: EtienneLescot/openscreen#2

Scope: multi-display ghost cursor/trail, cursor-overflow clipping, preview/export size normalization, lost rounded corners & zoom over-crop fixes, an optional "Clip to Canvas" toggle, and i18n for the cursor settings section across 11 locales.

Plan: merge into @EtienneLescot's feat/macos-native-capture-pipeline branch first, then it lands in main together with this #573. Done and ready for review — thanks! 🙏

@siddharthvaddem
Copy link
Copy Markdown
Owner

siddharthvaddem commented May 18, 2026

feel free to tag me again when everything has been cleared out and is ready to merge

everything looks good on my end - one nit is that the cursor type does not change between a pointer to insert text/ resize/ drag and so on, it remains the default so I am wondering if that is the intended behavior? If so - should we think about making that dynamic? and also support for custom cursors https://sweezy-cursors.com/

let me check if this is a regression from my local setup (not sure what I am doing wrong, but it seems like the cursor remains the default pointer type)

and putting this here again just in case it got missed in the thread

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

EtienneLescot commented May 18, 2026

feel free to tag me again when everything has been cleared out and is ready to merge

everything looks good on my end - one nit is that the cursor type does not change between a pointer to insert text/ resize/ drag and so on, it remains the default so I am wondering if that is the intended behavior? If so - should we think about making that dynamic? and also support for custom cursors https://sweezy-cursors.com/

let me check if this is a regression from my local setup (not sure what I am doing wrong, but it seems like the cursor remains the default pointer type)

and putting this here again just in case it got missed in the thread

@siddharthvaddem the cursor is changing correctly on my end, strange, what is your macOS version?

@auberginewly
Copy link
Copy Markdown
Contributor

auberginewly commented May 18, 2026

feel free to tag me again when everything has been cleared out and is ready to merge

everything looks good on my end - one nit is that the cursor type does not change between a pointer to insert text/ resize/ drag and so on, it remains the default so I am wondering if that is the intended behavior? If so - should we think about making that dynamic? and also support for custom cursors https://sweezy-cursors.com/

let me check if this is a regression from my local setup (not sure what I am doing wrong, but it seems like the cursor remains the default pointer type)
and putting this here again just in case it got missed in the thread

@siddharthvaddem the cursor is changing correctly on my end, strange, what is your macOS version?

is alright on my end, my macOS version is 26.4
image

EtienneLescot and others added 14 commits May 18, 2026 12:19
…iew and export

- Add nativeCursorClipRef div (outside preserve-3d) with CSS inset() clip-path that
  tracks the camera-transformed video boundary, including border-radius
- Add cameraAwareMaskRect() in FrameRenderer that computes the same boundary for
  Canvas 2D clip in the export path; remove stage-clamping so rounded corners match
  the preview's inset() behavior when zoom/pan pushes the mask off-stage
- Cache maskBorderRadius in LayoutCache so both shadow and direct composite paths
  can apply camera-aware rounded clipping
- Fix double mask.x offset introduced by nativeCursorMaskRef; replace mask div with
  clip-path on the outer wrapper
- Normalize cursor size relative to maskRect.width so preview and export scale match
- Clip cursor to canvas boundary and hide on non-recorded display
- Wire cursorClipToBounds flag through FrameRenderConfig and VideoExporter
Add a cursor.clipToBounds toggle to the Settings panel (default on) that controls
whether the native cursor is clipped to the video canvas boundary in both preview
and export. Wire up 11 locale files (ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi,
zh-CN, zh-TW) with the new cursor settings section.
… i18n

- Add aria-label to cursorClipToBounds Switch so screen readers announce the control
- Mirror composite3D 3D transform onto nativeCursorClipRef so the cursor clip layer
  rotates with the video during 3D zoom regions (cursor stays outside preserve-3d
  so clip-path continues to work; only the transform string is mirrored)
- Fix vi cursor.motionBlur: "Mờ chuyển động" → "Làm mờ chuyển động" to match
  effects.motionBlur phrasing
- Fix zh-TW cursor.motionBlur: "運動模糊" → "動態模糊" to match effects.motionBlur
@EtienneLescot EtienneLescot force-pushed the feat/macos-native-capture-pipeline branch from 90a0447 to 8516707 Compare May 18, 2026 10:20
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.

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/VideoPlayback.tsx (1)

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

Clear the old supplemental audio immediately on clip swap.

lowkey risky: when videoPath changes from one non-empty value to another, the previous supplemental <audio> stays mounted until preparePreviewAudioTrack() resolves. This effect resets the main video, but not the old audio element, so the previous track can briefly keep playing or get resynced against the new clip.

🛠️ quick fix
 		useEffect(() => {
+			supplementalAudioRef.current?.pause();
+			if (supplementalAudioRef.current) {
+				supplementalAudioRef.current.currentTime = 0;
+			}
+			setSupplementalAudioPath(null);
+
 			if (!videoPath) {
 				lastResolvedDurationRef.current = null;
 				isResolvingDurationRef.current = false;
 				setVideoReady(false);
-				setSupplementalAudioPath(null);
 				return;
 			}
🤖 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/VideoPlayback.tsx` around lines 1045 - 1094, When
videoPath changes we must clear any previous supplemental audio immediately to
avoid the old <audio> staying mounted; in the useEffect that watches videoPath
(the one that calls window.electronAPI.preparePreviewAudioTrack), call
setSupplementalAudioPath(null) synchronously at the start (before the async
preparePreviewAudioTrack call and before returning early) so the old audio
unmounts instantly, and keep the existing cancelled flag and .then/.catch logic
to set the new path when the async call resolves.
🤖 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.

Outside diff comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1045-1094: When videoPath changes we must clear any previous
supplemental audio immediately to avoid the old <audio> staying mounted; in the
useEffect that watches videoPath (the one that calls
window.electronAPI.preparePreviewAudioTrack), call
setSupplementalAudioPath(null) synchronously at the start (before the async
preparePreviewAudioTrack call and before returning early) so the old audio
unmounts instantly, and keep the existing cancelled flag and .then/.catch logic
to set the new path when the async call resolves.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c54c4e9b-b7f9-400d-b956-bc70ac1f52ab

📥 Commits

Reviewing files that changed from the base of the PR and between 7675895 and 90a0447.

📒 Files selected for processing (20)
  • electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/components/video-editor/types.ts
  • src/components/video-editor/videoPlayback/layoutUtils.ts
  • src/i18n/locales/ar/settings.json
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/ja-JP/settings.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/vi/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-TW/settings.json
  • src/lib/exporter/frameRenderer.ts
  • src/lib/exporter/gifExporter.ts
  • src/lib/exporter/videoExporter.ts
✅ Files skipped from review due to trivial changes (6)
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/ar/settings.json
  • src/i18n/locales/es/settings.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/zh-TW/settings.json
  • src/i18n/locales/vi/settings.json
  • electron/native-bridge/cursor/recording/macNativeCursorRecordingSession.ts
  • src/i18n/locales/ja-JP/settings.json

@EtienneLescot
Copy link
Copy Markdown
Collaborator Author

@siddharthvaddem I just merged @auberginewly contribution.
Seems all ok.
Should be good to merge!

@siddharthvaddem siddharthvaddem merged commit e7ca9ec into siddharthvaddem:main May 18, 2026
9 checks passed
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