Skip to content

feat: add phone remote camera and microphone#426

Open
xsourabhsharma wants to merge 4 commits intowebadderallorg:mainfrom
xsourabhsharma:feature/phone-remote-media
Open

feat: add phone remote camera and microphone#426
xsourabhsharma wants to merge 4 commits intowebadderallorg:mainfrom
xsourabhsharma:feature/phone-remote-media

Conversation

@xsourabhsharma
Copy link
Copy Markdown

@xsourabhsharma xsourabhsharma commented May 5, 2026

Summary

  • Add phone-to-laptop remote media pairing with session code/link and QR code
  • Stream phone camera and microphone to the laptop over WebRTC for screen recordings
  • Use phone mic as the default narrator source while keeping laptop mic optional
  • Add remote preview, reconnect/disconnect handling, and FFmpeg-missing dev fallback

Testing

  • npm run test
  • npm run i18n:check
  • npx tsc --noEmit
  • npx vite build --config vite.config.ts
  • Manual Windows test with phone camera + mic recording/export

Refs #418

Summary by CodeRabbit

  • New Features

    • Phone-remote: use your phone as camera & mic with join-code/QR pairing, mobile join page, secure join links/tunneling, preview, flip/mute, auto-connect, and seamless laptop/phone switching; recorder mixes phone audio.
  • Bug Fixes

    • Recording validation now handles missing FFmpeg gracefully (returns size-only result).
  • Tests

    • Added tests for recording validation and phone-remote session flows.
  • Localization

    • Added locale strings for motion/zoom effects and cursor tuning.

Add a phone-to-laptop remote media workflow for recording the laptop screen with phone camera overlay and phone microphone audio.

Refs webadderallorg#418
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e34589bc-f496-4a9b-a916-a3a477252637

📥 Commits

Reviewing files that changed from the base of the PR and between 8b7614c and e278c31.

📒 Files selected for processing (2)
  • package.json
  • vite.config.ts

📝 Walkthrough

Walkthrough

Adds a phone-remote WebRTC system: types and renderer API, an in-memory session store, an HTTP bridge (optional cloudflared tunnel) and mobile client page, main-process IPC + preload wiring, a renderer hook and LaunchWindow integration for remote media, QR encoder, FFmpeg diagnostics fallback, tests, CSS, and an Electron build entry tweak.

Changes

Phone Remote (single cohesive DAG)

Layer / File(s) Summary
Types
electron/phoneRemote/types.ts, electron/electron-env.d.ts
New phone-remote types: session/status unions, signal/status message shapes, signal envelopes, public session/join URL shapes; renderer ambient window.electronAPI extended with phone-remote methods and subscriptions.
Session Store
electron/phoneRemote/sessionStore.ts, electron/phoneRemote/sessionStore.test.ts
In-memory session store with pairing codes, TTL pruning, indexed laptop/phone signal enqueueing, status updates, event subscriptions; tests for creation, ownership validation, event emission, and TTL refresh.
HTTP Bridge / Tunnel
electron/phoneRemote/server.ts
Lazily-started HTTP server serving mobile page, session lookup, signal polling/ingest, status updates; JSON body size limit, payload validation, optional cloudflared tunnel spawn/detection, periodic prune, and exports getPhoneRemoteJoinUrls and cleanupPhoneRemoteServer.
Mobile Client Page
electron/phoneRemote/mobilePage.ts
Self-contained mobile HTML+JS page: preview, media acquisition with fallbacks, WebRTC peer creation, polling-based signaling, heartbeat status posts, flip/mute controls, lifecycle handlers, and join UI.
IPC Handlers / Preload / Main
electron/ipc/register/phoneRemote.ts, electron/ipc/handlers.ts, electron/preload.ts, electron/main.ts
Main-process registration of phone-remote handlers (create/end session, send signal), store-event forwarding to owner webContents, signal validation/parsing, preload exposes invoke/on APIs, and main calls cleanupPhoneRemoteServer() on before-quit.
Renderer Hook / Integration
src/hooks/usePhoneRemoteMedia.ts, src/hooks/useScreenRecorder.ts, src/components/launch/LaunchWindow.tsx, src/components/launch/LaunchWindow.module.css
Hook manages session lifecycle, RTCPeerConnection, canvas-driven video capture and audio routing; screen-recorder accepts remote streams and mixes audio; LaunchWindow UI adds pairing panel, QR/link, join/copy, mic/camera selection, preview wiring, and styles.

Utilities, Tests & Build

Layer / File(s) Summary
QR Encoder
src/lib/simpleQr.ts
New QR encoder that builds a QR module matrix, computes ECC, applies masks, scores candidates, and returns an SVG path + size for a given string.
FFmpeg diagnostics
electron/ipc/recording/diagnostics.ts, electron/ipc/recording/diagnostics.test.ts
validateRecordedVideo now handles missing-FFmpeg by returning { fileSizeBytes, durationSeconds: null } instead of throwing; added a test mocking missing FFmpeg.
Vite / package entry
vite.config.ts, package.json
Electron main build output filenames changed from .cjs to .mjs and formats: ["cjs"] removed; package.json main updated to dist-electron/main.mjs.
Minor editor/tests/localization
src/components/video-editor/*, src/i18n/locales/*
Formatting and test expectation tweaks, and multiple i18n locale additions/adjustments across language files.

Sequence Diagram

sequenceDiagram
    participant Phone as Mobile Browser (Phone Client)
    participant Tunnel as Cloudflared Tunnel
    participant Server as Electron HTTP Server
    participant Store as Session Store
    participant Renderer as Renderer Process
    participant AppPeer as App Recording/Peer

    Phone->>Server: GET /phone (mobile page)
    Server-->>Phone: HTML + JS (mobile client)
    Phone->>Phone: getUserMedia / acquire tracks
    Phone->>Server: POST /api/phone-remote/session { code? }
    Server->>Store: lookup/create session
    Store-->>Server: session public payload
    Server-->>Phone: { session, joinUrl, ... }

    Phone->>Phone: create RTCPeerConnection + local tracks
    Phone->>Server: POST /api/phone-remote/signal (offer / ice)
    Server->>Store: addPhoneSignal()
    Store->>Renderer: emit "phone-remote-signal"
    Renderer->>AppPeer: forward signal via IPC to in-app peer

    AppPeer->>Server: phone-remote:send-signal (answer / ice)
    Server->>Store: addLaptopSignal()
    Store->>Renderer: emit "phone-remote-signal"
    Renderer-->>Phone: forwarded answer/ice via server polling

    Phone->>Server: GET /api/phone-remote/signals?after=idx
    Server->>Store: getLaptopSignalsSince(after)
    Store-->>Server: [signals], nextIndex
    Server-->>Phone: { signals, nextIndex }
    Phone->>Phone: apply remote descriptions / candidates
    Phone->>Server: POST /api/phone-remote/status {status, hasAudio,...}
    Server->>Store: updatePhoneRemoteStatus()
    Store->>Renderer: emit "phone-remote-status"
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

"I hopped through signals, bytes, and code,
A QR, a tunnel, and a tiny node,
Tracks hum, ICE flies, statuses parade,
A rabbit's cheer for the remote parade! 🐇🎥"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add phone remote camera and microphone' is clear, specific, and accurately summarizes the main feature addition in the changeset.
Description check ✅ Passed The PR description covers key aspects (summary, testing, and related issue reference), though it lacks formal template structure with all required sections like Screenshots/Video and Checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@xsourabhsharma xsourabhsharma marked this pull request as ready for review May 5, 2026 06:51
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: 6

🧹 Nitpick comments (6)
electron/ipc/recording/diagnostics.ts (1)

222-224: ⚡ Quick win

Harden missing-FFmpeg detection to avoid message-coupling.

On Line 223, fallback is keyed to a specific message string. If getFfmpegBinaryPath() wording changes, this path silently stops working and dev recordings regress. Prefer a tolerant match (or a structured error code from the source).

Proposed localized tweak
-		const message = error instanceof Error ? error.message : String(error);
-		if (message.includes("FFmpeg binary is unavailable")) {
+		const message = error instanceof Error ? error.message : String(error);
+		if (/ffmpeg\s+binary\s+is\s+unavailable/i.test(message)) {
🤖 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/recording/diagnostics.ts` around lines 222 - 224, The check in
diagnostics.ts that detects missing FFmpeg is tightly coupled to an exact
message string; update the detection logic around the block that builds
"message" (where it currently does message.includes("FFmpeg binary is
unavailable")) to use a tolerant match or structured code: first, if the thrown
error exposes a code (e.g., error.code === 'FFMPEG_UNAVAILABLE' or similar)
prefer that; otherwise replace the exact string check with a case-insensitive,
fuzzy test such as testing for /ffmpeg/i and one of "unavail|missing|not found"
(or using message.toLowerCase().includes("ffmpeg") &&
(message.includes("unavailable") || message.includes("not found") ||
message.includes("missing"))) so the detection still fires if wording changes.
src/hooks/usePhoneRemoteMedia.ts (2)

334-338: 💤 Low value

Consider adding fallback ICE servers for reliability.

The peer connection uses only stun:stun.l.google.com:19302. While this is a reliable public STUN server, having a single ICE server can cause connectivity failures if it's unreachable. Consider adding backup STUN servers or making the ICE server list configurable.

♻️ Optional: Add backup STUN servers
 const peer = new RTCPeerConnection({
-	iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
+	iceServers: [
+		{ urls: "stun:stun.l.google.com:19302" },
+		{ urls: "stun:stun1.l.google.com:19302" },
+		{ urls: "stun:stun2.l.google.com:19302" },
+	],
 });
🤖 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/usePhoneRemoteMedia.ts` around lines 334 - 338, The
RTCPeerConnection created in createPeer (inside the useCallback) currently uses
only "stun:stun.l.google.com:19302"; update the ICE configuration to include
additional fallback STUN servers or read the iceServers list from a configurable
source (env/config) so connectivity is more robust: modify the RTCPeerConnection
constructor call in createPeer to use an array of multiple STUN (and optionally
TURN) entries or inject a configurable iceServers value so callers can override
the list.

476-521: 💤 Low value

Async effect callback may access stale refs after cleanup.

The onPhoneRemoteSignal callback is async, and if the effect's cleanup runs while the callback is mid-execution, subsequent peerRef.current access could reference a closed or null peer. While the if (!peer) guard at line 487 helps, the peer could be closed between the check and usage.

This is a common pattern in React hooks and the current guards are reasonable, but for critical WebRTC operations you may want to track a mounted flag.

🤖 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/usePhoneRemoteMedia.ts` around lines 476 - 521, The async
onPhoneRemoteSignal callback can observe stale/closed refs after the effect
cleanup; to fix, introduce an "active" (or "mounted") boolean captured by the
effect and flipped to false in its cleanup, and inside the callback bail out
early if active is false (and/or capture local snapshots of sessionRef.current
and peerRef.current into consts at the start of the callback and re-check active
before doing any await-bound operations). Update the useEffect that registers
window.electronAPI.onPhoneRemoteSignal (and the code paths handling "answer" and
"ice-candidate") to check the active flag before calling
peer.setRemoteDescription, peer.addIceCandidate, mutating
pendingRemoteIceCandidatesRef, or calling setStatus/setStatusDetail; reference
symbols: useEffect/onPhoneRemoteSignal, sessionRef, peerRef,
pendingRemoteIceCandidatesRef, setStatus, setStatusDetail.
electron/ipc/register/phoneRemote.ts (1)

84-101: 💤 Low value

Handler registration lacks idempotency guard.

The subscribed flag only prevents duplicate store subscriptions, but the ipcMain.handle() calls on lines 103, 127, and 131 will throw if registerPhoneRemoteHandlers() is invoked more than once. While the current call site in registerIpcHandlers() appears to be invoked only once during app.whenReady(), the guard pattern is incomplete.

Consider either wrapping the handler registrations in the same guard, or using ipcMain.removeHandler() before re-registering to make this function idempotent.

♻️ Optional: Make handler registration idempotent
 export function registerPhoneRemoteHandlers() {
 	if (!subscribed) {
 		subscribed = true;
 		subscribePhoneRemoteStore((event) => {
 			// ... existing subscription code ...
 		});
-	}
 
-	ipcMain.handle("phone-remote:create-session", async (event) => {
+		ipcMain.handle("phone-remote:create-session", async (event) => {
 		// ...
-	});
+		});
 
-	ipcMain.handle("phone-remote:end-session", (_event, sessionId: string) => {
+		ipcMain.handle("phone-remote:end-session", (_event, sessionId: string) => {
 		// ...
-	});
+		});
 
-	ipcMain.handle("phone-remote:send-signal", (event, sessionId: string, value: unknown) => {
+		ipcMain.handle("phone-remote:send-signal", (event, sessionId: string, value: unknown) => {
 		// ...
-	});
+		});
+	}
 }
🤖 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/register/phoneRemote.ts` around lines 84 - 101, The handler
registration in registerPhoneRemoteHandlers is not fully idempotent: the
subscribed flag only guards subscribePhoneRemoteStore but ipcMain.handle
registrations (the handlers registered in this function) will throw if called
twice; update registerPhoneRemoteHandlers to either include the
ipcMain.handle(...) calls inside the same subscribed guard (so they run only
once alongside subscribePhoneRemoteStore) or call ipcMain.removeHandler for each
IPC channel before calling ipcMain.handle (ensuring safe re-registration);
reference registerPhoneRemoteHandlers, subscribed, subscribePhoneRemoteStore and
the ipcMain.handle-registered channel names when applying the change.
src/components/launch/LaunchWindow.tsx (2)

1070-1083: 💤 Low value

Consider a non-blocking confirmation for better UX.

Using window.confirm() blocks the UI thread and can feel jarring. Consider using a toast with action buttons or a custom modal component for a smoother experience consistent with the rest of the UI.

🤖 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 1070 - 1083, Replace the
blocking window.confirm() in the LaunchWindow component with a non-blocking UI
element (e.g., existing Modal or Toast with action buttons) so the thread is not
blocked; locate the confirm call in the conditional that checks webcamSource ===
"phone" && webcamEnabled && !phoneRemote.videoActive && !phoneRemote.micActive,
and instead render your modal/toast that shows the same message and callbacks:
if the user confirms call the current logic (proceed with recording), otherwise
call setActiveDropdown("webcam") and return/abort; ensure the modal’s confirm
and cancel handlers mirror the existing behavior and keep state (e.g., showModal
boolean) local to LaunchWindow or use the app’s shared modal/toast component.

1012-1023: 💤 Low value

Side-effect coupling in toggle function.

togglePhoneMicrophone enables webcam and starts a phone session when phone mic is enabled without an existing session. This coupling might surprise users who only want microphone access. Consider separating these concerns or adding a clear UI indication that enabling phone mic will also initiate the camera session.

🤖 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 1012 - 1023, The
togglePhoneMicrophone function is doing two unrelated things—toggling the phone
mic and starting a phone camera/session via selectPhoneAsCamera when no session
exists—which surprises users; change it so togglePhoneMicrophone (referenced
symbols: togglePhoneMicrophone, phoneMicrophoneEnabled, phoneRemote.session,
setPhoneMicrophoneEnabled, setMicrophoneEnabled) only flips mic state and
updates setMicrophoneEnabled when enabling, and remove the automatic void
selectPhoneAsCamera() call; instead introduce a separate explicit action (e.g.,
startPhoneSession or a UI confirmation) that calls selectPhoneAsCamera when the
user intends to start the phone camera/session, or show a prompt before calling
selectPhoneAsCamera if a session is missing.
🤖 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/phoneRemote/server.ts`:
- Around line 126-145: The current block handling offer/answer allows mismatched
envelope types (e.g., record.type === "offer" but description.type ===
"answer"), so update the validation in the offer/answer branch (the code that
calls asRecord(record.description) and inspects description.type/sdp) to also
require description.type === record.type and return null if they differ; adjust
the checks around description.type and description.sdp in the offer/answer
branch (the logic around record.type, description, and their return object) so
only matching type pairs are considered valid.
- Around line 391-423: The timeout handler for TUNNEL_READY_TIMEOUT_MS must also
terminate the spawned cloudflared process to avoid leaking a background child;
update the timeout callback (the one that sets tunnelError = "Secure tunnel did
not become ready in time." and calls settle(null)) to check tunnelProcess,
remove or ignore its listeners (stdout/stderr 'data', 'error', 'exit'), attempt
to gracefully kill it (e.g., tunnelProcess.kill() / fallback to force kill), and
null out tunnelProcess after killing, then call settle(null); ensure the rest of
the handlers (inspectOutput, tunnelProcess.once('error'),
tunnelProcess.once('exit')) still tolerate the case where tunnelProcess has been
killed/nullified so they don't re-settle.

In `@electron/phoneRemote/sessionStore.ts`:
- Around line 168-180: updatePhoneRemoteStatus currently updates status fields
but never refreshes the session TTL causing live sessions to expire after 10
minutes; update the function (updatePhoneRemoteStatus) to also set
session.expiresAt to Date.now() plus the session TTL whenever a heartbeat/status
arrives (use the existing SESSION_TTL / PHONE_SESSION_TTL constant if present,
otherwise set to 10*60*1000) before calling emit so active sessions won't be
pruned prematurely.
- Around line 18-25: The createPairingCode function can produce fewer than 8
chars because randomBytes(5) yields too few base64url characters and filtering
removes '-'/'_', so change createPairingCode to generate until you have 8
alphanumeric chars: repeatedly call crypto.randomBytes (e.g., in a loop) to
append more base64url output, filter with .replace(/[^A-Z0-9]/gi, "") and
.toUpperCase(), and only .slice(0, 8) once the filtered string length is >= 8;
keep the logic inside createPairingCode so it always returns an 8-character
alphanumeric code.

In `@src/hooks/useScreenRecorder.ts`:
- Line 142: The default state for phone microphone is currently true which can
mislead users; change the initial value of phoneMicrophoneEnabled in
useScreenRecorder (the useState call that defines phoneMicrophoneEnabled and
setPhoneMicrophoneEnabled) to false, and add logic to
setPhoneMicrophoneEnabled(true) only when a phone session is actually
established (e.g., where phone connection/session is detected in the hook — call
setPhoneMicrophoneEnabled(true) in that connection-success handler and keep it
false on disconnect).

---

Nitpick comments:
In `@electron/ipc/recording/diagnostics.ts`:
- Around line 222-224: The check in diagnostics.ts that detects missing FFmpeg
is tightly coupled to an exact message string; update the detection logic around
the block that builds "message" (where it currently does
message.includes("FFmpeg binary is unavailable")) to use a tolerant match or
structured code: first, if the thrown error exposes a code (e.g., error.code ===
'FFMPEG_UNAVAILABLE' or similar) prefer that; otherwise replace the exact string
check with a case-insensitive, fuzzy test such as testing for /ffmpeg/i and one
of "unavail|missing|not found" (or using
message.toLowerCase().includes("ffmpeg") && (message.includes("unavailable") ||
message.includes("not found") || message.includes("missing"))) so the detection
still fires if wording changes.

In `@electron/ipc/register/phoneRemote.ts`:
- Around line 84-101: The handler registration in registerPhoneRemoteHandlers is
not fully idempotent: the subscribed flag only guards subscribePhoneRemoteStore
but ipcMain.handle registrations (the handlers registered in this function) will
throw if called twice; update registerPhoneRemoteHandlers to either include the
ipcMain.handle(...) calls inside the same subscribed guard (so they run only
once alongside subscribePhoneRemoteStore) or call ipcMain.removeHandler for each
IPC channel before calling ipcMain.handle (ensuring safe re-registration);
reference registerPhoneRemoteHandlers, subscribed, subscribePhoneRemoteStore and
the ipcMain.handle-registered channel names when applying the change.

In `@src/components/launch/LaunchWindow.tsx`:
- Around line 1070-1083: Replace the blocking window.confirm() in the
LaunchWindow component with a non-blocking UI element (e.g., existing Modal or
Toast with action buttons) so the thread is not blocked; locate the confirm call
in the conditional that checks webcamSource === "phone" && webcamEnabled &&
!phoneRemote.videoActive && !phoneRemote.micActive, and instead render your
modal/toast that shows the same message and callbacks: if the user confirms call
the current logic (proceed with recording), otherwise call
setActiveDropdown("webcam") and return/abort; ensure the modal’s confirm and
cancel handlers mirror the existing behavior and keep state (e.g., showModal
boolean) local to LaunchWindow or use the app’s shared modal/toast component.
- Around line 1012-1023: The togglePhoneMicrophone function is doing two
unrelated things—toggling the phone mic and starting a phone camera/session via
selectPhoneAsCamera when no session exists—which surprises users; change it so
togglePhoneMicrophone (referenced symbols: togglePhoneMicrophone,
phoneMicrophoneEnabled, phoneRemote.session, setPhoneMicrophoneEnabled,
setMicrophoneEnabled) only flips mic state and updates setMicrophoneEnabled when
enabling, and remove the automatic void selectPhoneAsCamera() call; instead
introduce a separate explicit action (e.g., startPhoneSession or a UI
confirmation) that calls selectPhoneAsCamera when the user intends to start the
phone camera/session, or show a prompt before calling selectPhoneAsCamera if a
session is missing.

In `@src/hooks/usePhoneRemoteMedia.ts`:
- Around line 334-338: The RTCPeerConnection created in createPeer (inside the
useCallback) currently uses only "stun:stun.l.google.com:19302"; update the ICE
configuration to include additional fallback STUN servers or read the iceServers
list from a configurable source (env/config) so connectivity is more robust:
modify the RTCPeerConnection constructor call in createPeer to use an array of
multiple STUN (and optionally TURN) entries or inject a configurable iceServers
value so callers can override the list.
- Around line 476-521: The async onPhoneRemoteSignal callback can observe
stale/closed refs after the effect cleanup; to fix, introduce an "active" (or
"mounted") boolean captured by the effect and flipped to false in its cleanup,
and inside the callback bail out early if active is false (and/or capture local
snapshots of sessionRef.current and peerRef.current into consts at the start of
the callback and re-check active before doing any await-bound operations).
Update the useEffect that registers window.electronAPI.onPhoneRemoteSignal (and
the code paths handling "answer" and "ice-candidate") to check the active flag
before calling peer.setRemoteDescription, peer.addIceCandidate, mutating
pendingRemoteIceCandidatesRef, or calling setStatus/setStatusDetail; reference
symbols: useEffect/onPhoneRemoteSignal, sessionRef, peerRef,
pendingRemoteIceCandidatesRef, setStatus, setStatusDetail.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a87f013-b4c3-49b8-98d4-b4a0ec448d69

📥 Commits

Reviewing files that changed from the base of the PR and between 467f1aa and e342184.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/ipc/recording/diagnostics.test.ts
  • electron/ipc/recording/diagnostics.ts
  • electron/ipc/register/phoneRemote.ts
  • electron/main.ts
  • electron/phoneRemote/mobilePage.ts
  • electron/phoneRemote/server.ts
  • electron/phoneRemote/sessionStore.test.ts
  • electron/phoneRemote/sessionStore.ts
  • electron/phoneRemote/types.ts
  • electron/preload.ts
  • src/components/launch/LaunchWindow.module.css
  • src/components/launch/LaunchWindow.tsx
  • src/hooks/usePhoneRemoteMedia.ts
  • src/hooks/useScreenRecorder.ts
  • src/lib/simpleQr.ts
  • vite.config.ts

Comment thread electron/phoneRemote/server.ts
Comment thread electron/phoneRemote/server.ts
Comment thread electron/phoneRemote/sessionStore.ts
Comment thread electron/phoneRemote/sessionStore.ts
Comment thread src/hooks/useScreenRecorder.ts Outdated
Comment thread src/lib/simpleQr.ts
@xsourabhsharma
Copy link
Copy Markdown
Author

@coderabbitai Thanks for the review. I pushed a follow-up that resolves the merge conflict with latest main and addresses the actionable review items:

  • added SDP offer/answer type validation on both phone and laptop signaling paths
  • made phone-remote IPC registration idempotent
  • cleaned up timed-out cloudflared tunnel child processes
  • strengthened pairing-code generation and refreshed session TTL on heartbeats
  • kept phone mic enabled by default only after selecting the phone-camera flow
  • added fallback STUN servers and an async cleanup guard for phone remote media
  • tightened QR mask scoring and FFmpeg-unavailable detection

Verified locally with npx tsc --noEmit, npm run test, npm run i18n:check, npx vite build --config vite.config.ts, and targeted Biome checks. The phone camera/mic recording flow was also manually tested successfully.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@xsourabhsharma Thanks for the thorough follow-up — the list of fixes is well-rounded and covers the key concerns from the initial review:

  • SDP validation on both paths closes the injection risk for malformed signaling payloads.
  • Idempotent IPC registration prevents duplicate handler warnings on hot reloads.
  • Cloudflared process cleanup avoids zombie child processes on tunnel timeout.
  • Strengthened pairing-code generation + TTL refresh improves both entropy and session longevity for active connections.
  • Phone-mic default only after camera selection is the right UX behaviour — avoids surprising the user.
  • Fallback STUN servers + async cleanup guard improves connectivity resilience and prevents use-after-unmount races.
  • QR mask scoring + FFmpeg-unavailable detection tightens correctness in edge cases.

I'll kick off a fresh review to verify the follow-up commits against the outstanding items.

[review]

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 (1)
src/components/video-editor/projectPersistence.ts (1)

331-333: ⚡ Quick win

Please add a regression test for the new overlap fallback clamp.

This logic fix is good; adding a focused test for “invalid zoomInOverlapMs + very small zoomInDurationMs” will lock in the behavior and prevent backslides.

🤖 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 331 - 333,
Add a regression test that exercises the fallback/clamping behavior around
normalizedZoomInOverlapMs: create a case where editor.zoomInOverlapMs is invalid
(e.g., NaN or Infinity) and zoomInDurationMs is very small so
normalizedZoomInDurationMs < DEFAULT_ZOOM_IN_OVERLAP_MS, then assert that
normalizedZoomInOverlapMs equals clamp(DEFAULT_ZOOM_IN_OVERLAP_MS, 0,
normalizedZoomInDurationMs) (i.e., it gets clamped down to the small duration).
Use the same helper or function that computes normalizedZoomInOverlapMs
(referencing editor.zoomInOverlapMs, normalizedZoomInDurationMs,
DEFAULT_ZOOM_IN_OVERLAP_MS and clamp) to ensure the behavior is locked in.
🤖 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/phoneRemote/sessionStore.ts`:
- Around line 77-97: When creating a new session in createPhoneRemoteSession,
ensure the generated pairing code is unique among currently active sessions
before storing: after pruneExpiredPhoneRemoteSessions(now) call, generate a code
with createPairingCode(), then check sessions (the sessions Map) for any
non-expired session whose code equals that code; if a collision exists,
re-generate the code (loop until unique) and only then construct the
PhoneRemoteSession object, set it in sessions, and return
toPublicSession(session). Keep using session.id = crypto.randomUUID(),
ownerWebContentsId, createdAt/expiresAt, and status/laptopSignals/phoneSignals
as before.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 554-575: The sidecar recorder started by
startMicrophoneSidecarRecorder (micFallbackRecorder, micFallbackChunks,
micFallbackStartDelayMs) is not hooked into the native lifecycle; update
pauseRecording and resumeRecording to call micFallbackRecorder.current?.pause()
and micFallbackRecorder.current?.resume() respectively (guarding for null and
MediaRecorder state), and modify cancelRecording to stop and cleanup the
micFallbackRecorder (call stop(), remove ondataavailable, clear
micFallbackChunks.current and micFallbackRecorder.current) and also stop
underlying AudioTrack(s) if any remain; ensure these branches mirror the main
recorder lifecycle handling so paused sections aren’t captured and cancel fully
terminates microphone capture.
- Around line 1298-1324: The fallback currently auto-selects sources[0] after
window.electronAPI.getSources when the portal flow fails; instead abort and
re-prompt the user for a source selection. Replace the automatic selection of
source = sources[0] (and the subsequent navigator.mediaDevices.getUserMedia
call) with logic that either throws a clear error or invokes the existing
source-selection UI/flow (e.g., call the same selection prompt used elsewhere)
so the user explicitly chooses which screen to record; keep the alert about
audio if withAudio is true and ensure you reference
window.electronAPI.getSources, withAudio, sources, and
navigator.mediaDevices.getUserMedia when making the change.

In `@src/i18n/locales/es/settings.json`:
- Around line 45-49: The listed Spanish locale entries are still in English;
update the values for keys such as "temporalZoomMotionBlur",
"temporalZoomMotionBlurDescription", "zoomMotionBlurSamples",
"zoomMotionBlurShutter", "auto" (and the additional keys in the ranges
mentioned: lines 62-75 and 93-95) with proper Spanish translations so the UI
reads in Spanish; locate these keys in src/i18n/locales/es/settings.json and
replace the English strings with concise Spanish equivalents (e.g., "Temporal
Zoom Blur" → "Desenfoque de zoom temporal", "Blur Samples" → "Muestras de
desenfoque", "Shutter" → "Obturador", "Auto" → "Automático") while keeping
punctuation and JSON structure intact.

In `@src/i18n/locales/nl/settings.json`:
- Around line 45-49: Replace the English strings in the Dutch locale with proper
Dutch translations: update keys like "temporalZoomMotionBlur",
"temporalZoomMotionBlurDescription", "zoomMotionBlurSamples",
"zoomMotionBlurShutter", and "auto" to Dutch equivalents and similarly localize
other affected keys referenced in the comment (rows around keys in the same file
at the ranges mentioned) so the settings UI is fully Dutch; edit the values for
those keys in src/i18n/locales/nl/settings.json (e.g., translate labels,
descriptions, motion preset texts, and cursor spring labels) ensuring the JSON
values are in Dutch and preserve the exact keys.

In `@src/i18n/locales/pt-BR/settings.json`:
- Around line 45-49: The listed pt-BR locale entries are still in English;
update the values for the keys temporalZoomMotionBlur,
temporalZoomMotionBlurDescription, zoomMotionBlurSamples, zoomMotionBlurShutter,
auto (and the other untranslated ranges referenced at lines 62-75 and 93-95) to
Portuguese equivalents so the UI is fully localized; locate those keys in
src/i18n/locales/pt-BR/settings.json and replace the English strings with
appropriate Portuguese translations while preserving the JSON keys and
punctuation.

In `@src/i18n/locales/zh-CN/settings.json`:
- Around line 45-49: The Chinese locale file contains untranslated English
values for keys temporalZoomMotionBlur, temporalZoomMotionBlurDescription,
zoomMotionBlurSamples, zoomMotionBlurShutter, and auto; replace the English
strings with appropriate Simplified Chinese translations for each key so the UI
is fully localized (update "temporalZoomMotionBlur" label,
"temporalZoomMotionBlurDescription" description, "zoomMotionBlurSamples" label,
"zoomMotionBlurShutter" label, and "auto" value).

In `@src/i18n/locales/zh-TW/settings.json`:
- Around line 45-49: The listed zh-TW locale entries are still in
English—translate the keys "temporalZoomMotionBlur",
"temporalZoomMotionBlurDescription", "zoomMotionBlurSamples",
"zoomMotionBlurShutter", and "auto" into Traditional Chinese (and likewise
translate other untranslated strings in this file). Update the JSON values with
proper Traditional Chinese text, preserving the exact JSON keys, punctuation and
any escape sequences; keep the surrounding quotes and commas intact and ensure
the strings are natural for UI use.

---

Nitpick comments:
In `@src/components/video-editor/projectPersistence.ts`:
- Around line 331-333: Add a regression test that exercises the
fallback/clamping behavior around normalizedZoomInOverlapMs: create a case where
editor.zoomInOverlapMs is invalid (e.g., NaN or Infinity) and zoomInDurationMs
is very small so normalizedZoomInDurationMs < DEFAULT_ZOOM_IN_OVERLAP_MS, then
assert that normalizedZoomInOverlapMs equals clamp(DEFAULT_ZOOM_IN_OVERLAP_MS,
0, normalizedZoomInDurationMs) (i.e., it gets clamped down to the small
duration). Use the same helper or function that computes
normalizedZoomInOverlapMs (referencing editor.zoomInOverlapMs,
normalizedZoomInDurationMs, DEFAULT_ZOOM_IN_OVERLAP_MS and clamp) to ensure the
behavior is locked in.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5007818b-b3c8-48c0-b58b-42047dc5f6e4

📥 Commits

Reviewing files that changed from the base of the PR and between e342184 and 2e84bbb.

📒 Files selected for processing (20)
  • electron/electron-env.d.ts
  • electron/ipc/recording/diagnostics.ts
  • electron/ipc/register/phoneRemote.ts
  • electron/phoneRemote/server.ts
  • electron/phoneRemote/sessionStore.test.ts
  • electron/phoneRemote/sessionStore.ts
  • electron/preload.ts
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/editorPreferences.test.ts
  • src/components/video-editor/projectPersistence.ts
  • src/hooks/usePhoneRemoteMedia.ts
  • src/hooks/useScreenRecorder.ts
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/ko/settings.json
  • src/i18n/locales/nl/settings.json
  • src/i18n/locales/pt-BR/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-TW/settings.json
  • src/lib/simpleQr.ts
✅ Files skipped from review due to trivial changes (6)
  • src/components/video-editor/SettingsPanel.tsx
  • electron/phoneRemote/sessionStore.test.ts
  • src/i18n/locales/ko/settings.json
  • src/i18n/locales/fr/settings.json
  • src/lib/simpleQr.ts
  • electron/ipc/register/phoneRemote.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • electron/preload.ts
  • electron/ipc/recording/diagnostics.ts
  • electron/phoneRemote/server.ts
  • src/hooks/usePhoneRemoteMedia.ts

Comment on lines +77 to +97
export function createPhoneRemoteSession(
ownerWebContentsId: number,
urls: PhoneRemoteJoinUrls,
now = Date.now(),
): PhoneRemotePublicSession {
pruneExpiredPhoneRemoteSessions(now);

const session: PhoneRemoteSession = {
...urls,
id: crypto.randomUUID(),
code: createPairingCode(),
createdAt: now,
expiresAt: now + SESSION_TTL_MS,
ownerWebContentsId,
status: "waiting",
laptopSignals: [],
phoneSignals: [],
};

sessions.set(session.id, session);
return toPublicSession(session);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce active-session pairing code uniqueness before storing the session.

Right now, a rare code collision can create two active sessions with the same code, and code-based lookup will resolve to whichever entry is encountered first. That can mis-pair devices.

Suggested minimal fix
+function isPairingCodeInUse(code: string): boolean {
+	for (const session of sessions.values()) {
+		if (session.code === code) {
+			return true;
+		}
+	}
+	return false;
+}
+
 export function createPhoneRemoteSession(
 	ownerWebContentsId: number,
 	urls: PhoneRemoteJoinUrls,
 	now = Date.now(),
 ): PhoneRemotePublicSession {
 	pruneExpiredPhoneRemoteSessions(now);
+
+	let pairingCode = createPairingCode();
+	while (isPairingCodeInUse(pairingCode)) {
+		pairingCode = createPairingCode();
+	}
 
 	const session: PhoneRemoteSession = {
 		...urls,
 		id: crypto.randomUUID(),
-		code: createPairingCode(),
+		code: pairingCode,
 		createdAt: now,
 		expiresAt: now + SESSION_TTL_MS,
 		ownerWebContentsId,
 		status: "waiting",
🤖 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/phoneRemote/sessionStore.ts` around lines 77 - 97, When creating a
new session in createPhoneRemoteSession, ensure the generated pairing code is
unique among currently active sessions before storing: after
pruneExpiredPhoneRemoteSessions(now) call, generate a code with
createPairingCode(), then check sessions (the sessions Map) for any non-expired
session whose code equals that code; if a collision exists, re-generate the code
(loop until unique) and only then construct the PhoneRemoteSession object, set
it in sessions, and return toPublicSession(session). Keep using session.id =
crypto.randomUUID(), ownerWebContentsId, createdAt/expiresAt, and
status/laptopSignals/phoneSignals as before.

Comment on lines +554 to +575
const startMicrophoneSidecarRecorder = useCallback(
(audioStreamForSidecar: MediaStream | null, mainStartedAt: number) => {
const audioTrack = audioStreamForSidecar?.getAudioTracks()[0];
if (!audioTrack) {
return false;
}

micFallbackChunks.current = [];
const recorder = new MediaRecorder(new MediaStream([audioTrack]), {
mimeType: "audio/webm;codecs=opus",
});
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
micFallbackChunks.current.push(event.data);
}
};
micFallbackStartDelayMs.current = Math.max(0, Date.now() - mainStartedAt);
recorder.start(RECORDER_TIMESLICE_MS);
micFallbackRecorder.current = recorder;
return true;
},
[],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Integrate the sidecar recorder with the native pause/cancel lifecycle.

This new recorder is started for phone/fallback mic capture, but pauseRecording() / resumeRecording() never pause it and the native cancelRecording() branch returns without stopping it. The result is that paused sections still get recorded into the sidecar, and cancel can leave microphone capture running after the user thinks recording has ended.

Also applies to: 1225-1230

🤖 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 554 - 575, The sidecar recorder
started by startMicrophoneSidecarRecorder (micFallbackRecorder,
micFallbackChunks, micFallbackStartDelayMs) is not hooked into the native
lifecycle; update pauseRecording and resumeRecording to call
micFallbackRecorder.current?.pause() and micFallbackRecorder.current?.resume()
respectively (guarding for null and MediaRecorder state), and modify
cancelRecording to stop and cleanup the micFallbackRecorder (call stop(), remove
ondataavailable, clear micFallbackChunks.current and
micFallbackRecorder.current) and also stop underlying AudioTrack(s) if any
remain; ensure these branches mirror the main recorder lifecycle handling so
paused sections aren’t captured and cancel fully terminates microphone capture.

Comment on lines +1298 to +1324
if (withAudio) {
alert(
"System audio is not supported in fallback mode. Recording will continue without audio.",
);
}

const sources = await window.electronAPI.getSources({ types: ["screen"] });

if (!sources.length) {
throw new Error("No screen sources available");
}

const source = sources[0];
console.log("Using fallback source:", source);

return await navigator.mediaDevices.getUserMedia({
audio: false, //intentional
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: source.id,
maxWidth: TARGET_WIDTH,
maxHeight: TARGET_HEIGHT,
maxFrameRate: TARGET_FRAME_RATE,
},
},
} as unknown as MediaStreamConstraints);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't silently fall back to the first Linux screen source.

When the portal flow fails, this code records sources[0]. On a multi-monitor setup that can capture a completely different display than the user intended, and the only user-facing message is about losing system audio. Please abort here and re-prompt for source selection instead of choosing an arbitrary source.

🤖 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 1298 - 1324, The fallback
currently auto-selects sources[0] after window.electronAPI.getSources when the
portal flow fails; instead abort and re-prompt the user for a source selection.
Replace the automatic selection of source = sources[0] (and the subsequent
navigator.mediaDevices.getUserMedia call) with logic that either throws a clear
error or invokes the existing source-selection UI/flow (e.g., call the same
selection prompt used elsewhere) so the user explicitly chooses which screen to
record; keep the alert about audio if withAudio is true and ensure you reference
window.electronAPI.getSources, withAudio, sources, and
navigator.mediaDevices.getUserMedia when making the change.

Comment on lines +45 to +49
"temporalZoomMotionBlur": "Temporal Zoom Blur",
"temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.",
"zoomMotionBlurSamples": "Blur Samples",
"zoomMotionBlurShutter": "Shutter",
"auto": "Auto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New es locale strings are currently English.

Please localize these newly introduced entries (temporal blur, motion presets, cursor spring labels) to keep Spanish UX consistent.

Also applies to: 62-75, 93-95

🤖 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/es/settings.json` around lines 45 - 49, The listed Spanish
locale entries are still in English; update the values for keys such as
"temporalZoomMotionBlur", "temporalZoomMotionBlurDescription",
"zoomMotionBlurSamples", "zoomMotionBlurShutter", "auto" (and the additional
keys in the ranges mentioned: lines 62-75 and 93-95) with proper Spanish
translations so the UI reads in Spanish; locate these keys in
src/i18n/locales/es/settings.json and replace the English strings with concise
Spanish equivalents (e.g., "Temporal Zoom Blur" → "Desenfoque de zoom temporal",
"Blur Samples" → "Muestras de desenfoque", "Shutter" → "Obturador", "Auto" →
"Automático") while keeping punctuation and JSON structure intact.

Comment on lines +45 to +49
"temporalZoomMotionBlur": "Temporal Zoom Blur",
"temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.",
"zoomMotionBlurSamples": "Blur Samples",
"zoomMotionBlurShutter": "Shutter",
"auto": "Auto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Several newly added nl locale entries are still in English.

Temporal blur labels, motion preset texts, and cursor spring labels should be localized to Dutch to avoid mixed-language settings UI.

Also applies to: 62-75, 93-95

🤖 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/nl/settings.json` around lines 45 - 49, Replace the English
strings in the Dutch locale with proper Dutch translations: update keys like
"temporalZoomMotionBlur", "temporalZoomMotionBlurDescription",
"zoomMotionBlurSamples", "zoomMotionBlurShutter", and "auto" to Dutch
equivalents and similarly localize other affected keys referenced in the comment
(rows around keys in the same file at the ranges mentioned) so the settings UI
is fully Dutch; edit the values for those keys in
src/i18n/locales/nl/settings.json (e.g., translate labels, descriptions, motion
preset texts, and cursor spring labels) ensuring the JSON values are in Dutch
and preserve the exact keys.

Comment on lines +45 to +49
"temporalZoomMotionBlur": "Temporal Zoom Blur",
"temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.",
"zoomMotionBlurSamples": "Blur Samples",
"zoomMotionBlurShutter": "Shutter",
"auto": "Auto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New pt-BR locale strings are still English.

Please translate these added entries (temporal blur, motion presets, cursor spring labels) to Portuguese to avoid mixed-language UI.

Also applies to: 62-75, 93-95

🤖 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/pt-BR/settings.json` around lines 45 - 49, The listed pt-BR
locale entries are still in English; update the values for the keys
temporalZoomMotionBlur, temporalZoomMotionBlurDescription,
zoomMotionBlurSamples, zoomMotionBlurShutter, auto (and the other untranslated
ranges referenced at lines 62-75 and 93-95) to Portuguese equivalents so the UI
is fully localized; locate those keys in src/i18n/locales/pt-BR/settings.json
and replace the English strings with appropriate Portuguese translations while
preserving the JSON keys and punctuation.

Comment on lines +45 to +49
"temporalZoomMotionBlur": "Temporal Zoom Blur",
"temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.",
"zoomMotionBlurSamples": "Blur Samples",
"zoomMotionBlurShutter": "Shutter",
"auto": "Auto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New zh-CN strings are untranslated (English values).

These added effects labels/descriptions are currently English, which causes mixed-language UI in the Chinese locale.

🤖 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-CN/settings.json` around lines 45 - 49, The Chinese
locale file contains untranslated English values for keys
temporalZoomMotionBlur, temporalZoomMotionBlurDescription,
zoomMotionBlurSamples, zoomMotionBlurShutter, and auto; replace the English
strings with appropriate Simplified Chinese translations for each key so the UI
is fully localized (update "temporalZoomMotionBlur" label,
"temporalZoomMotionBlurDescription" description, "zoomMotionBlurSamples" label,
"zoomMotionBlurShutter" label, and "auto" value).

Comment on lines +45 to +49
"temporalZoomMotionBlur": "Temporal Zoom Blur",
"temporalZoomMotionBlurDescription": "Control the shutter window and frame samples used by the newer zoom blur pass.",
"zoomMotionBlurSamples": "Blur Samples",
"zoomMotionBlurShutter": "Shutter",
"auto": "Auto",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New zh-TW entries are untranslated (left in English).

These newly added settings strings should be localized to Traditional Chinese for a consistent locale experience.

Also applies to: 62-75, 93-95

🤖 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` around lines 45 - 49, The listed zh-TW
locale entries are still in English—translate the keys "temporalZoomMotionBlur",
"temporalZoomMotionBlurDescription", "zoomMotionBlurSamples",
"zoomMotionBlurShutter", and "auto" into Traditional Chinese (and likewise
translate other untranslated strings in this file). Update the JSON values with
proper Traditional Chinese text, preserving the exact JSON keys, punctuation and
any escape sequences; keep the surrounding quotes and commas intact and ensure
the strings are natural for UI use.

@xsourabhsharma
Copy link
Copy Markdown
Author

Follow-up: upstream main moved again after the previous push, so I merged the latest base into this branch and pushed 8b7614c. GitHub now reports the PR as mergeable. Re-verified locally with npx tsc --noEmit, npm run test (364 passed), npm run i18n:check, targeted Biome checks, and npx vite build --config vite.config.ts.

@xsourabhsharma
Copy link
Copy Markdown
Author

Follow-up fix pushed in e278c31: the Electron main process bundle was being emitted as ESM but named main.cjs, which caused local npm run dev to fail with SyntaxError: Cannot use import statement outside a module. The package entry now points to dist-electron/main.mjs, and the Vite Electron main output now emits .mjs to match the repo's type: module setup.

Verified locally with npx vite build --config vite.config.ts, a short npm run dev smoke test, npx tsc --noEmit, and targeted Biome checks for package.json and vite.config.ts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant